diff options
Diffstat (limited to 'spec')
34 files changed, 1338 insertions, 40 deletions
diff --git a/spec/controllers/admin/clusters/applications_controller_spec.rb b/spec/controllers/admin/clusters/applications_controller_spec.rb new file mode 100644 index 00000000000..76f261e7d3f --- /dev/null +++ b/spec/controllers/admin/clusters/applications_controller_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::Clusters::ApplicationsController do + include AccessMatchersForController + + def current_application + Clusters::Cluster::APPLICATIONS[application] + end + + shared_examples 'a secure endpoint' do + it { expect { subject }.to be_allowed_for(:admin) } + it { expect { subject }.to be_denied_for(:user) } + it { expect { subject }.to be_denied_for(:external) } + + context 'when instance clusters are disabled' do + before do + stub_feature_flags(instance_clusters: false) + end + + it 'returns 404' do + is_expected.to have_http_status(:not_found) + end + end + end + + let(:cluster) { create(:cluster, :instance, :provided_by_gcp) } + + describe 'POST create' do + subject do + post :create, params: params + end + + let(:application) { 'helm' } + let(:params) { { application: application, id: cluster.id } } + + describe 'functionality' do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + it 'schedule an application installation' do + expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once + + expect { subject }.to change { current_application.count } + expect(response).to have_http_status(:no_content) + expect(cluster.application_helm).to be_scheduled + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it 'return 404' do + expect { subject }.not_to change { current_application.count } + expect(response).to have_http_status(:not_found) + end + end + + context 'when application is unknown' do + let(:application) { 'unkwnown-app' } + + it 'return 404' do + is_expected.to have_http_status(:not_found) + end + end + + context 'when application is already installing' do + before do + create(:clusters_applications_helm, :installing, cluster: cluster) + end + + it 'returns 400' do + is_expected.to have_http_status(:bad_request) + end + end + end + + describe 'security' do + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end + + describe 'PATCH update' do + subject do + patch :update, params: params + end + + let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } } + + describe 'functionality' do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + context "when cluster and app exists" do + it "schedules an application update" do + expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_cert_manager).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + it { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(ClusterPatchAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end +end diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb new file mode 100644 index 00000000000..7b77cb186a4 --- /dev/null +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -0,0 +1,540 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::ClustersController do + include AccessMatchersForController + include GoogleApi::CloudPlatformHelpers + + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe 'GET #index' do + def get_index(params = {}) + get :index, params: params + end + + context 'when feature flag is not enabled' do + before do + stub_feature_flags(instance_clusters: false) + end + + it 'responds with not found' do + get_index + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when feature flag is enabled' do + before do + stub_feature_flags(instance_clusters: true) + end + + describe 'functionality' do + context 'when instance has one or more clusters' do + let!(:enabled_cluster) do + create(:cluster, :provided_by_gcp, :instance) + end + + let!(:disabled_cluster) do + create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance) + end + + it 'lists available clusters' do + get_index + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster]) + end + + context 'when page is specified' do + let(:last_page) { Clusters::Cluster.instance_type.page.total_pages } + + before do + allow(Clusters::Cluster).to receive(:paginates_per).and_return(1) + create_list(:cluster, 2, :provided_by_gcp, :production_environment, :instance) + end + + it 'redirects to the page' do + get_index(page: last_page) + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:clusters).current_page).to eq(last_page) + end + end + end + + context 'when instance does not have a cluster' do + it 'returns an empty state page' do + get_index + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index, partial: :empty_state) + expect(assigns(:clusters)).to eq([]) + end + end + end + end + + describe 'security' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { expect { get_index }.to be_allowed_for(:admin) } + it { expect { get_index }.to be_denied_for(:user) } + it { expect { get_index }.to be_denied_for(:external) } + end + end + + describe 'GET #new' do + def get_new + get :new + end + + describe 'functionality for new cluster' do + context 'when omniauth has been configured' do + let(:key) { 'secret-key' } + let(:session_key_for_redirect_uri) do + GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key) + end + + before do + allow(SecureRandom).to receive(:hex).and_return(key) + end + + it 'has authorize_url' do + get_new + + expect(assigns(:authorize_url)).to include(key) + expect(session[session_key_for_redirect_uri]).to eq(new_admin_cluster_path) + end + end + + context 'when omniauth has not configured' do + before do + stub_omniauth_setting(providers: []) + end + + it 'does not have authorize_url' do + get_new + + expect(assigns(:authorize_url)).to be_nil + end + end + + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'has new object' do + get_new + + expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter) + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(@valid_gcp_token).to be_falsey } + end + + context 'when access token is not stored in session' do + it { expect(@valid_gcp_token).to be_falsey } + end + end + + describe 'functionality for existing cluster' do + it 'has new object' do + get_new + + expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter) + end + end + + describe 'security' do + it { expect { get_new }.to be_allowed_for(:admin) } + it { expect { get_new }.to be_denied_for(:user) } + it { expect { get_new }.to be_denied_for(:external) } + end + end + + describe 'POST #create_gcp' do + let(:legacy_abac_param) { 'true' } + let(:params) do + { + cluster: { + name: 'new-cluster', + provider_gcp_attributes: { + gcp_project_id: 'gcp-project-12345', + legacy_abac: legacy_abac_param + } + } + } + end + + def post_create_gcp + post :create_gcp, params: params + end + + describe 'functionality' do + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { post_create_gcp }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + + cluster = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(cluster).to be_gcp + expect(cluster).to be_kubernetes + expect(cluster.provider_gcp).to be_legacy_abac + end + + context 'when legacy_abac param is false' do + let(:legacy_abac_param) { 'false' } + + it 'creates a new cluster with legacy_abac_disabled' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { post_create_gcp }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + expect(Clusters::Cluster.instance_type.first.provider_gcp).not_to be_legacy_abac + end + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(@valid_gcp_token).to be_falsey } + end + + context 'when access token is not stored in session' do + it { expect(@valid_gcp_token).to be_falsey } + end + end + + describe 'security' do + before do + allow_any_instance_of(described_class) + .to receive(:token_in_session).and_return('token') + allow_any_instance_of(described_class) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + 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) + end + + it { expect { post_create_gcp }.to be_allowed_for(:admin) } + it { expect { post_create_gcp }.to be_denied_for(:user) } + it { expect { post_create_gcp }.to be_denied_for(:external) } + end + end + + describe 'POST #create_user' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test' + } + } + } + end + + def post_create_user + post :create_user, params: params + end + + describe 'functionality' do + context 'when creates a cluster' do + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { post_create_user }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + + cluster = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(cluster).to be_user + expect(cluster).to be_kubernetes + end + end + + context 'when creates a RBAC-enabled cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + authorization_type: 'rbac' + } + } + } + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { post_create_user }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + + cluster = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(cluster).to be_user + expect(cluster).to be_kubernetes + expect(cluster).to be_platform_kubernetes_rbac + end + end + end + + describe 'security' do + it { expect { post_create_user }.to be_allowed_for(:admin) } + it { expect { post_create_user }.to be_denied_for(:user) } + it { expect { post_create_user }.to be_denied_for(:external) } + end + end + + describe 'GET #cluster_status' do + let(:cluster) { create(:cluster, :providing_by_gcp, :instance) } + + def get_cluster_status + get :cluster_status, + params: { + id: cluster + }, + format: :json + end + + describe 'functionality' do + it 'responds with matching schema' do + get_cluster_status + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('cluster_status') + end + + it 'invokes schedule_status_update on each application' do + expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update) + + get_cluster_status + end + end + + describe 'security' do + it { expect { get_cluster_status }.to be_allowed_for(:admin) } + it { expect { get_cluster_status }.to be_denied_for(:user) } + it { expect { get_cluster_status }.to be_denied_for(:external) } + end + end + + describe 'GET #show' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + def get_show + get :show, + params: { + id: cluster + } + end + + describe 'functionality' do + it 'responds successfully' do + get_show + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:cluster)).to eq(cluster) + end + end + + describe 'security' do + it { expect { get_show }.to be_allowed_for(:admin) } + it { expect { get_show }.to be_denied_for(:user) } + it { expect { get_show }.to be_denied_for(:external) } + end + end + + describe 'PUT #update' do + def put_update(format: :html) + put :update, params: params.merge( + id: cluster, + format: format + ) + end + + let(:cluster) { create(:cluster, :provided_by_user, :instance) } + let(:domain) { 'test-domain.com' } + + let(:params) do + { + cluster: { + enabled: false, + name: 'my-new-cluster-name', + base_domain: domain + } + } + end + + it 'updates and redirects back to show page' do + put_update + + cluster.reload + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') + expect(cluster.enabled).to be_falsey + expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster.domain).to eq('test-domain.com') + end + + context 'when domain is invalid' do + let(:domain) { 'http://not-a-valid-domain' } + + it 'does not update cluster attributes' do + put_update + + cluster.reload + expect(response).to render_template(:show) + expect(cluster.name).not_to eq('my-new-cluster-name') + expect(cluster.domain).not_to eq('test-domain.com') + end + end + + context 'when format is json' do + context 'when changing parameters' do + context 'when valid parameters are used' do + let(:params) do + { + cluster: { + enabled: false, + name: 'my-new-cluster-name', + domain: domain + } + } + end + + it 'updates and redirects back to show page' do + put_update(format: :json) + + cluster.reload + expect(response).to have_http_status(:no_content) + expect(cluster.enabled).to be_falsey + expect(cluster.name).to eq('my-new-cluster-name') + end + end + + context 'when invalid parameters are used' do + let(:params) do + { + cluster: { + enabled: false, + name: '' + } + } + end + + it 'rejects changes' do + put_update(format: :json) + + expect(response).to have_http_status(:bad_request) + end + end + end + end + + describe 'security' do + set(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { expect { put_update }.to be_allowed_for(:admin) } + it { expect { put_update }.to be_denied_for(:user) } + it { expect { put_update }.to be_denied_for(:external) } + end + end + + describe 'DELETE #destroy' do + let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) } + + def delete_destroy + delete :destroy, + params: { + id: cluster + } + end + + describe 'functionality' do + context 'when cluster is provided by GCP' do + context 'when cluster is created' do + it 'destroys and redirects back to clusters list' do + expect { delete_destroy } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Platforms::Kubernetes.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(-1) + + expect(response).to redirect_to(admin_clusters_path) + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') + end + end + + context 'when cluster is being created' do + let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, :instance) } + + it 'destroys and redirects back to clusters list' do + expect { delete_destroy } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(-1) + + expect(response).to redirect_to(admin_clusters_path) + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') + end + end + end + + context 'when cluster is provided by user' do + let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, :instance) } + + it 'destroys and redirects back to clusters list' do + expect { delete_destroy } + .to change { Clusters::Cluster.count }.by(-1) + .and change { Clusters::Platforms::Kubernetes.count }.by(-1) + .and change { Clusters::Providers::Gcp.count }.by(0) + + expect(response).to redirect_to(admin_clusters_path) + expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.') + end + end + end + + describe 'security' do + set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) } + + it { expect { delete_destroy }.to be_allowed_for(:admin) } + it { expect { delete_destroy }.to be_denied_for(:user) } + it { expect { delete_destroy }.to be_denied_for(:external) } + end + end +end diff --git a/spec/controllers/concerns/enforces_admin_authentication_spec.rb b/spec/controllers/concerns/enforces_admin_authentication_spec.rb new file mode 100644 index 00000000000..e6a6702fdea --- /dev/null +++ b/spec/controllers/concerns/enforces_admin_authentication_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe EnforcesAdminAuthentication do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + controller(ApplicationController) do + # `described_class` is not available in this context + include EnforcesAdminAuthentication # rubocop:disable RSpec/DescribedClass + + def index + head :ok + end + end + + describe 'authenticate_admin!' do + context 'as an admin' do + let(:user) { create(:admin) } + + it 'renders ok' do + get :index + + expect(response).to have_gitlab_http_status(200) + end + end + + context 'as a user' do + it 'renders a 404' do + get :index + + expect(response).to have_gitlab_http_status(404) + end + end + end +end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 9d1c1e3acc7..d1ed64cce7f 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -112,6 +112,14 @@ describe 'Dashboard Projects' do expect(first('.project-row')).to have_content(project_with_most_stars.title) end + + it 'shows tabs to filter by all projects or personal' do + visit dashboard_projects_path + segmented_button = page.find('.filtered-search-nav .button-filter-group') + + expect(segmented_button).to have_content 'All' + expect(segmented_button).to have_content 'Personal' + end end context 'when on Starred projects tab', :js do @@ -134,6 +142,12 @@ describe 'Dashboard Projects' do expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1) expect(find('.nav-links li:nth-child(2) .badge-pill')).to have_content(1) end + + it 'does not show tabs to filter by all projects or personal' do + visit(starred_dashboard_projects_path) + + expect(page).not_to have_content '.filtered-search-nav' + end end describe 'with a pipeline', :clean_gitlab_redis_shared_state do diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb index cc86114e436..e0553086fd7 100644 --- a/spec/features/dashboard/user_filters_projects_spec.rb +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -14,6 +14,7 @@ describe 'Dashboard > User filters projects' do describe 'filtering personal projects' do before do + stub_feature_flags(project_list_filter_bar: false) project2.add_developer(user) visit dashboard_projects_path @@ -30,6 +31,7 @@ describe 'Dashboard > User filters projects' do describe 'filtering starred projects', :js do before do + stub_feature_flags(project_list_filter_bar: false) user.toggle_star(project) visit dashboard_projects_path @@ -42,4 +44,219 @@ describe 'Dashboard > User filters projects' do expect(page).not_to have_content('You don\'t have starred projects yet') end end + + describe 'without search bar', :js do + before do + stub_feature_flags(project_list_filter_bar: false) + + project2.add_developer(user) + visit dashboard_projects_path + end + + it 'autocompletes searches upon typing', :js do + expect(page).to have_content 'Victorialand' + expect(page).to have_content 'Treasure' + + fill_in 'project-filter-form-field', with: 'Lord beerus\n' + + expect(page).not_to have_content 'Victorialand' + expect(page).not_to have_content 'Treasure' + end + end + + describe 'with search bar', :js do + before do + stub_feature_flags(project_list_filter_bar: true) + + project2.add_developer(user) + visit dashboard_projects_path + end + + # TODO: move these helpers somewhere more useful + def click_sort_direction + page.find('.filtered-search-block #filtered-search-sorting-dropdown .reverse-sort-btn').click + end + + def select_dropdown_option(selector, label) + dropdown = page.find(selector) + dropdown.click + + dropdown.find('.dropdown-menu a', text: label, match: :first).click + end + + def expect_to_see_projects(sorted_projects) + list = page.all('.projects-list .project-name').map(&:text) + expect(list).to match(sorted_projects) + end + + describe 'Search' do + it 'executes when the search button is clicked' do + expect(page).to have_content 'Victorialand' + expect(page).to have_content 'Treasure' + + fill_in 'project-filter-form-field', with: 'Lord vegeta\n' + find('.filtered-search .btn').click + + expect(page).not_to have_content 'Victorialand' + expect(page).not_to have_content 'Treasure' + end + + it 'will execute when i press enter' do + expect(page).to have_content 'Victorialand' + expect(page).to have_content 'Treasure' + + fill_in 'project-filter-form-field', with: 'Lord frieza\n' + find('#project-filter-form-field').native.send_keys :enter + + expect(page).not_to have_content 'Victorialand' + expect(page).not_to have_content 'Treasure' + end + end + + describe 'Filter' do + before do + private_project = create(:project, :private, name: 'Private project', namespace: user.namespace) + internal_project = create(:project, :internal, name: 'Internal project', namespace: user.namespace) + + private_project.add_maintainer(user) + internal_project.add_maintainer(user) + end + + it 'filters private projects only' do + select_dropdown_option '#filtered-search-visibility-dropdown', 'Private' + + expect(current_url).to match(/visibility_level=0/) + + list = page.all('.projects-list .project-name').map(&:text) + + expect(list).to match(["Private project", "Treasure", "Victorialand"]) + end + + it 'filters internal projects only' do + select_dropdown_option '#filtered-search-visibility-dropdown', 'Internal' + + expect(current_url).to match(/visibility_level=10/) + + list = page.all('.projects-list .project-name').map(&:text) + + expect(list).to match(['Internal project']) + end + + it 'filters any project' do + select_dropdown_option '#filtered-search-visibility-dropdown', 'Any' + list = page.all('.projects-list .project-name').map(&:text) + + expect(list).to match(["Internal project", "Private project", "Treasure", "Victorialand"]) + end + end + + describe 'Sorting' do + before do + [ + { name: 'Red ribbon army', created_at: 2.days.ago }, + { name: 'Cell saga', created_at: Time.now }, + { name: 'Frieza saga', created_at: 10.days.ago } + ].each do |item| + project = create(:project, name: item[:name], namespace: user.namespace, created_at: item[:created_at]) + project.add_developer(user) + end + + user.toggle_star(project) + user.toggle_star(project2) + user2.toggle_star(project2) + end + + it 'includes sorting direction' do + sorting_dropdown = page.find('.filtered-search-block #filtered-search-sorting-dropdown') + + expect(sorting_dropdown).to have_css '.reverse-sort-btn' + end + + it 'has all sorting options', :js do + sorting_dropdown = page.find('.filtered-search-block #filtered-search-sorting-dropdown') + sorting_option_labels = ['Last updated', 'Created date', 'Name', 'Stars'] + + sorting_dropdown.click + + sorting_option_labels.each do |label| + expect(sorting_dropdown).to have_content(label) + end + end + + it 'defaults to "Last updated"', :js do + page.find('.filtered-search-block #filtered-search-sorting-dropdown').click + active_sorting_option = page.first('.filtered-search-block #filtered-search-sorting-dropdown .is-active') + + expect(active_sorting_option).to have_content 'Last updated' + end + + context 'Sorting by name' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Name' + + desc = ['Victorialand', 'Treasure', 'Red ribbon army', 'Frieza saga', 'Cell saga'] + asc = ['Cell saga', 'Frieza saga', 'Red ribbon army', 'Treasure', 'Victorialand'] + + click_sort_direction + + expect_to_see_projects(desc) + + click_sort_direction + + expect_to_see_projects(asc) + end + end + + context 'Sorting by Last updated' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Last updated' + + desc = ["Frieza saga", "Red ribbon army", "Victorialand", "Treasure", "Cell saga"] + asc = ["Cell saga", "Treasure", "Victorialand", "Red ribbon army", "Frieza saga"] + + click_sort_direction + + expect_to_see_projects(desc) + + click_sort_direction + + expect_to_see_projects(asc) + end + end + + context 'Sorting by Created date' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Created date' + + desc = ["Frieza saga", "Red ribbon army", "Victorialand", "Treasure", "Cell saga"] + asc = ["Cell saga", "Treasure", "Victorialand", "Red ribbon army", "Frieza saga"] + + click_sort_direction + + expect_to_see_projects(desc) + + click_sort_direction + + expect_to_see_projects(asc) + end + end + + context 'Sorting by Stars' do + it 'sorts the project list' do + select_dropdown_option '#filtered-search-sorting-dropdown', 'Stars' + + desc = ["Red ribbon army", "Cell saga", "Frieza saga", "Victorialand", "Treasure"] + asc = ["Treasure", "Victorialand", "Red ribbon army", "Cell saga", "Frieza saga"] + + click_sort_direction + + expect_to_see_projects(desc) + + click_sort_direction + + expect_to_see_projects(asc) + end + end + end + end end diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb index 7a91c64d7db..439803f9255 100644 --- a/spec/features/groups/members/leave_group_spec.rb +++ b/spec/features/groups/members/leave_group_spec.rb @@ -21,6 +21,20 @@ describe 'Groups > Members > Leave group' do expect(group.users).not_to include(user) end + it 'guest leaves the group by url param', :js do + group.add_guest(user) + group.add_owner(other_user) + + visit group_path(group, leave: 1) + + page.accept_confirm + + expect(find('.flash-notice')).to have_content "You left the \"#{group.full_name}\" group" + expect(page).to have_content left_group_message(group) + expect(current_path).to eq(dashboard_groups_path) + expect(group.users).not_to include(user) + end + it 'guest leaves the group as last member' do group.add_guest(user) @@ -32,7 +46,7 @@ describe 'Groups > Members > Leave group' do expect(group.users).not_to include(user) end - it 'owner leaves the group if they is not the last owner' do + it 'owner leaves the group if they are not the last owner' do group.add_owner(user) group.add_owner(other_user) @@ -44,7 +58,7 @@ describe 'Groups > Members > Leave group' do expect(group.users).not_to include(user) end - it 'owner can not leave the group if they is a last owner' do + it 'owner can not leave the group if they are the last owner' do group.add_owner(user) visit group_path(group) @@ -56,6 +70,14 @@ describe 'Groups > Members > Leave group' do expect(find(:css, '.project-members-page li', text: user.name)).not_to have_selector(:css, 'a.btn-remove') end + it 'owner can not leave the group by url param if they are the last owner', :js do + group.add_owner(user) + + visit group_path(group, leave: 1) + + expect(find('.flash-alert')).to have_content 'You do not have permission to leave this group' + end + def left_group_message(group) "You left the \"#{group.name}\"" end diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb index 0ab29660189..a645b917568 100644 --- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb +++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb @@ -8,10 +8,17 @@ describe 'Projects > Members > Group member cannot leave group project' do before do group.add_developer(user) sign_in(user) - visit project_path(project) end it 'user does not see a "Leave project" link' do + visit project_path(project) + expect(page).not_to have_content 'Leave project' end + + it 'renders a flash message if attempting to leave by url', :js do + visit project_path(project, leave: 1) + + expect(find('.flash-alert')).to have_content 'You do not have permission to leave this project' + end end diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index 94b29de4686..bd2ef9c07c4 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -7,13 +7,24 @@ describe 'Projects > Members > Member leaves project' do before do project.add_developer(user) sign_in(user) - visit project_path(project) end it 'user leaves project' do + visit project_path(project) + click_link 'Leave project' expect(current_path).to eq(dashboard_projects_path) expect(project.users.exists?(user.id)).to be_falsey end + + it 'user leaves project by url param', :js do + visit project_path(project, leave: 1) + + page.accept_confirm + + expect(find('.flash-notice')).to have_content "You left the \"#{project.full_name}\" project" + expect(current_path).to eq(dashboard_projects_path) + expect(project.users.exists?(user.id)).to be_falsey + end end diff --git a/spec/finders/cluster_ancestors_finder_spec.rb b/spec/finders/cluster_ancestors_finder_spec.rb index 332086c42e2..750042b6b54 100644 --- a/spec/finders/cluster_ancestors_finder_spec.rb +++ b/spec/finders/cluster_ancestors_finder_spec.rb @@ -8,11 +8,15 @@ describe ClusterAncestorsFinder, '#execute' do let(:user) { create(:user) } let!(:project_cluster) do - create(:cluster, :provided_by_user, cluster_type: :project_type, projects: [project]) + create(:cluster, :provided_by_user, :project, projects: [project]) end let!(:group_cluster) do - create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) + create(:cluster, :provided_by_user, :group, groups: [group]) + end + + let!(:instance_cluster) do + create(:cluster, :provided_by_user, :instance) end subject { described_class.new(clusterable, user).execute } @@ -25,7 +29,7 @@ describe ClusterAncestorsFinder, '#execute' do end it 'returns the project clusters followed by group clusters' do - is_expected.to eq([project_cluster, group_cluster]) + is_expected.to eq([project_cluster, group_cluster, instance_cluster]) end context 'nested groups', :nested_groups do @@ -33,11 +37,11 @@ describe ClusterAncestorsFinder, '#execute' do let(:parent_group) { create(:group) } let!(:parent_group_cluster) do - create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group]) + create(:cluster, :provided_by_user, :group, groups: [parent_group]) end it 'returns the project clusters followed by group clusters ordered ascending the hierarchy' do - is_expected.to eq([project_cluster, group_cluster, parent_group_cluster]) + is_expected.to eq([project_cluster, group_cluster, parent_group_cluster, instance_cluster]) end end end @@ -58,7 +62,7 @@ describe ClusterAncestorsFinder, '#execute' do end it 'returns the list of group clusters' do - is_expected.to eq([group_cluster]) + is_expected.to eq([group_cluster, instance_cluster]) end context 'nested groups', :nested_groups do @@ -66,12 +70,21 @@ describe ClusterAncestorsFinder, '#execute' do let(:parent_group) { create(:group) } let!(:parent_group_cluster) do - create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group]) + create(:cluster, :provided_by_user, :group, groups: [parent_group]) end it 'returns the list of group clusters ordered ascending the hierarchy' do - is_expected.to eq([group_cluster, parent_group_cluster]) + is_expected.to eq([group_cluster, parent_group_cluster, instance_cluster]) end end end + + context 'for an instance' do + let(:clusterable) { Clusters::Instance.new } + let(:user) { create(:admin) } + + it 'returns the list of instance clusters' do + is_expected.to eq([instance_cluster]) + end + end end diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js index 7c54a27d950..8bcf02f0a34 100644 --- a/spec/frontend/clusters/components/applications_spec.js +++ b/spec/frontend/clusters/components/applications_spec.js @@ -75,7 +75,7 @@ describe('Applications', () => { }); it('renders a row for Prometheus', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull(); + expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull(); }); it('renders a row for GitLab Runner', () => { diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb index e3a34762b62..9982288e206 100644 --- a/spec/graphql/resolvers/base_resolver_spec.rb +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -28,4 +28,19 @@ describe Resolvers::BaseResolver do expect(result).to eq(test: 1) end end + + it 'increases complexity based on arguments' do + field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1) + + expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 3 + expect(field.to_graphql.complexity.call({}, { search: 'foo' }, 1)).to eq 7 + end + + it 'does not increase complexity when filtering by iids' do + field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100) + + expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 6 + expect(field.to_graphql.complexity.call({}, { sort: 'foo', iid: 1 }, 1)).to eq 3 + expect(field.to_graphql.complexity.call({}, { sort: 'foo', iids: [1, 2, 3] }, 1)).to eq 3 + end end diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb index ea7159eacf9..3140af27af5 100644 --- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb +++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb @@ -46,6 +46,14 @@ describe ResolvesPipelines do expect(resolve_pipelines({}, {})).to be_empty end + it 'increases field complexity based on arguments' do + field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: false, max_page_size: 1) + + expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 2 + expect(field.to_graphql.complexity.call({}, { sha: 'foo' }, 1)).to eq 4 + expect(field.to_graphql.complexity.call({}, { sha: 'ref' }, 1)).to eq 4 + end + def resolve_pipelines(args = {}, context = { current_user: current_user }) resolve(resolver, obj: project, args: args, ctx: context) end diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 399a33dae75..bffcdbfe915 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -120,6 +120,13 @@ describe Resolvers::IssuesResolver do end end + it 'increases field complexity based on arguments' do + field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100) + + expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 4 + expect(field.to_graphql.complexity.call({}, { labelName: 'foo' }, 1)).to eq 8 + end + def resolve_issues(args = {}, context = { current_user: current_user }) resolve(described_class, obj: project, args: args, ctx: context) end diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb index d4990c6492c..4fdbb3aa43e 100644 --- a/spec/graphql/resolvers/project_resolver_spec.rb +++ b/spec/graphql/resolvers/project_resolver_spec.rb @@ -26,6 +26,14 @@ describe Resolvers::ProjectResolver do end end + it 'does not increase complexity depending on number of load limits' do + field1 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100) + field2 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1) + + expect(field1.to_graphql.complexity.call({}, {}, 1)).to eq 2 + expect(field2.to_graphql.complexity.call({}, {}, 1)).to eq 2 + end + def resolve_project(full_path) resolve(described_class, args: { full_path: full_path }) end diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index b5697ee5245..4fe426e2447 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -4,6 +4,18 @@ require 'spec_helper' describe Types::BaseField do context 'when considering complexity' do + let(:resolver) do + Class.new(described_class) do + def self.resolver_complexity(args) + 2 if args[:foo] + end + + def self.complexity_multiplier(args) + 0.01 + end + end + end + it 'defaults to 1' do field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true) @@ -15,5 +27,19 @@ describe Types::BaseField do expect(field.to_graphql.complexity).to eq 12 end + + it 'sets complexity depending on arguments for resolvers' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true) + + expect(field.to_graphql.complexity.call({}, {}, 2)).to eq 4 + expect(field.to_graphql.complexity.call({}, { first: 50 }, 2)).to eq 3 + end + + it 'sets complexity depending on number load limits for resolvers' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true) + + expect(field.to_graphql.complexity.call({}, { first: 1 }, 2)).to eq 2 + expect(field.to_graphql.complexity.call({}, { first: 1, foo: true }, 2)).to eq 4 + end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 37c63807c82..554cb861563 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -445,6 +445,10 @@ describe ProjectsHelper do Project.all end + before do + stub_feature_flags(project_list_filter_bar: false) + end + it 'returns true when there are projects' do expect(helper.show_projects?(projects, {})).to eq(true) end diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb index 1e90a2ef27f..cc09804fd53 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb @@ -105,6 +105,7 @@ describe Gitlab::BitbucketServerImport::Importer do expect(merge_request.metrics.merged_by).to eq(project.owner) expect(merge_request.metrics.merged_at).to eq(@merge_event.merge_timestamp) expect(merge_request.merge_commit_sha).to eq('12345678') + expect(merge_request.state_id).to eq(3) end it 'imports comments' do diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index 7901ae005d9..dab5767ece1 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -98,6 +98,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach description: 'This is my issue', milestone_id: milestone.id, state: :opened, + state_id: 1, created_at: created_at, updated_at: updated_at }, @@ -127,6 +128,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach description: "*Created by: alice*\n\nThis is my issue", milestone_id: milestone.id, state: :opened, + state_id: 1, created_at: created_at, updated_at: updated_at }, diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 2e4a7c36fb8..6d614c6527a 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -93,6 +93,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, + state_id: 3, milestone_id: milestone.id, author_id: user.id, assignee_id: user.id, @@ -138,6 +139,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, + state_id: 3, milestone_id: milestone.id, author_id: project.creator_id, assignee_id: user.id, @@ -184,6 +186,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_branch: 'master-42', target_branch: 'master', state: :merged, + state_id: 3, milestone_id: milestone.id, author_id: user.id, assignee_id: user.id, diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index fee1d701e3a..8f348b1b053 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -701,6 +701,8 @@ describe Notify do is_expected.to have_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.human_access + is_expected.to have_body_text 'leave the project' + is_expected.to have_body_text project_url(project, leave: 1) end end @@ -1144,6 +1146,8 @@ describe Notify do is_expected.to have_body_text group.name is_expected.to have_body_text group.web_url is_expected.to have_body_text group_member.human_access + is_expected.to have_body_text 'leave the group' + is_expected.to have_body_text group_url(group, leave: 1) end end diff --git a/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb new file mode 100644 index 00000000000..105c05bb7ca --- /dev/null +++ b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190506135400_schedule_sync_issuables_state_id_where_nil') + +describe ScheduleSyncIssuablesStateIdWhereNil, :migration, :sidekiq do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:merge_requests) { table(:merge_requests) } + let(:issues) { table(:issues) } + let(:migration) { described_class.new } + let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') } + let(:project) { projects.create!(namespace_id: group.id) } + + shared_examples 'scheduling migrations' do + before do + Sidekiq::Worker.clear_all + stub_const("#{described_class.name}::BATCH_SIZE", 2) + end + + it 'correctly schedules issuable sync background migration' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(migration).to be_scheduled_delayed_migration(120.seconds, resource_1.id, resource_3.id) + expect(migration).to be_scheduled_delayed_migration(240.seconds, resource_5.id, resource_5.id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end + + describe '#up' do + context 'issues' do + it_behaves_like 'scheduling migrations' do + let(:migration) { described_class::ISSUES_MIGRATION } + let!(:resource_1) { issues.create!(description: 'first', state: 'opened', state_id: nil) } + let!(:resource_2) { issues.create!(description: 'second', state: 'closed', state_id: 2) } + let!(:resource_3) { issues.create!(description: 'third', state: 'closed', state_id: nil) } + let!(:resource_4) { issues.create!(description: 'fourth', state: 'closed', state_id: 2) } + let!(:resource_5) { issues.create!(description: 'fifth', state: 'closed', state_id: nil) } + end + end + + context 'merge requests' do + it_behaves_like 'scheduling migrations' do + let(:migration) { described_class::MERGE_REQUESTS_MIGRATION } + let!(:resource_1) { merge_requests.create!(state: 'opened', state_id: nil, target_project_id: project.id, target_branch: 'feature1', source_branch: 'master') } + let!(:resource_2) { merge_requests.create!(state: 'closed', state_id: 2, target_project_id: project.id, target_branch: 'feature2', source_branch: 'master') } + let!(:resource_3) { merge_requests.create!(state: 'merged', state_id: nil, target_project_id: project.id, target_branch: 'feature3', source_branch: 'master') } + let!(:resource_4) { merge_requests.create!(state: 'locked', state_id: 3, target_project_id: project.id, target_branch: 'feature4', source_branch: 'master') } + let!(:resource_5) { merge_requests.create!(state: 'locked', state_id: nil, target_project_id: project.id, target_branch: 'feature4', source_branch: 'master') } + end + end + end +end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 81913f4a3b6..1bfc14d2839 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -35,6 +35,15 @@ describe Ci::PipelineSchedule do expect(pipeline_schedule).not_to be_valid end end + + context 'when cron contains trailing whitespaces' do + it 'strips the attribute' do + pipeline_schedule = build(:ci_pipeline_schedule, cron: ' 0 0 * * * ') + + expect(pipeline_schedule).to be_valid + expect(pipeline_schedule.cron).to eq('0 0 * * *') + end + end end describe '#set_next_run_at' do diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index bdc0cb8ed86..4f0cd0efe9c 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -69,8 +69,8 @@ describe Clusters::Applications::Runner do expect(values).to include('privileged: true') expect(values).to include('image: ubuntu:16.04') expect(values).to include('resources') - expect(values).to match(/runnerToken: '?#{ci_runner.token}/) - expect(values).to match(/gitlabUrl: '?#{Gitlab::Routing.url_helpers.root_url}/) + expect(values).to match(/runnerToken: '?#{Regexp.escape(ci_runner.token)}/) + expect(values).to match(/gitlabUrl: '?#{Regexp.escape(Gitlab::Routing.url_helpers.root_url)}/) end context 'without a runner' do @@ -83,7 +83,7 @@ describe Clusters::Applications::Runner do end it 'uses the new runner token' do - expect(values).to match(/runnerToken: '?#{runner.token}/) + expect(values).to match(/runnerToken: '?#{Regexp.escape(runner.token)}/) end end @@ -114,6 +114,18 @@ describe Clusters::Applications::Runner do expect(runner.groups).to eq [group] end end + + context 'instance cluster' do + let(:cluster) { create(:cluster, :with_installed_helm, :instance) } + + include_examples 'runner creation' + + it 'creates an instance runner' do + subject + + expect(runner).to be_instance_type + end + end end context 'with duplicated values on vendor/runner/values.yaml' do diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index e1506c06044..58203da5b22 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -325,6 +325,15 @@ describe Clusters::Cluster do end end + context 'when group and instance have configured kubernetes clusters' do + let(:project) { create(:project, group: group) } + let!(:instance_cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it 'returns clusters in order, descending the hierachy' do + is_expected.to eq([group_cluster, instance_cluster]) + end + end + context 'when sub-group has configured kubernetes cluster', :nested_groups do let(:sub_group_cluster) { create(:cluster, :provided_by_gcp, :group) } let(:sub_group) { sub_group_cluster.group } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 0cd69cb4817..cc777cbf749 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -55,6 +55,29 @@ describe Issue do end end + describe 'locking' do + using RSpec::Parameterized::TableSyntax + + where(:lock_version) do + [ + [0], + ["0"] + ] + end + + with_them do + it 'works when an issue has a NULL lock_version' do + issue = create(:issue) + + described_class.where(id: issue.id).update_all('lock_version = NULL') + + issue.update!(lock_version: lock_version, title: 'locking test') + + expect(issue.reload.title).to eq('locking test') + end + end + end + describe '#order_by_position_and_priority' do let(:project) { create :project } let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 7457efef013..c72b6e9033d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -31,6 +31,29 @@ describe MergeRequest do end end + describe 'locking' do + using RSpec::Parameterized::TableSyntax + + where(:lock_version) do + [ + [0], + ["0"] + ] + end + + with_them do + it 'works when a merge request has a NULL lock_version' do + merge_request = create(:merge_request) + + described_class.where(id: merge_request.id).update_all('lock_version = NULL') + + merge_request.update!(lock_version: lock_version, title: 'locking test') + + expect(merge_request.reload.title).to eq('locking test') + end + end + end + describe '#squash_in_progress?' do let(:repo_path) do Gitlab::GitalyClient::StorageSettings.allow_disk_access do diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb index b2f0ca1bc30..cc3dde154dc 100644 --- a/spec/policies/clusters/cluster_policy_spec.rb +++ b/spec/policies/clusters/cluster_policy_spec.rb @@ -66,5 +66,21 @@ describe Clusters::ClusterPolicy, :models do it { expect(policy).to be_disallowed :admin_cluster } end end + + context 'instance cluster' do + let(:cluster) { create(:cluster, :instance) } + + context 'when user' do + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + + context 'when admin' do + let(:user) { create(:admin) } + + it { expect(policy).to be_allowed :update_cluster } + it { expect(policy).to be_allowed :admin_cluster } + end + end end end diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb new file mode 100644 index 00000000000..9d755c6d29d --- /dev/null +++ b/spec/policies/clusters/instance_policy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::InstancePolicy do + let(:user) { create(:user) } + let(:policy) { described_class.new(user, Clusters::Instance.new) } + + describe 'rules' do + context 'when user' do + it { expect(policy).to be_disallowed :read_cluster } + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + + context 'when admin' do + let(:user) { create(:admin) } + + context 'with instance_level_clusters enabled' do + it { expect(policy).to be_allowed :read_cluster } + it { expect(policy).to be_allowed :update_cluster } + it { expect(policy).to be_allowed :admin_cluster } + end + + context 'with instance_level_clusters disabled' do + before do + stub_feature_flags(instance_clusters: false) + end + + it { expect(policy).to be_disallowed :read_cluster } + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + end + end +end diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb index a9d786bc872..42701a5f8d1 100644 --- a/spec/presenters/clusters/cluster_presenter_spec.rb +++ b/spec/presenters/clusters/cluster_presenter_spec.rb @@ -210,6 +210,12 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq('Group cluster') } end + + context 'instance_type cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { is_expected.to eq('Instance cluster') } + end end describe '#show_path' do @@ -227,6 +233,12 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq(group_cluster_path(group, cluster)) } end + + context 'instance_type cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { is_expected.to eq(admin_cluster_path(cluster)) } + end end describe '#read_only_kubernetes_platform_fields?' do diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb index cb623fa1fa4..fa77273f6aa 100644 --- a/spec/presenters/group_clusterable_presenter_spec.rb +++ b/spec/presenters/group_clusterable_presenter_spec.rb @@ -82,10 +82,4 @@ describe GroupClusterablePresenter do it { is_expected.to eq(group_cluster_path(group, cluster)) } end - - describe '#clusters_path' do - subject { presenter.clusters_path } - - it { is_expected.to eq(group_clusters_path(group)) } - end end diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb index e5857f75aed..6786a84243f 100644 --- a/spec/presenters/project_clusterable_presenter_spec.rb +++ b/spec/presenters/project_clusterable_presenter_spec.rb @@ -82,10 +82,4 @@ describe ProjectClusterablePresenter do it { is_expected.to eq(project_cluster_path(project, cluster)) } end - - describe '#clusters_path' do - subject { presenter.clusters_path } - - it { is_expected.to eq(project_clusters_path(project)) } - end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 8a80652b3d8..9a3ac75e418 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -773,7 +773,7 @@ describe Ci::CreatePipelineService do end end - describe 'Merge request pipelines' do + describe 'Pipelines for merge requests' do let(:pipeline) do execute_service(source: source, merge_request: merge_request, @@ -817,12 +817,14 @@ describe Ci::CreatePipelineService do let(:merge_request) do create(:merge_request, source_project: project, - source_branch: Gitlab::Git.ref_name(ref_name), + source_branch: 'feature', target_project: project, target_branch: 'master') end - it 'creates a merge request pipeline' do + let(:ref_name) { merge_request.ref_path } + + it 'creates a detached merge request pipeline' do expect(pipeline).to be_persisted expect(pipeline).to be_merge_request_event expect(pipeline.merge_request).to eq(merge_request) @@ -837,6 +839,13 @@ describe Ci::CreatePipelineService do expect(pipeline.target_sha).to be_nil end + it 'schedules update for the head pipeline of the merge request' do + expect(UpdateHeadPipelineForMergeRequestWorker) + .to receive(:perform_async).with(merge_request.id) + + pipeline + end + context 'when target sha is specified' do let(:target_sha) { merge_request.target_branch_sha } @@ -858,15 +867,16 @@ describe Ci::CreatePipelineService do let(:merge_request) do create(:merge_request, source_project: project, - source_branch: Gitlab::Git.ref_name(ref_name), + source_branch: 'feature', target_project: target_project, target_branch: 'master') end + let(:ref_name) { 'refs/heads/feature' } let!(:project) { fork_project(target_project, nil, repository: true) } let!(:target_project) { create(:project, :repository) } - it 'creates a merge request pipeline in the forked project' do + it 'creates a legacy detached merge request pipeline in the forked project' do expect(pipeline).to be_persisted expect(project.ci_pipelines).to eq([pipeline]) expect(target_project.ci_pipelines).to be_empty @@ -884,7 +894,7 @@ describe Ci::CreatePipelineService do } end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."]) end @@ -894,7 +904,7 @@ describe Ci::CreatePipelineService do context 'when merge request is not specified' do let(:merge_request) { nil } - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:merge_request]).to eq(["can't be blank"]) end @@ -928,7 +938,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -939,7 +949,7 @@ describe Ci::CreatePipelineService do context 'when merge request is not specified' do let(:merge_request) { nil } - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -968,7 +978,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -999,7 +1009,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) @@ -1028,7 +1038,7 @@ describe Ci::CreatePipelineService do target_branch: 'master') end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(pipeline).not_to be_persisted expect(pipeline.errors[:base]) diff --git a/spec/services/clusters/build_service_spec.rb b/spec/services/clusters/build_service_spec.rb index da0cb42b3a1..f3e852726f4 100644 --- a/spec/services/clusters/build_service_spec.rb +++ b/spec/services/clusters/build_service_spec.rb @@ -21,5 +21,13 @@ describe Clusters::BuildService do is_expected.to be_group_type end end + + describe 'when cluster subject is an instance' do + let(:cluster_subject) { Clusters::Instance.new } + + it 'sets the cluster_type to instance_type' do + is_expected.to be_instance_type + end + end end end diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb index f4f0415985c..87672c8896d 100644 --- a/spec/support/helpers/select2_helper.rb +++ b/spec/support/helpers/select2_helper.rb @@ -35,6 +35,10 @@ module Select2Helper execute_script("$('#{selector}').select2('open');") end + def close_select2(selector) + execute_script("$('#{selector}').select2('close');") + end + def scroll_select2_to_bottom(selector) evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');" end |