diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-18 21:08:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-18 21:08:47 +0300 |
commit | 128d4d89e98177996d1ff6e0b3d7a8a0c9b35929 (patch) | |
tree | 88b02d3bf972bac281d673e99f854303e0dd13ed /spec | |
parent | cc1066db64a2a283a3d229b9bbb67c01716ca871 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
44 files changed, 1490 insertions, 541 deletions
diff --git a/spec/controllers/projects/pipelines/tests_controller_spec.rb b/spec/controllers/projects/pipelines/tests_controller_spec.rb index deb96cc5bf9..ddcab8b048e 100644 --- a/spec/controllers/projects/pipelines/tests_controller_spec.rb +++ b/spec/controllers/projects/pipelines/tests_controller_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Projects::Pipelines::TestsController do get_tests_show_json(build_ids) expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['errors']).to eq('Test report artifacts have expired') + expect(json_response['errors']).to eq('Test report artifacts not found') end end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index fa8db1befb5..9a1e216c6d2 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -97,6 +97,31 @@ RSpec.describe 'Group show page' do end end + context 'when a public project is shared with a private group' do + let_it_be(:private_group) { create(:group, :private) } + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:project_group_link) { create(:project_group_link, group: private_group, project: public_project) } + + before do + private_group.add_owner(user) + sign_in(user) + end + + it 'shows warning popover', :js do + visit group_path(private_group) + + click_link _('Shared projects') + + wait_for_requests + + page.within("[data-testid=\"group-overview-item-#{public_project.id}\"]") do + click_button _('Less restrictive visibility') + end + + expect(page).to have_content _('Project visibility level is less restrictive than the group settings.') + end + end + context 'when user does not have permissions to create new subgroups or projects', :js do before do group.add_reporter(user) diff --git a/spec/frontend/ci_lint/mock_data.js b/spec/frontend/ci_lint/mock_data.js index 28ea0f55bf8..660b2ad6e8b 100644 --- a/spec/frontend/ci_lint/mock_data.js +++ b/spec/frontend/ci_lint/mock_data.js @@ -1,5 +1,16 @@ import { mockJobs } from 'jest/pipeline_editor/mock_data'; +export const mockLintDataError = { + data: { + lintCI: { + errors: ['Error message'], + warnings: ['Warning message'], + valid: false, + jobs: mockJobs, + }, + }, +}; + export const mockLintDataValid = { data: { lintCI: { diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js deleted file mode 100644 index 0cafe6d3b9d..00000000000 --- a/spec/frontend/google_cloud/components/app_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { mapValues } from 'lodash'; -import App from '~/google_cloud/components/app.vue'; -import Home from '~/google_cloud/components/home.vue'; -import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; -import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue'; -import GcpError from '~/google_cloud/components/errors/gcp_error.vue'; -import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue'; - -const BASE_FEEDBACK_URL = - 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new'; -const SCREEN_COMPONENTS = { - Home, - ServiceAccountsForm, - GcpError, - NoGcpProjects, -}; -const SERVICE_ACCOUNTS_FORM_PROPS = { - gcpProjects: [1, 2, 3], - refs: [4, 5, 6], - cancelPath: '', -}; -const HOME_PROPS = { - serviceAccounts: [{}, {}], - gcpRegions: [{}, {}], - createServiceAccountUrl: '#url-create-service-account', - configureGcpRegionsUrl: '#url-configure-gcp-regions', - emptyIllustrationUrl: '#url-empty-illustration', - enableCloudRunUrl: '#url-enable-cloud-run', - enableCloudStorageUrl: '#enableCloudStorageUrl', - revokeOauthUrl: '#revokeOauthUrl', -}; - -describe('google_cloud App component', () => { - let wrapper; - - const findIncubationBanner = () => wrapper.findComponent(IncubationBanner); - - afterEach(() => { - wrapper.destroy(); - }); - - describe.each` - screen | extraProps | componentName - ${'gcp_error'} | ${{ error: 'mock_gcp_client_error' }} | ${'GcpError'} - ${'no_gcp_projects'} | ${{}} | ${'NoGcpProjects'} - ${'service_accounts_form'} | ${SERVICE_ACCOUNTS_FORM_PROPS} | ${'ServiceAccountsForm'} - ${'home'} | ${HOME_PROPS} | ${'Home'} - `('for screen=$screen', ({ screen, extraProps, componentName }) => { - const component = SCREEN_COMPONENTS[componentName]; - - beforeEach(() => { - wrapper = shallowMount(App, { propsData: { screen, ...extraProps } }); - }); - - it(`renders only ${componentName}`, () => { - const existences = mapValues(SCREEN_COMPONENTS, (x) => wrapper.findComponent(x).exists()); - - expect(existences).toEqual({ - ...mapValues(SCREEN_COMPONENTS, () => false), - [componentName]: true, - }); - }); - - it(`renders the ${componentName} with props`, () => { - expect(wrapper.findComponent(component).props()).toEqual(extraProps); - }); - - it('renders incubation banner', () => { - expect(findIncubationBanner().props()).toEqual({ - shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`, - reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`, - featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`, - }); - }); - }); -}); diff --git a/spec/frontend/google_cloud/components/errors/gcp_error_spec.js b/spec/frontend/google_cloud/components/errors/gcp_error_spec.js deleted file mode 100644 index 4062a8b902a..00000000000 --- a/spec/frontend/google_cloud/components/errors/gcp_error_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlAlert } from '@gitlab/ui'; -import GcpError from '~/google_cloud/components/errors/gcp_error.vue'; - -describe('GcpError component', () => { - let wrapper; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findBlockquote = () => wrapper.find('blockquote'); - - const propsData = { error: 'IAM and CloudResourceManager API disabled' }; - - beforeEach(() => { - wrapper = shallowMount(GcpError, { propsData }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('contains alert', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('contains relevant text', () => { - const alertText = findAlert().text(); - expect(findAlert().props('title')).toBe(GcpError.i18n.title); - expect(alertText).toContain(GcpError.i18n.description); - }); - - it('contains error stacktrace', () => { - expect(findBlockquote().text()).toBe(propsData.error); - }); -}); diff --git a/spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js b/spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js deleted file mode 100644 index e1e20377880..00000000000 --- a/spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlAlert, GlButton } from '@gitlab/ui'; -import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue'; - -describe('NoGcpProjects component', () => { - let wrapper; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findButton = () => wrapper.findComponent(GlButton); - - beforeEach(() => { - wrapper = mount(NoGcpProjects); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('contains alert', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('contains relevant text', () => { - expect(findAlert().props('title')).toBe(NoGcpProjects.i18n.title); - expect(findAlert().text()).toContain(NoGcpProjects.i18n.description); - }); - - it('contains create gcp project button', () => { - const button = findButton(); - expect(button.text()).toBe(NoGcpProjects.i18n.createLabel); - expect(button.attributes('href')).toBe('https://console.cloud.google.com/projectcreate'); - }); -}); diff --git a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js new file mode 100644 index 00000000000..4809ea37045 --- /dev/null +++ b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js @@ -0,0 +1,40 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue'; + +describe('google_cloud/components/google_cloud_menu', () => { + let wrapper; + + const props = { + active: 'configuration', + configurationUrl: 'configuration-url', + deploymentsUrl: 'deployments-url', + databasesUrl: 'databases-url', + }; + + beforeEach(() => { + wrapper = mountExtended(GoogleCloudMenu, { propsData: props }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains active configuration link', () => { + const link = wrapper.findByTestId('configurationLink'); + expect(link.text()).toBe(GoogleCloudMenu.i18n.configuration.title); + expect(link.attributes('href')).toBe(props.configurationUrl); + expect(link.element.classList.contains('gl-tab-nav-item-active')).toBe(true); + }); + + it('contains deployments link', () => { + const link = wrapper.findByTestId('deploymentsLink'); + expect(link.text()).toBe(GoogleCloudMenu.i18n.deployments.title); + expect(link.attributes('href')).toBe(props.deploymentsUrl); + }); + + it('contains databases link', () => { + const link = wrapper.findByTestId('databasesLink'); + expect(link.text()).toBe(GoogleCloudMenu.i18n.databases.title); + expect(link.attributes('href')).toBe(props.databasesUrl); + }); +}); diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js deleted file mode 100644 index 42e3d72577d..00000000000 --- a/spec/frontend/google_cloud/components/home_spec.js +++ /dev/null @@ -1,66 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlTab, GlTabs } from '@gitlab/ui'; -import Home from '~/google_cloud/components/home.vue'; -import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue'; - -describe('google_cloud Home component', () => { - let wrapper; - - const findTabs = () => wrapper.findComponent(GlTabs); - const findTabItems = () => findTabs().findAllComponents(GlTab); - const findTabItemsModel = () => - findTabs() - .findAllComponents(GlTab) - .wrappers.map((x) => ({ - title: x.attributes('title'), - disabled: x.attributes('disabled'), - })); - - const TEST_HOME_PROPS = { - serviceAccounts: [{}, {}], - gcpRegions: [{}, {}], - createServiceAccountUrl: '#url-create-service-account', - configureGcpRegionsUrl: '#url-configure-gcp-regions', - emptyIllustrationUrl: '#url-empty-illustration', - enableCloudRunUrl: '#url-enable-cloud-run', - enableCloudStorageUrl: '#enableCloudStorageUrl', - revokeOauthUrl: '#revokeOauthUrl', - }; - - beforeEach(() => { - const propsData = { - screen: 'home', - ...TEST_HOME_PROPS, - }; - wrapper = shallowMount(Home, { propsData }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('google_cloud App tabs', () => { - it('should contain tabs', () => { - expect(findTabs().exists()).toBe(true); - }); - - it('should contain three tab items', () => { - expect(findTabItemsModel()).toEqual([ - { title: 'Configuration', disabled: undefined }, - { title: 'Deployments', disabled: undefined }, - { title: 'Services', disabled: '' }, - ]); - }); - - describe('configuration tab', () => { - it('should contain service accounts component', () => { - const serviceAccounts = findTabItems().at(0).findComponent(ServiceAccountsList); - expect(serviceAccounts.props()).toEqual({ - list: TEST_HOME_PROPS.serviceAccounts, - createUrl: TEST_HOME_PROPS.createServiceAccountUrl, - emptyIllustrationUrl: TEST_HOME_PROPS.emptyIllustrationUrl, - }); - }); - }); - }); -}); diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js index 89517be4ef1..09a4d92dca2 100644 --- a/spec/frontend/google_cloud/components/incubation_banner_spec.js +++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; import { GlAlert, GlLink } from '@gitlab/ui'; import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; -describe('IncubationBanner component', () => { +describe('google_cloud/components/incubation_banner', () => { let wrapper; const findAlert = () => wrapper.findComponent(GlAlert); @@ -12,12 +12,7 @@ describe('IncubationBanner component', () => { const findShareFeedbackLink = () => findLinks().at(2); beforeEach(() => { - const propsData = { - shareFeedbackUrl: 'url_general_feedback', - reportBugUrl: 'url_report_bug', - featureRequestUrl: 'url_feature_request', - }; - wrapper = mount(IncubationBanner, { propsData }); + wrapper = mount(IncubationBanner); }); afterEach(() => { @@ -41,20 +36,26 @@ describe('IncubationBanner component', () => { it('contains feature request link', () => { const link = findFeatureRequestLink(); + const expected = + 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=feature_request'; expect(link.text()).toBe('request a feature'); - expect(link.attributes('href')).toBe('url_feature_request'); + expect(link.attributes('href')).toBe(expected); }); it('contains report bug link', () => { const link = findReportBugLink(); + const expected = + 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=report_bug'; expect(link.text()).toBe('report a bug'); - expect(link.attributes('href')).toBe('url_report_bug'); + expect(link.attributes('href')).toBe(expected); }); it('contains share feedback link', () => { const link = findShareFeedbackLink(); + const expected = + 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=general_feedback'; expect(link.text()).toBe('share feedback'); - expect(link.attributes('href')).toBe('url_general_feedback'); + expect(link.attributes('href')).toBe(expected); }); }); }); diff --git a/spec/frontend/google_cloud/components/revoke_oauth_spec.js b/spec/frontend/google_cloud/components/revoke_oauth_spec.js index 87580dbf6de..faaec07fc35 100644 --- a/spec/frontend/google_cloud/components/revoke_oauth_spec.js +++ b/spec/frontend/google_cloud/components/revoke_oauth_spec.js @@ -5,7 +5,7 @@ import RevokeOauth, { GOOGLE_CLOUD_REVOKE_DESCRIPTION, } from '~/google_cloud/components/revoke_oauth.vue'; -describe('RevokeOauth component', () => { +describe('google_cloud/components/revoke_oauth', () => { let wrapper; const findTitle = () => wrapper.find('h2'); diff --git a/spec/frontend/google_cloud/configuration/panel_spec.js b/spec/frontend/google_cloud/configuration/panel_spec.js new file mode 100644 index 00000000000..79eb4cb4918 --- /dev/null +++ b/spec/frontend/google_cloud/configuration/panel_spec.js @@ -0,0 +1,65 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import Panel from '~/google_cloud/configuration/panel.vue'; +import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; +import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue'; +import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue'; +import GcpRegionsList from '~/google_cloud/gcp_regions/list.vue'; +import RevokeOauth from '~/google_cloud/components/revoke_oauth.vue'; + +describe('google_cloud/configuration/panel', () => { + let wrapper; + + const props = { + configurationUrl: 'configuration-url', + deploymentsUrl: 'deployments-url', + databasesUrl: 'databases-url', + serviceAccounts: [], + createServiceAccountUrl: 'create-service-account-url', + emptyIllustrationUrl: 'empty-illustration-url', + gcpRegions: [], + configureGcpRegionsUrl: 'configure-gcp-regions-url', + revokeOauthUrl: 'revoke-oauth-url', + }; + + beforeEach(() => { + wrapper = shallowMountExtended(Panel, { propsData: props }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains incubation banner', () => { + const target = wrapper.findComponent(IncubationBanner); + expect(target.exists()).toBe(true); + }); + + it('contains google cloud menu with `configuration` active', () => { + const target = wrapper.findComponent(GoogleCloudMenu); + expect(target.exists()).toBe(true); + expect(target.props('active')).toBe('configuration'); + expect(target.props('configurationUrl')).toBe(props.configurationUrl); + expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl); + expect(target.props('databasesUrl')).toBe(props.databasesUrl); + }); + + it('contains service accounts list', () => { + const target = wrapper.findComponent(ServiceAccountsList); + expect(target.exists()).toBe(true); + expect(target.props('list')).toBe(props.serviceAccounts); + expect(target.props('createUrl')).toBe(props.createServiceAccountUrl); + expect(target.props('emptyIllustrationUrl')).toBe(props.emptyIllustrationUrl); + }); + + it('contains gcp regions list', () => { + const target = wrapper.findComponent(GcpRegionsList); + expect(target.props('list')).toBe(props.gcpRegions); + expect(target.props('createUrl')).toBe(props.configureGcpRegionsUrl); + expect(target.props('emptyIllustrationUrl')).toBe(props.emptyIllustrationUrl); + }); + + it('contains revoke oauth', () => { + const target = wrapper.findComponent(RevokeOauth); + expect(target.props('url')).toBe(props.revokeOauthUrl); + }); +}); diff --git a/spec/frontend/google_cloud/components/cloudsql/create_instance_form_spec.js b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js index de644a33b50..48e4b0ca1ad 100644 --- a/spec/frontend/google_cloud/components/cloudsql/create_instance_form_spec.js +++ b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js @@ -1,8 +1,8 @@ import { GlFormCheckbox } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import InstanceForm from '~/google_cloud/components/cloudsql/create_instance_form.vue'; +import InstanceForm from '~/google_cloud/databases/cloudsql/create_instance_form.vue'; -describe('google_cloud::cloudsql::create_instance_form component', () => { +describe('google_cloud/databases/cloudsql/create_instance_form', () => { let wrapper; const findByTestId = (id) => wrapper.findByTestId(id); diff --git a/spec/frontend/google_cloud/components/cloudsql/instance_table_spec.js b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js index 286f2b8e379..a5736d0a524 100644 --- a/spec/frontend/google_cloud/components/cloudsql/instance_table_spec.js +++ b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js @@ -1,8 +1,8 @@ import { shallowMount } from '@vue/test-utils'; import { GlEmptyState, GlTable } from '@gitlab/ui'; -import InstanceTable from '~/google_cloud/components/cloudsql/instance_table.vue'; +import InstanceTable from '~/google_cloud/databases/cloudsql/instance_table.vue'; -describe('google_cloud::databases::service_table component', () => { +describe('google_cloud/databases/cloudsql/instance_table', () => { let wrapper; const findEmptyState = () => wrapper.findComponent(GlEmptyState); diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js new file mode 100644 index 00000000000..490c0136651 --- /dev/null +++ b/spec/frontend/google_cloud/databases/panel_spec.js @@ -0,0 +1,36 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import Panel from '~/google_cloud/databases/panel.vue'; +import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; +import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue'; + +describe('google_cloud/databases/panel', () => { + let wrapper; + + const props = { + configurationUrl: 'configuration-url', + deploymentsUrl: 'deployments-url', + databasesUrl: 'databases-url', + }; + + beforeEach(() => { + wrapper = shallowMountExtended(Panel, { propsData: props }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains incubation banner', () => { + const target = wrapper.findComponent(IncubationBanner); + expect(target.exists()).toBe(true); + }); + + it('contains google cloud menu with `databases` active', () => { + const target = wrapper.findComponent(GoogleCloudMenu); + expect(target.exists()).toBe(true); + expect(target.props('active')).toBe('databases'); + expect(target.props('configurationUrl')).toBe(props.configurationUrl); + expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl); + expect(target.props('databasesUrl')).toBe(props.databasesUrl); + }); +}); diff --git a/spec/frontend/google_cloud/components/databases/service_table_spec.js b/spec/frontend/google_cloud/databases/service_table_spec.js index 142e32c1a4b..4a622e544e1 100644 --- a/spec/frontend/google_cloud/components/databases/service_table_spec.js +++ b/spec/frontend/google_cloud/databases/service_table_spec.js @@ -1,8 +1,8 @@ import { GlTable } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import ServiceTable from '~/google_cloud/components/databases/service_table.vue'; +import ServiceTable from '~/google_cloud/databases/service_table.vue'; -describe('google_cloud::databases::service_table component', () => { +describe('google_cloud/databases/service_table', () => { let wrapper; const findTable = () => wrapper.findComponent(GlTable); diff --git a/spec/frontend/google_cloud/deployments/panel_spec.js b/spec/frontend/google_cloud/deployments/panel_spec.js new file mode 100644 index 00000000000..729db1707a7 --- /dev/null +++ b/spec/frontend/google_cloud/deployments/panel_spec.js @@ -0,0 +1,46 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import Panel from '~/google_cloud/deployments/panel.vue'; +import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; +import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue'; +import ServiceTable from '~/google_cloud/deployments/service_table.vue'; + +describe('google_cloud/deployments/panel', () => { + let wrapper; + + const props = { + configurationUrl: 'configuration-url', + deploymentsUrl: 'deployments-url', + databasesUrl: 'databases-url', + enableCloudRunUrl: 'cloud-run-url', + enableCloudStorageUrl: 'cloud-storage-url', + }; + + beforeEach(() => { + wrapper = shallowMountExtended(Panel, { propsData: props }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains incubation banner', () => { + const target = wrapper.findComponent(IncubationBanner); + expect(target.exists()).toBe(true); + }); + + it('contains google cloud menu with `deployments` active', () => { + const target = wrapper.findComponent(GoogleCloudMenu); + expect(target.exists()).toBe(true); + expect(target.props('active')).toBe('deployments'); + expect(target.props('configurationUrl')).toBe(props.configurationUrl); + expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl); + expect(target.props('databasesUrl')).toBe(props.databasesUrl); + }); + + it('contains service-table', () => { + const target = wrapper.findComponent(ServiceTable); + expect(target.exists()).toBe(true); + expect(target.props('cloudRunUrl')).toBe(props.enableCloudRunUrl); + expect(target.props('cloudStorageUrl')).toBe(props.enableCloudStorageUrl); + }); +}); diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/deployments/service_table_spec.js index 882376547c4..8faad64e313 100644 --- a/spec/frontend/google_cloud/components/deployments_service_table_spec.js +++ b/spec/frontend/google_cloud/deployments/service_table_spec.js @@ -1,8 +1,8 @@ import { mount } from '@vue/test-utils'; import { GlButton, GlTable } from '@gitlab/ui'; -import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue'; +import DeploymentsServiceTable from '~/google_cloud/deployments/service_table.vue'; -describe('google_cloud DeploymentsServiceTable component', () => { +describe('google_cloud/deployments/service_table', () => { let wrapper; const findTable = () => wrapper.findComponent(GlTable); diff --git a/spec/frontend/google_cloud/components/gcp_regions_form_spec.js b/spec/frontend/google_cloud/gcp_regions/form_spec.js index a8b7593e7c8..1030e9c8a18 100644 --- a/spec/frontend/google_cloud/components/gcp_regions_form_spec.js +++ b/spec/frontend/google_cloud/gcp_regions/form_spec.js @@ -1,8 +1,8 @@ import { shallowMount } from '@vue/test-utils'; import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui'; -import GcpRegionsForm from '~/google_cloud/components/gcp_regions_form.vue'; +import GcpRegionsForm from '~/google_cloud/gcp_regions/form.vue'; -describe('GcpRegionsForm component', () => { +describe('google_cloud/gcp_regions/form', () => { let wrapper; const findHeader = () => wrapper.find('header'); diff --git a/spec/frontend/google_cloud/components/gcp_regions_list_spec.js b/spec/frontend/google_cloud/gcp_regions/list_spec.js index ab0c17451e8..6d8c389e5a1 100644 --- a/spec/frontend/google_cloud/components/gcp_regions_list_spec.js +++ b/spec/frontend/google_cloud/gcp_regions/list_spec.js @@ -1,8 +1,8 @@ import { mount } from '@vue/test-utils'; import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; -import GcpRegionsList from '~/google_cloud/components/gcp_regions_list.vue'; +import GcpRegionsList from '~/google_cloud/gcp_regions/list.vue'; -describe('GcpRegions component', () => { +describe('google_cloud/gcp_regions/list', () => { describe('when the project does not have any configured regions', () => { let wrapper; diff --git a/spec/frontend/google_cloud/components/service_accounts_form_spec.js b/spec/frontend/google_cloud/service_accounts/form_spec.js index 38602d4e8cc..8be481774fa 100644 --- a/spec/frontend/google_cloud/components/service_accounts_form_spec.js +++ b/spec/frontend/google_cloud/service_accounts/form_spec.js @@ -1,8 +1,8 @@ import { shallowMount } from '@vue/test-utils'; import { GlButton, GlFormGroup, GlFormSelect, GlFormCheckbox } from '@gitlab/ui'; -import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue'; +import ServiceAccountsForm from '~/google_cloud/service_accounts/form.vue'; -describe('ServiceAccountsForm component', () => { +describe('google_cloud/service_accounts/form', () => { let wrapper; const findHeader = () => wrapper.find('header'); diff --git a/spec/frontend/google_cloud/components/service_accounts_list_spec.js b/spec/frontend/google_cloud/service_accounts/list_spec.js index f7051c8a53d..7a76a893757 100644 --- a/spec/frontend/google_cloud/components/service_accounts_list_spec.js +++ b/spec/frontend/google_cloud/service_accounts/list_spec.js @@ -1,8 +1,8 @@ import { mount } from '@vue/test-utils'; import { GlAlert, GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; -import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue'; +import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue'; -describe('ServiceAccounts component', () => { +describe('google_cloud/service_accounts/list', () => { describe('when the project does not have any service accounts', () => { let wrapper; diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 8ea7e54aef4..0bc80df6535 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -1,4 +1,4 @@ -import { mount } from '@vue/test-utils'; +import { GlPopover } from '@gitlab/ui'; import waitForPromises from 'helpers/wait_for_promises'; import GroupFolder from '~/groups/components/group_folder.vue'; import GroupItem from '~/groups/components/group_item.vue'; @@ -6,14 +6,25 @@ import ItemActions from '~/groups/components/item_actions.vue'; import eventHub from '~/groups/event_hub'; import { getGroupItemMicrodata } from '~/groups/store/utils'; import * as urlUtilities from '~/lib/utils/url_utility'; +import { + ITEM_TYPE, + VISIBILITY_INTERNAL, + VISIBILITY_PRIVATE, + VISIBILITY_PUBLIC, +} from '~/groups/constants'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mockParentGroupItem, mockChildren } from '../mock_data'; const createComponent = ( propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] }, + provide = { + currentGroupVisibility: VISIBILITY_PRIVATE, + }, ) => { - return mount(GroupItem, { + return mountExtended(GroupItem, { propsData, components: { GroupFolder }, + provide, }); }; @@ -276,4 +287,90 @@ describe('GroupItemComponent', () => { }); }); }); + + describe('visibility warning popover', () => { + const findPopover = () => wrapper.findComponent(GlPopover); + + const itDoesNotRenderVisibilityWarningPopover = () => { + it('does not render visibility warning popover', () => { + expect(findPopover().exists()).toBe(false); + }); + }; + + describe('when showing groups', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + itDoesNotRenderVisibilityWarningPopover(); + }); + + describe('when `action` prop is not `shared`', () => { + beforeEach(() => { + wrapper = createComponent({ + group: mockParentGroupItem, + parentGroup: mockChildren[0], + action: 'subgroups_and_projects', + }); + }); + + itDoesNotRenderVisibilityWarningPopover(); + }); + + describe('when showing projects', () => { + describe.each` + itemVisibility | currentGroupVisibility | isPopoverShown + ${VISIBILITY_PRIVATE} | ${VISIBILITY_PUBLIC} | ${false} + ${VISIBILITY_INTERNAL} | ${VISIBILITY_PUBLIC} | ${false} + ${VISIBILITY_PUBLIC} | ${VISIBILITY_PUBLIC} | ${false} + ${VISIBILITY_PRIVATE} | ${VISIBILITY_PRIVATE} | ${false} + ${VISIBILITY_INTERNAL} | ${VISIBILITY_PRIVATE} | ${true} + ${VISIBILITY_PUBLIC} | ${VISIBILITY_PRIVATE} | ${true} + `( + 'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility', + ({ itemVisibility, currentGroupVisibility, isPopoverShown }) => { + beforeEach(() => { + wrapper = createComponent( + { + group: { + ...mockParentGroupItem, + visibility: itemVisibility, + type: ITEM_TYPE.PROJECT, + }, + parentGroup: mockChildren[0], + action: 'shared', + }, + { + currentGroupVisibility, + }, + ); + }); + + if (isPopoverShown) { + it('renders visibility warning popover', () => { + expect(findPopover().exists()).toBe(true); + }); + } else { + itDoesNotRenderVisibilityWarningPopover(); + } + }, + ); + }); + + it('sets up popover `target` prop correctly', () => { + wrapper = createComponent({ + group: { + ...mockParentGroupItem, + visibility: VISIBILITY_PUBLIC, + type: ITEM_TYPE.PROJECT, + }, + parentGroup: mockChildren[0], + action: 'shared', + }); + + expect(findPopover().props('target')()).toEqual( + wrapper.findByRole('button', { name: GroupItem.i18n.popoverTitle }).element, + ); + }); + }); }); diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js index 590b4fb3d57..48a2319cf96 100644 --- a/spec/frontend/groups/components/groups_spec.js +++ b/spec/frontend/groups/components/groups_spec.js @@ -1,45 +1,55 @@ -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import groupFolderComponent from '~/groups/components/group_folder.vue'; -import groupItemComponent from '~/groups/components/group_item.vue'; -import groupsComponent from '~/groups/components/groups.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import GroupFolderComponent from '~/groups/components/group_folder.vue'; +import GroupItemComponent from '~/groups/components/group_item.vue'; +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; +import GroupsComponent from '~/groups/components/groups.vue'; import eventHub from '~/groups/event_hub'; +import { VISIBILITY_PRIVATE } from '~/groups/constants'; import { mockGroups, mockPageInfo } from '../mock_data'; -const createComponent = (searchEmpty = false) => { - const Component = Vue.extend(groupsComponent); +describe('GroupsComponent', () => { + let wrapper; - return mountComponent(Component, { + const defaultPropsData = { groups: mockGroups, pageInfo: mockPageInfo, searchEmptyMessage: 'No matching results', - searchEmpty, - }); -}; + searchEmpty: false, + }; -describe('GroupsComponent', () => { - let vm; - - beforeEach(async () => { - Vue.component('GroupFolder', groupFolderComponent); - Vue.component('GroupItem', groupItemComponent); + const createComponent = ({ propsData } = {}) => { + wrapper = mountExtended(GroupsComponent, { + propsData: { + ...defaultPropsData, + ...propsData, + }, + provide: { + currentGroupVisibility: VISIBILITY_PRIVATE, + }, + }); + }; - vm = createComponent(); + const findPaginationLinks = () => wrapper.findComponent(PaginationLinks); - await nextTick(); + beforeEach(async () => { + Vue.component('GroupFolder', GroupFolderComponent); + Vue.component('GroupItem', GroupItemComponent); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('methods', () => { describe('change', () => { it('should emit `fetchPage` event when page is changed via pagination', () => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(); - vm.change(2); + findPaginationLinks().props('change')(2); expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', { page: 2, @@ -52,18 +62,18 @@ describe('GroupsComponent', () => { }); describe('template', () => { - it('should render component template correctly', async () => { - await nextTick(); - expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); - expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); - expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); - expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0); + it('should render component template correctly', () => { + createComponent(); + + expect(wrapper.findComponent(GroupFolderComponent).exists()).toBe(true); + expect(findPaginationLinks().exists()).toBe(true); + expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(false); }); - it('should render empty search message when `searchEmpty` is `true`', async () => { - vm.searchEmpty = true; - await nextTick(); - expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined(); + it('should render empty search message when `searchEmpty` is `true`', () => { + createComponent({ propsData: { searchEmpty: true } }); + + expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/groups/mock_data.js b/spec/frontend/groups/mock_data.js index 65a62876893..9a325776374 100644 --- a/spec/frontend/groups/mock_data.js +++ b/spec/frontend/groups/mock_data.js @@ -29,6 +29,7 @@ export const mockParentGroupItem = { isChildrenLoading: false, isBeingRemoved: false, updatedAt: '2017-04-09T18:40:39.101Z', + lastActivityAt: '2017-04-09T18:40:39.101Z', }; export const mockRawChildren = [ diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js index ae19ed9ab02..82ac390971d 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js @@ -152,4 +152,26 @@ describe('CI Lint Results', () => { expect(findAfterScripts()).toHaveLength(filterEmptyScripts('afterScript').length); }); }); + + describe('Hide Alert', () => { + it('hides alert on success if hide-alert prop is true', async () => { + await createComponent({ dryRun: true, hideAlert: true }, mount); + + expect(findStatus().exists()).toBe(false); + }); + + it('hides alert on error if hide-alert prop is true', async () => { + await createComponent( + { + hideAlert: true, + isValid: false, + errors: mockErrors, + warnings: mockWarnings, + }, + mount, + ); + + expect(findStatus().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index af5029b7c3b..87a7f07f7d4 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -1,6 +1,8 @@ import { GlAlert, GlBadge, GlLoadingIcon, GlTabs } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; import Vue, { nextTick } from 'vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; @@ -19,7 +21,17 @@ import { VALIDATE_TAB_BADGE_DISMISSED_KEY, } from '~/pipeline_editor/constants'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; -import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data'; +import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql'; +import { + mockBlobContentQueryResponse, + mockCiLintPath, + mockCiYml, + mockLintResponse, + mockLintResponseWithoutMerged, +} from '../mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); Vue.config.ignoredElements = ['gl-emoji']; @@ -35,6 +47,7 @@ describe('Pipeline editor tabs component', () => { provide = {}, appStatus = EDITOR_APP_STATUS_VALID, mountFn = shallowMount, + options = {}, } = {}) => { wrapper = mountFn(PipelineEditorTabs, { propsData: { @@ -50,12 +63,34 @@ describe('Pipeline editor tabs component', () => { appStatus, }; }, - provide: { ...provide }, + provide: { + ciLintPath: mockCiLintPath, + ...provide, + }, stubs: { TextEditor: MockTextEditor, EditorTab, }, listeners, + ...options, + }); + }; + + let mockBlobContentData; + let mockApollo; + + const createComponentWithApollo = ({ props, provide = {}, mountFn = shallowMount } = {}) => { + const handlers = [[getBlobContent, mockBlobContentData]]; + mockApollo = createMockApollo(handlers); + + createComponent({ + props, + provide, + mountFn, + options: { + localVue, + apolloProvider: mockApollo, + }, }); }; @@ -76,6 +111,10 @@ describe('Pipeline editor tabs component', () => { const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview); const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover); + beforeEach(() => { + mockBlobContentData = jest.fn(); + }); + afterEach(() => { wrapper.destroy(); }); @@ -118,27 +157,6 @@ describe('Pipeline editor tabs component', () => { describe('validate tab', () => { describe('with simulatePipeline feature flag ON', () => { - describe('while loading', () => { - beforeEach(() => { - createComponent({ - appStatus: EDITOR_APP_STATUS_LOADING, - provide: { - glFeatures: { - simulatePipeline: true, - }, - }, - }); - }); - - it('displays a loading icon if the lint query is loading', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('does not display the validate component', () => { - expect(findCiValidate().exists()).toBe(false); - }); - }); - describe('after loading', () => { beforeEach(() => { createComponent({ @@ -155,13 +173,17 @@ describe('Pipeline editor tabs component', () => { describe('NEW badge', () => { describe('default', () => { beforeEach(() => { - createComponent({ + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + createComponentWithApollo({ mountFn: mount, props: { currentTab: VALIDATE_TAB, }, provide: { glFeatures: { simulatePipeline: true }, + ciConfigPath: '/path/to/ci-config', + currentBranch: 'main', + projectFullPath: '/path/to/project', simulatePipelineHelpPagePath: 'path/to/help/page', validateTabIllustrationPath: 'path/to/svg', }, @@ -185,10 +207,14 @@ describe('Pipeline editor tabs component', () => { describe('if badge has been dismissed before', () => { beforeEach(() => { localStorage.setItem(VALIDATE_TAB_BADGE_DISMISSED_KEY, 'true'); - createComponent({ + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + createComponentWithApollo({ mountFn: mount, provide: { glFeatures: { simulatePipeline: true }, + ciConfigPath: '/path/to/ci-config', + currentBranch: 'main', + projectFullPath: '/path/to/project', simulatePipelineHelpPagePath: 'path/to/help/page', validateTabIllustrationPath: 'path/to/svg', }, diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js index e1fbcd34e7c..f5f01b675b2 100644 --- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js +++ b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js @@ -1,45 +1,132 @@ -import { GlButton, GlDropdown, GlIcon, GlPopover } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue'; import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_validate.vue'; import ValidatePipelinePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue'; -import { mockSimulatePipelineHelpPagePath } from '../../mock_data'; +import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql'; +import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql'; +import { + mockBlobContentQueryResponse, + mockCiLintPath, + mockCiYml, + mockSimulatePipelineHelpPagePath, +} from '../../mock_data'; +import { mockLintDataError, mockLintDataValid } from '../../../ci_lint/mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); describe('Pipeline Editor Validate Tab', () => { let wrapper; + let mockApollo; + let mockBlobContentData; - const createComponent = ({ stubs } = {}) => { - wrapper = shallowMount(CiValidate, { + const createComponent = ({ + props, + stubs, + options, + isBlobLoading = false, + isSimulationLoading = false, + } = {}) => { + wrapper = shallowMountExtended(CiValidate, { + propsData: { + ciFileContent: mockCiYml, + ...props, + }, provide: { + ciConfigPath: '/path/to/ci-config', + ciLintPath: mockCiLintPath, + currentBranch: 'main', + projectFullPath: '/path/to/project', validateTabIllustrationPath: '/path/to/img', simulatePipelineHelpPagePath: mockSimulatePipelineHelpPagePath, }, stubs, + mocks: { + $apollo: { + queries: { + initialBlobContent: { + loading: isBlobLoading, + }, + }, + mutations: { + lintCiMutation: { + loading: isSimulationLoading, + }, + }, + }, + }, + ...options, + }); + }; + + const createComponentWithApollo = ({ props, stubs } = {}) => { + const handlers = [[getBlobContent, mockBlobContentData]]; + mockApollo = createMockApollo(handlers); + + createComponent({ + props, + stubs, + options: { + localVue, + apolloProvider: mockApollo, + mocks: {}, + }, }); }; - const findCta = () => wrapper.findComponent(GlButton); + const findAlert = () => wrapper.findComponent(GlAlert); + const findCancelBtn = () => wrapper.findByTestId('cancel-simulation'); + const findContentChangeStatus = () => wrapper.findByTestId('content-status'); + const findCta = () => wrapper.findByTestId('simulate-pipeline-button'); + const findDisabledCtaTooltip = () => wrapper.findByTestId('cta-tooltip'); const findHelpIcon = () => wrapper.findComponent(GlIcon); + const findIllustration = () => wrapper.findByRole('img'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findPipelineSource = () => wrapper.findComponent(GlDropdown); const findPopover = () => wrapper.findComponent(GlPopover); + const findCiLintResults = () => wrapper.findComponent(CiLintResults); + const findResultsCta = () => wrapper.findByTestId('resimulate-pipeline-button'); + + beforeEach(() => { + mockBlobContentData = jest.fn(); + }); afterEach(() => { wrapper.destroy(); }); - describe('template', () => { + describe('while initial CI content is loading', () => { beforeEach(() => { - createComponent({ stubs: { GlPopover, ValidatePipelinePopover } }); + createComponent({ isBlobLoading: true }); + }); + + it('renders disabled CTA with tooltip', () => { + expect(findCta().props('disabled')).toBe(true); + expect(findDisabledCtaTooltip().exists()).toBe(true); + }); + }); + + describe('after initial CI content is loaded', () => { + beforeEach(async () => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + await createComponentWithApollo({ stubs: { GlPopover, ValidatePipelinePopover } }); }); it('renders disabled pipeline source dropdown', () => { expect(findPipelineSource().exists()).toBe(true); expect(findPipelineSource().attributes('text')).toBe(i18n.pipelineSourceDefault); - expect(findPipelineSource().attributes('disabled')).toBe('true'); + expect(findPipelineSource().props('disabled')).toBe(true); }); - it('renders CTA', () => { + it('renders enabled CTA without tooltip', () => { expect(findCta().exists()).toBe(true); - expect(findCta().text()).toBe(i18n.cta); + expect(findCta().props('disabled')).toBe(false); + expect(findDisabledCtaTooltip().exists()).toBe(false); }); it('popover is set to render when hovering over help icon', () => { @@ -47,4 +134,146 @@ describe('Pipeline Editor Validate Tab', () => { expect(findPopover().props('triggers')).toBe('hover focus'); }); }); + + describe('simulating the pipeline', () => { + beforeEach(async () => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + await createComponentWithApollo(); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); + }); + + it('renders loading state while simulation is ongoing', async () => { + findCta().vm.$emit('click'); + await nextTick(); + + expect(findLoadingIcon().exists()).toBe(true); + expect(findCancelBtn().exists()).toBe(true); + expect(findCta().props('loading')).toBe(true); + }); + + it('calls mutation with the correct input', async () => { + await findCta().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: lintCIMutation, + variables: { + dry_run: true, + content: mockCiYml, + endpoint: mockCiLintPath, + }, + }); + }); + + describe('when results are successful', () => { + beforeEach(async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); + await findCta().vm.$emit('click'); + }); + + it('renders success alert', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().attributes('variant')).toBe('success'); + expect(findAlert().attributes('title')).toBe(i18n.successAlertTitle); + }); + + it('does not render content change status or CTA for results page', () => { + expect(findContentChangeStatus().exists()).toBe(false); + expect(findResultsCta().exists()).toBe(false); + }); + + it('renders CI lint results with correct props', () => { + expect(findCiLintResults().exists()).toBe(true); + expect(findCiLintResults().props()).toMatchObject({ + dryRun: true, + hideAlert: true, + isValid: true, + jobs: mockLintDataValid.data.lintCI.jobs, + }); + }); + }); + + describe('when results have errors', () => { + beforeEach(async () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataError); + await findCta().vm.$emit('click'); + }); + + it('renders error alert', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().attributes('variant')).toBe('danger'); + expect(findAlert().attributes('title')).toBe(i18n.errorAlertTitle); + }); + + it('renders CI lint results with correct props', () => { + expect(findCiLintResults().exists()).toBe(true); + expect(findCiLintResults().props()).toMatchObject({ + dryRun: true, + hideAlert: true, + isValid: false, + errors: mockLintDataError.data.lintCI.errors, + warnings: mockLintDataError.data.lintCI.warnings, + }); + }); + }); + }); + + describe('when CI content has changed after a simulation', () => { + beforeEach(async () => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + await createComponentWithApollo(); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); + await findCta().vm.$emit('click'); + }); + + it('renders content change status', async () => { + await wrapper.setProps({ ciFileContent: 'new yaml content' }); + + expect(findContentChangeStatus().exists()).toBe(true); + expect(findResultsCta().exists()).toBe(true); + }); + + it('calls mutation with new content', async () => { + await wrapper.setProps({ ciFileContent: 'new yaml content' }); + await findResultsCta().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(2); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: lintCIMutation, + variables: { + dry_run: true, + content: 'new yaml content', + endpoint: mockCiLintPath, + }, + }); + }); + }); + + describe('canceling a simulation', () => { + beforeEach(async () => { + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + await createComponentWithApollo(); + }); + + it('returns to init state', async () => { + // init state + expect(findIllustration().exists()).toBe(true); + expect(findCiLintResults().exists()).toBe(false); + + // mutations should have successful results + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); + findCta().vm.$emit('click'); + await nextTick(); + + // cancel before simulation succeeds + expect(findCancelBtn().exists()).toBe(true); + await findCancelBtn().vm.$emit('click'); + + // should still render init state + expect(findIllustration().exists()).toBe(true); + expect(findCiLintResults().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index f50ab1ab2a6..2ea580b7b53 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -7,6 +7,7 @@ export const mockProjectFullPath = `${mockProjectNamespace}/${mockProjectPath}`; export const mockDefaultBranch = 'main'; export const mockNewBranch = 'new-branch'; export const mockNewMergeRequestPath = '/-/merge_requests/new'; +export const mockCiLintPath = '/-/ci/lint'; export const mockCommitSha = 'aabbccdd'; export const mockCommitNextSha = 'eeffgghh'; export const mockIncludesHelpPagePath = '/-/includes/help'; diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js index 6ab479a257c..f9b9da01a2b 100644 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js @@ -49,7 +49,7 @@ describe('Mutations TestReports Store', () => { describe('set suite error', () => { it('should set the error message in state if provided', () => { - const message = 'Test report artifacts have expired'; + const message = 'Test report artifacts not found'; mutations[types.SET_SUITE_ERROR](mockState, { response: { data: { errors: message } }, diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb index a895d3e4fd7..bc9e47a4ca1 100644 --- a/spec/helpers/ci/pipeline_editor_helper_spec.rb +++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb @@ -48,6 +48,7 @@ RSpec.describe Ci::PipelineEditorHelper do "ci-config-path": project.ci_config_path_or_default, "ci-examples-help-page-path" => help_page_path('ci/examples/index'), "ci-help-page-path" => help_page_path('ci/index'), + "ci-lint-path" => project_ci_lint_path(project), "default-branch" => project.default_branch_or_main, "empty-state-illustration-path" => 'illustrations/empty.svg', "initial-branch-name" => nil, @@ -78,6 +79,7 @@ RSpec.describe Ci::PipelineEditorHelper do "ci-config-path": project.ci_config_path_or_default, "ci-examples-help-page-path" => help_page_path('ci/examples/index'), "ci-help-page-path" => help_page_path('ci/index'), + "ci-lint-path" => project_ci_lint_path(project), "default-branch" => project.default_branch_or_main, "empty-state-illustration-path" => 'illustrations/empty.svg', "initial-branch-name" => nil, diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb new file mode 100644 index 00000000000..8b82078bcb9 --- /dev/null +++ b/spec/lib/gitlab/memory/watchdog_spec.rb @@ -0,0 +1,308 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do + context 'watchdog' do + let(:logger) { instance_double(::Logger) } + let(:handler) { instance_double(described_class::NullHandler) } + + let(:heap_frag_limit_gauge) { instance_double(::Prometheus::Client::Gauge) } + let(:heap_frag_violations_counter) { instance_double(::Prometheus::Client::Counter) } + let(:heap_frag_violations_handled_counter) { instance_double(::Prometheus::Client::Counter) } + + let(:sleep_time) { 0.1 } + let(:max_heap_fragmentation) { 0.2 } + + subject(:watchdog) do + described_class.new(handler: handler, logger: logger, sleep_time_seconds: sleep_time, + max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation) + end + + before do + allow(handler).to receive(:on_high_heap_fragmentation).and_return(true) + + allow(logger).to receive(:warn) + allow(logger).to receive(:info) + + allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(fragmentation) + end + + after do + watchdog.stop + end + + context 'when starting up' do + let(:fragmentation) { 0 } + let(:max_strikes) { 0 } + + it 'sets the heap fragmentation limit gauge' do + allow(Gitlab::Metrics).to receive(:gauge).and_return(heap_frag_limit_gauge) + + expect(heap_frag_limit_gauge).to receive(:set).with({}, max_heap_fragmentation) + end + + context 'when no settings are set in the environment' do + it 'initializes with defaults' do + watchdog = described_class.new(handler: handler, logger: logger) + + expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_HEAP_FRAG_THRESHOLD) + expect(watchdog.max_strikes).to eq(described_class::DEFAULT_MAX_STRIKES) + expect(watchdog.sleep_time_seconds).to eq(described_class::DEFAULT_SLEEP_TIME_SECONDS) + end + end + + context 'when settings are passed through the environment' do + before do + stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 1) + stub_env('GITLAB_MEMWD_MAX_STRIKES', 2) + stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 3) + end + + it 'initializes with these settings' do + watchdog = described_class.new(handler: handler, logger: logger) + + expect(watchdog.max_heap_fragmentation).to eq(1) + expect(watchdog.max_strikes).to eq(2) + expect(watchdog.sleep_time_seconds).to eq(3) + end + end + end + + context 'when process does not exceed heap fragmentation threshold' do + let(:fragmentation) { max_heap_fragmentation - 0.1 } + let(:max_strikes) { 0 } # To rule out that we were granting too many strikes. + + it 'does not signal the handler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 3 + end + end + + context 'when process exceeds heap fragmentation threshold permanently' do + let(:fragmentation) { max_heap_fragmentation + 0.1 } + + before do + allow(Gitlab::Metrics).to receive(:counter) + .with(:gitlab_memwd_heap_frag_violations_total, anything, anything) + .and_return(heap_frag_violations_counter) + allow(Gitlab::Metrics).to receive(:counter) + .with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything) + .and_return(heap_frag_violations_handled_counter) + allow(heap_frag_violations_counter).to receive(:increment) + allow(heap_frag_violations_handled_counter).to receive(:increment) + end + + context 'when process has not exceeded allowed number of strikes' do + let(:max_strikes) { 10 } + + it 'does not signal the handler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'does not log any events' do + expect(logger).not_to receive(:warn) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'increments the violations counter' do + expect(heap_frag_violations_counter).to receive(:increment) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'does not increment violations handled counter' do + expect(heap_frag_violations_handled_counter).not_to receive(:increment) + + watchdog.start + + sleep sleep_time * 3 + end + end + + context 'when process exceeds the allowed number of strikes' do + let(:max_strikes) { 1 } + + it 'signals the handler and resets strike counter' do + expect(handler).to receive(:on_high_heap_fragmentation).and_return(true) + + watchdog.start + + sleep sleep_time * 3 + + expect(watchdog.strikes).to eq(0) + end + + it 'logs the event' do + expect(::Prometheus::PidProvider).to receive(:worker_id).at_least(:once).and_return('worker_1') + expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024) + expect(logger).to receive(:warn).with({ + message: 'heap fragmentation limit exceeded', + pid: Process.pid, + worker_id: 'worker_1', + memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble', + memwd_sleep_time_s: sleep_time, + memwd_max_heap_frag: max_heap_fragmentation, + memwd_cur_heap_frag: fragmentation, + memwd_max_strikes: max_strikes, + memwd_cur_strikes: max_strikes + 1, + memwd_rss_bytes: 1024 + }) + + watchdog.start + + sleep sleep_time * 3 + end + + it 'increments both the violations and violations handled counters' do + expect(heap_frag_violations_counter).to receive(:increment) + expect(heap_frag_violations_handled_counter).to receive(:increment) + + watchdog.start + + sleep sleep_time * 3 + end + + context 'when enforce_memory_watchdog ops toggle is off' do + before do + stub_feature_flags(enforce_memory_watchdog: false) + end + + it 'always uses the NullHandler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + expect(described_class::NullHandler.instance).to( + receive(:on_high_heap_fragmentation).with(fragmentation).and_return(true) + ) + + watchdog.start + + sleep sleep_time * 3 + end + end + end + + context 'when handler result is true' do + let(:max_strikes) { 1 } + + it 'considers the event handled and stops itself' do + expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true) + + watchdog.start + + sleep sleep_time * 3 + end + end + + context 'when handler result is false' do + let(:max_strikes) { 1 } + + it 'keeps running' do + # Return true the third time to terminate the daemon. + expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true) + + watchdog.start + + sleep sleep_time * 4 + end + end + end + + context 'when process exceeds heap fragmentation threshold temporarily' do + let(:fragmentation) { max_heap_fragmentation } + let(:max_strikes) { 1 } + + before do + allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return( + fragmentation - 0.1, + fragmentation + 0.2, + fragmentation - 0.1, + fragmentation + 0.1 + ) + end + + it 'does not signal the handler' do + expect(handler).not_to receive(:on_high_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 4 + end + end + + context 'when gitlab_memory_watchdog ops toggle is off' do + let(:fragmentation) { 0 } + let(:max_strikes) { 0 } + + before do + stub_feature_flags(gitlab_memory_watchdog: false) + end + + it 'does not monitor heap fragmentation' do + expect(Gitlab::Metrics::Memory).not_to receive(:gc_heap_fragmentation) + + watchdog.start + + sleep sleep_time * 3 + end + end + end + + context 'handlers' do + context 'NullHandler' do + subject(:handler) { described_class::NullHandler.instance } + + describe '#on_high_heap_fragmentation' do + it 'does nothing' do + expect(handler.on_high_heap_fragmentation(1.0)).to be(false) + end + end + end + + context 'TermProcessHandler' do + subject(:handler) { described_class::TermProcessHandler.new(42) } + + describe '#on_high_heap_fragmentation' do + it 'sends SIGTERM to the current process' do + expect(Process).to receive(:kill).with(:TERM, 42) + + expect(handler.on_high_heap_fragmentation(1.0)).to be(true) + end + end + end + + context 'PumaHandler' do + # rubocop: disable RSpec/VerifiedDoubles + # In tests, the Puma constant is not loaded so we cannot make this an instance_double. + let(:puma_worker_handle_class) { double('Puma::Cluster::WorkerHandle') } + let(:puma_worker_handle) { double('worker') } + # rubocop: enable RSpec/VerifiedDoubles + + subject(:handler) { described_class::PumaHandler.new({}) } + + before do + stub_const('::Puma::Cluster::WorkerHandle', puma_worker_handle_class) + end + + describe '#on_high_heap_fragmentation' do + it 'invokes orderly termination via Puma API' do + expect(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle) + expect(puma_worker_handle).to receive(:term) + + expect(handler.on_high_heap_fragmentation(1.0)).to be(true) + end + end + end + end +end diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb index 867ad843406..fb1601a5f9c 100644 --- a/spec/models/project_setting_spec.rb +++ b/spec/models/project_setting_spec.rb @@ -6,6 +6,17 @@ RSpec.describe ProjectSetting, type: :model do using RSpec::Parameterized::TableSyntax it { is_expected.to belong_to(:project) } + describe 'scopes' do + let_it_be(:project_1) { create(:project) } + let_it_be(:project_2) { create(:project) } + let_it_be(:project_setting_1) { create(:project_setting, project: project_1) } + let_it_be(:project_setting_2) { create(:project_setting, project: project_2) } + + it 'returns project setting for the given projects' do + expect(described_class.for_projects(project_1)).to contain_exactly(project_setting_1) + end + end + describe 'validations' do it { is_expected.not_to allow_value(nil).for(:target_platforms) } it { is_expected.to allow_value([]).for(:target_platforms) } diff --git a/spec/requests/api/graphql/crm/contacts_spec.rb b/spec/requests/api/graphql/crm/contacts_spec.rb new file mode 100644 index 00000000000..7e824140894 --- /dev/null +++ b/spec/requests/api/graphql/crm/contacts_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting CRM contacts' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :crm_enabled) } + + let_it_be(:contact_a) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "ghi@test.com", + description: "LMNO", + state: "inactive" + ) + end + + let_it_be(:contact_b) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "vwx@test.com", + description: "YZ", + state: "active" + ) + end + + let_it_be(:contact_c) do + create( + :contact, + group: group, + first_name: "PQR", + last_name: "STU", + email: "aaa@test.com", + description: "YZ", + state: "active" + ) + end + + before do + group.add_reporter(current_user) + end + + it_behaves_like 'sorted paginated query' do + let(:sort_argument) { {} } + let(:first_param) { 2 } + let(:all_records) { [contact_a, contact_b, contact_c] } + let(:data_path) { [:group, :contacts] } + + def pagination_query(params) + graphql_query_for( + :group, + { full_path: group.full_path }, + query_graphql_field(:contacts, params, "#{page_info} nodes { id }") + ) + end + + def pagination_results_data(nodes) + nodes.map { |item| GlobalID::Locator.locate(item['id']) } + end + end +end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 69e14eace66..596e023a027 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -223,6 +223,7 @@ RSpec.describe 'getting an issue list for a project' do end describe 'sorting and pagination' do + let_it_be(:sort_project) { create(:project, :public) } let_it_be(:data_path) { [:project, :issues] } def pagination_query(params) @@ -237,8 +238,38 @@ RSpec.describe 'getting an issue list for a project' do data.map { |issue| issue['iid'].to_i } end + context 'when sorting by severity' do + let_it_be(:severty_issue1) { create(:issue, project: sort_project) } + let_it_be(:severty_issue2) { create(:issue, project: sort_project) } + let_it_be(:severty_issue3) { create(:issue, project: sort_project) } + let_it_be(:severty_issue4) { create(:issue, project: sort_project) } + let_it_be(:severty_issue5) { create(:issue, project: sort_project) } + + before(:all) do + create(:issuable_severity, issue: severty_issue1, severity: :unknown) + create(:issuable_severity, issue: severty_issue2, severity: :low) + create(:issuable_severity, issue: severty_issue4, severity: :critical) + create(:issuable_severity, issue: severty_issue5, severity: :high) + end + + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_ASC } + let(:first_param) { 2 } + let(:all_records) { [severty_issue3.iid, severty_issue1.iid, severty_issue2.iid, severty_issue5.iid, severty_issue4.iid] } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_DESC } + let(:first_param) { 2 } + let(:all_records) { [severty_issue4.iid, severty_issue5.iid, severty_issue2.iid, severty_issue1.iid, severty_issue3.iid] } + end + end + end + context 'when sorting by due date' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } @@ -263,7 +294,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by relative position' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } @@ -285,7 +315,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by priority' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:on_project) { { project: sort_project } } let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) } let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) } @@ -321,7 +350,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by label priority' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:label1) { create(:label, project: sort_project, priority: 1) } let_it_be(:label2) { create(:label, project: sort_project, priority: 5) } let_it_be(:label3) { create(:label, project: sort_project, priority: 10) } @@ -348,7 +376,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by milestone due date' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } let_it_be(:milestone_issue1) { create(:issue, project: sort_project) } diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb new file mode 100644 index 00000000000..08d4ad2f9ba --- /dev/null +++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Mock Types +MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret) + +RSpec.describe Projects::GoogleCloud::ConfigurationController do + let_it_be(:project) { create(:project, :public) } + let_it_be(:url) { project_google_cloud_configuration_path(project) } + + let_it_be(:user_guest) { create(:user) } + let_it_be(:user_developer) { create(:user) } + let_it_be(:user_maintainer) { create(:user) } + + let_it_be(:unauthorized_members) { [user_guest, user_developer] } + let_it_be(:authorized_members) { [user_maintainer] } + + before do + project.add_guest(user_guest) + project.add_developer(user_developer) + project.add_maintainer(user_maintainer) + end + + context 'when accessed by unauthorized members' do + it 'returns not found on GET request' do + unauthorized_members.each do |unauthorized_member| + sign_in(unauthorized_member) + + get url + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'error_access_denied', + property: 'invalid_user', + project: project, + user: unauthorized_member + ) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when accessed by authorized members' do + it 'returns successful' do + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to be_successful + expect(response).to render_template('projects/google_cloud/configuration/index') + end + end + + context 'but gitlab instance is not configured for google oauth2' do + it 'returns forbidden' do + unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '') + allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) + .with('google_oauth2') + .and_return(unconfigured_google_oauth2) + + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to have_gitlab_http_status(:forbidden) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'google_oauth2_enabled!', + label: 'error_access_denied', + extra: { reason: 'google_oauth2_not_configured', + config: unconfigured_google_oauth2 }, + project: project, + user: authorized_member + ) + end + end + end + + context 'but feature flag is disabled' do + before do + stub_feature_flags(incubation_5mp_google_cloud: false) + end + + it 'returns not found' do + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'feature_flag_enabled!', + label: 'error_access_denied', + property: 'feature_flag_not_enabled', + project: project, + user: authorized_member + ) + end + end + end + + context 'but google oauth2 token is not valid' do + it 'does not return revoke oauth url' do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(false) + end + + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to be_successful + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'configuration#index', + label: 'success', + extra: { + configurationUrl: project_google_cloud_configuration_path(project), + deploymentsUrl: project_google_cloud_deployments_path(project), + databasesUrl: project_google_cloud_databases_path(project), + serviceAccounts: [], + createServiceAccountUrl: project_google_cloud_service_accounts_path(project), + emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'), + configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project), + gcpRegions: [], + revokeOauthUrl: nil + }, + project: project, + user: authorized_member + ) + end + end + end + end +end diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb new file mode 100644 index 00000000000..c9335f8f317 --- /dev/null +++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Mock Types +MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret) + +RSpec.describe Projects::GoogleCloud::DatabasesController do + let_it_be(:project) { create(:project, :public) } + let_it_be(:url) { project_google_cloud_databases_path(project) } + + let_it_be(:user_guest) { create(:user) } + let_it_be(:user_developer) { create(:user) } + let_it_be(:user_maintainer) { create(:user) } + + let_it_be(:unauthorized_members) { [user_guest, user_developer] } + let_it_be(:authorized_members) { [user_maintainer] } + + before do + project.add_guest(user_guest) + project.add_developer(user_developer) + project.add_maintainer(user_maintainer) + end + + context 'when accessed by unauthorized members' do + it 'returns not found on GET request' do + unauthorized_members.each do |unauthorized_member| + sign_in(unauthorized_member) + + get url + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'admin_project_google_cloud!', + label: 'error_access_denied', + property: 'invalid_user', + project: project, + user: unauthorized_member + ) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when accessed by authorized members' do + it 'returns successful' do + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to be_successful + expect(response).to render_template('projects/google_cloud/databases/index') + end + end + + context 'but gitlab instance is not configured for google oauth2' do + it 'returns forbidden' do + unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '') + allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) + .with('google_oauth2') + .and_return(unconfigured_google_oauth2) + + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to have_gitlab_http_status(:forbidden) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'google_oauth2_enabled!', + label: 'error_access_denied', + extra: { reason: 'google_oauth2_not_configured', + config: unconfigured_google_oauth2 }, + project: project, + user: authorized_member + ) + end + end + end + + context 'but feature flag is disabled' do + before do + stub_feature_flags(incubation_5mp_google_cloud: false) + end + + it 'returns not found' do + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to have_gitlab_http_status(:not_found) + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'feature_flag_enabled!', + label: 'error_access_denied', + property: 'feature_flag_not_enabled', + project: project, + user: authorized_member + ) + end + end + end + + context 'but google oauth2 token is not valid' do + it 'does not return revoke oauth url' do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(false) + end + + authorized_members.each do |authorized_member| + sign_in(authorized_member) + + get url + + expect(response).to be_successful + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'databases#index', + label: 'success', + extra: { + configurationUrl: project_google_cloud_configuration_path(project), + deploymentsUrl: project_google_cloud_deployments_path(project), + databasesUrl: project_google_cloud_databases_path(project) + }, + project: project, + user: authorized_member + ) + end + end + end + end +end diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb index 7bd9609a7dc..9e854e01516 100644 --- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb +++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb @@ -9,10 +9,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do let_it_be(:user_guest) { create(:user) } let_it_be(:user_developer) { create(:user) } let_it_be(:user_maintainer) { create(:user) } - let_it_be(:user_creator) { project.creator } let_it_be(:unauthorized_members) { [user_guest, user_developer] } - let_it_be(:authorized_members) { [user_maintainer, user_creator] } + let_it_be(:authorized_members) { [user_maintainer] } let_it_be(:urls_list) { %W[#{project_google_cloud_deployments_cloud_run_path(project)} #{project_google_cloud_deployments_cloud_storage_path(project)}] } @@ -32,7 +31,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'admin_project_google_cloud!', - label: 'access_denied', + label: 'error_access_denied', property: 'invalid_user', project: project, user: nil @@ -51,7 +50,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'admin_project_google_cloud!', - label: 'access_denied', + label: 'error_access_denied', property: 'invalid_user', project: project, user: nil @@ -87,15 +86,15 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do end end - it 'redirects to google_cloud home on enable service error' do + it 'redirects to google cloud deployments on enable service error' do get url - expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(response).to redirect_to(project_google_cloud_deployments_path(project)) # since GPC_PROJECT_ID is not set, enable cloud run service should return an error expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'deployments#cloud_run', - label: 'enable_cloud_run_error', + label: 'error_enable_cloud_run', extra: { message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.', status: :error }, project: project, @@ -103,7 +102,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do ) end - it 'redirects to gcp_error' do + it 'redirects to google cloud deployments with error' do mock_gcp_error = Google::Apis::ClientError.new('some_error') allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service| @@ -112,11 +111,11 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do get url - expect(response).to render_template(:gcp_error) + expect(response).to redirect_to(project_google_cloud_deployments_path(project)) expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'deployments#cloud_run', - label: 'gcp_error', + label: 'error_gcp', extra: mock_gcp_error, project: project, user: user_maintainer @@ -124,7 +123,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do end context 'GCP_PROJECT_IDs are defined' do - it 'redirects to google_cloud home on generate pipeline error' do + it 'redirects to google_cloud deployments on generate pipeline error' do allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service| allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success }) end @@ -135,11 +134,11 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do get url - expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(response).to redirect_to(project_google_cloud_deployments_path(project)) expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'deployments#cloud_run', - label: 'generate_pipeline_error', + label: 'error_generate_pipeline', extra: { status: :error }, project: project, user: user_maintainer @@ -162,7 +161,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'deployments#cloud_run', - label: 'cloud_run_success', + label: 'success', extra: { "title": "Enable deployments to Cloud Run", "description": "This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).\n\nThe `deploy-to-cloud-run` job:\n* Requires the following environment variables\n * `GCP_PROJECT_ID`\n * `GCP_SERVICE_ACCOUNT_KEY`\n* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library\n\nThis pipeline definition has been committed to the branch ``.\nYou may modify the pipeline definition further or accept the changes as-is if suitable.\n", "source_project_id": project.id, diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb index 56474b6520d..f88273080d5 100644 --- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb +++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb @@ -6,8 +6,8 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:repository) { project.repository } - let(:user_guest) { create(:user) } - let(:user_maintainer) { create(:user) } + let_it_be(:user_guest) { create(:user) } + let_it_be(:user_maintainer) { create(:user) } RSpec.shared_examples "should track not_found event" do it "tracks event" do @@ -15,7 +15,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'admin_project_google_cloud!', - label: 'access_denied', + label: 'error_access_denied', property: 'invalid_user', project: project, user: nil @@ -29,7 +29,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'admin_project_google_cloud!', - label: 'access_denied', + label: 'error_access_denied', property: 'invalid_user', project: project, user: nil @@ -43,7 +43,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'feature_flag_enabled!', - label: 'access_denied', + label: 'error_access_denied', property: 'feature_flag_not_enabled', project: project, user: user_maintainer @@ -57,7 +57,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'google_oauth2_enabled!', - label: 'access_denied', + label: 'error_access_denied', extra: { reason: 'google_oauth2_not_configured', config: config }, project: project, user: user_maintainer @@ -144,8 +144,8 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do sign_in(user_maintainer) end - it 'redirects to google cloud index' do - is_expected.to redirect_to(project_google_cloud_index_path(project)) + it 'redirects to google cloud configurations' do + is_expected.to redirect_to(project_google_cloud_configuration_path(project)) end end end diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb index 07590d3710e..36441a184cb 100644 --- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb +++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb @@ -47,13 +47,13 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do post url expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil - expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:notice]).to eq('Google OAuth2 token revocation requested') expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'revoke_oauth#create', - label: 'create', - property: 'success', + label: 'success', + property: '{}', project: project, user: user ) @@ -70,13 +70,13 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do post url expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil - expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:alert]).to eq('Google OAuth2 token revocation request failed') expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'revoke_oauth#create', - label: 'create', - property: 'failed', + label: 'error', + property: '{}', project: project, user: user ) diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb index 4b32965e2b0..ae2519855db 100644 --- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb +++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb @@ -8,13 +8,15 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do describe 'GET index', :snowplow do let_it_be(:url) { "#{project_google_cloud_service_accounts_path(project)}" } - let(:user_guest) { create(:user) } - let(:user_developer) { create(:user) } - let(:user_maintainer) { create(:user) } - let(:user_creator) { project.creator } + let_it_be(:user_guest) { create(:user) } + let_it_be(:user_developer) { create(:user) } + let_it_be(:user_maintainer) { create(:user) } + let_it_be(:user_creator) { project.creator } - let(:unauthorized_members) { [user_guest, user_developer] } - let(:authorized_members) { [user_maintainer, user_creator] } + let_it_be(:unauthorized_members) { [user_guest, user_developer] } + let_it_be(:authorized_members) { [user_maintainer, user_creator] } + + let_it_be(:google_client_error) { Google::Apis::ClientError.new('client-error') } before do project.add_guest(user_guest) @@ -30,7 +32,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'admin_project_google_cloud!', - label: 'access_denied', + label: 'error_access_denied', property: 'invalid_user', project: project, user: nil @@ -53,7 +55,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'admin_project_google_cloud!', - label: 'access_denied', + label: 'error_access_denied', property: 'invalid_user', project: project, user: unauthorized_member @@ -71,7 +73,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do expect_snowplow_event( category: 'Projects::GoogleCloud', action: 'admin_project_google_cloud!', - label: 'access_denied', + label: 'error_access_denied', property: 'invalid_user', project: project, user: unauthorized_member @@ -116,7 +118,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do end end - it 'renders no_gcp_projects' do + it 'flashes error and redirects to google cloud configurations' do authorized_members.each do |authorized_member| allow_next_instance_of(BranchesFinder) do |branches_finder| allow(branches_finder).to receive(:execute).and_return([]) @@ -130,7 +132,16 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do get url - expect(response).to render_template('projects/google_cloud/errors/no_gcp_projects') + expect(response).to redirect_to(project_google_cloud_configuration_path(project)) + expect(flash[:warning]).to eq('No Google Cloud projects - You need at least one Google Cloud project') + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'service_accounts#index', + label: 'error_form', + property: 'no_gcp_projects', + project: project, + user: authorized_member + ) end end end @@ -171,7 +182,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do post url, params: { gcp_project: 'prj1', ref: 'env1' } - expect(response).to redirect_to(project_google_cloud_index_path(project)) + expect(response).to redirect_to(project_google_cloud_configuration_path(project)) end end end @@ -181,29 +192,47 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do before do allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| allow(client).to receive(:validate_token).and_return(true) - allow(client).to receive(:list_projects).and_raise(Google::Apis::ClientError.new('')) - allow(client).to receive(:create_service_account).and_raise(Google::Apis::ClientError.new('')) - allow(client).to receive(:create_service_account_key).and_raise(Google::Apis::ClientError.new('')) + allow(client).to receive(:list_projects).and_raise(google_client_error) + allow(client).to receive(:create_service_account).and_raise(google_client_error) + allow(client).to receive(:create_service_account_key).and_raise(google_client_error) end end - it 'renders gcp_error template on GET' do + it 'GET flashes error and redirects to -/google_cloud/configurations' do authorized_members.each do |authorized_member| sign_in(authorized_member) get url - expect(response).to render_template(:gcp_error) + expect(response).to redirect_to(project_google_cloud_configuration_path(project)) + expect(flash[:warning]).to eq('Google Cloud Error - client-error') + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'service_accounts#index', + label: 'error_gcp', + extra: google_client_error, + project: project, + user: authorized_member + ) end end - it 'renders gcp_error template on POST' do + it 'POST flashes error and redirects to -/google_cloud/configurations' do authorized_members.each do |authorized_member| sign_in(authorized_member) post url, params: { gcp_project: 'prj1', environment: 'env1' } - expect(response).to render_template(:gcp_error) + expect(response).to redirect_to(project_google_cloud_configuration_path(project)) + expect(flash[:warning]).to eq('Google Cloud Error - client-error') + expect_snowplow_event( + category: 'Projects::GoogleCloud', + action: 'service_accounts#create', + label: 'error_gcp', + extra: google_client_error, + project: project, + user: authorized_member + ) end end end diff --git a/spec/requests/projects/google_cloud_controller_spec.rb b/spec/requests/projects/google_cloud_controller_spec.rb deleted file mode 100644 index d0814990989..00000000000 --- a/spec/requests/projects/google_cloud_controller_spec.rb +++ /dev/null @@ -1,178 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# Mock Types -MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret) - -RSpec.describe Projects::GoogleCloudController do - let_it_be(:project) { create(:project, :public) } - - describe 'GET index', :snowplow do - let_it_be(:url) { "#{project_google_cloud_index_path(project)}" } - - context 'when a public request is made' do - it 'returns not found' do - get url - - expect(response).to have_gitlab_http_status(:not_found) - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'access_denied', - property: 'invalid_user', - project: project, - user: nil) - end - end - - context 'when a project.guest makes request' do - let(:user) { create(:user) } - - it 'returns not found' do - project.add_guest(user) - sign_in(user) - - get url - - expect(response).to have_gitlab_http_status(:not_found) - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'access_denied', - property: 'invalid_user', - project: project, - user: user - ) - end - end - - context 'when project.developer makes request' do - let(:user) { create(:user) } - - it 'returns not found' do - project.add_developer(user) - sign_in(user) - - get url - - expect(response).to have_gitlab_http_status(:not_found) - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'access_denied', - property: 'invalid_user', - project: project, - user: user - ) - end - end - - context 'when project.maintainer makes request' do - let(:user) { create(:user) } - - it 'returns successful' do - project.add_maintainer(user) - sign_in(user) - - get url - - expect(response).to be_successful - end - end - - context 'when project.creator makes request' do - let(:user) { project.creator } - - it 'returns successful' do - sign_in(user) - - get url - - expect(response).to be_successful - end - end - - describe 'when authorized user makes request' do - let(:user) { project.creator } - - context 'but gitlab instance is not configured for google oauth2' do - it 'returns forbidden' do - unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '') - allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for) - .with('google_oauth2') - .and_return(unconfigured_google_oauth2) - - sign_in(user) - - get url - - expect(response).to have_gitlab_http_status(:forbidden) - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'google_oauth2_enabled!', - label: 'access_denied', - extra: { reason: 'google_oauth2_not_configured', - config: unconfigured_google_oauth2 }, - project: project, - user: user - ) - end - end - - context 'but feature flag is disabled' do - before do - stub_feature_flags(incubation_5mp_google_cloud: false) - end - - it 'returns not found' do - sign_in(user) - - get url - - expect(response).to have_gitlab_http_status(:not_found) - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'feature_flag_enabled!', - label: 'access_denied', - property: 'feature_flag_not_enabled', - project: project, - user: user - ) - end - end - - context 'but google oauth2 token is not valid' do - it 'does not return revoke oauth url' do - allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| - allow(client).to receive(:validate_token).and_return(false) - end - - sign_in(user) - - get url - - expect(response).to be_successful - expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'google_cloud#index', - label: 'index', - extra: { - screen: 'home', - serviceAccounts: [], - createServiceAccountUrl: project_google_cloud_service_accounts_path(project), - enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project), - enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project), - emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'), - configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project), - gcpRegions: [], - revokeOauthUrl: nil - }, - project: project, - user: user - ) - end - end - end - end -end diff --git a/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb b/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb index e2f5a2e719e..b2cd5632be0 100644 --- a/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb +++ b/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe GoogleCloud::GcpRegionAddOrReplaceService do service.execute('env_2', 'loc_2') service.execute('env_1', 'loc_3') - list = project.variables.reload.filter { |variable| variable.key == Projects::GoogleCloudController::GCP_REGION_CI_VAR_KEY } + list = project.variables.reload.filter { |variable| variable.key == Projects::GoogleCloud::GcpRegionsController::GCP_REGION_CI_VAR_KEY } list = list.sort_by(&:environment_scope) aggregate_failures 'testing list of gcp regions' do diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index f0d7f570c19..fbcca215282 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -439,6 +439,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do before do TestEnv.clean_test_path create(:group_member, :owner, group: new_parent_group, user: user) + allow(transfer_service).to receive(:update_project_settings) transfer_service.execute(new_parent_group) end @@ -478,6 +479,11 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do end end + it 'invokes #update_project_settings' do + expect(transfer_service).to have_received(:update_project_settings) + .with(group.projects.pluck(:id)) + end + it_behaves_like 'project namespace path is in sync with project path' do let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" } let(:projects_with_project_namespace) { [project1, project2] } diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index a479f3a82ff..e3c2466f807 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -532,7 +532,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do describe 'Google Cloud' do it 'has a link to the google cloud page' do render - expect(rendered).to have_link('Google Cloud', href: project_google_cloud_index_path(project)) + expect(rendered).to have_link('Google Cloud', href: project_google_cloud_configuration_path(project)) end describe 'when the user does not have access' do |