Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-18 21:08:47 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-18 21:08:47 +0300
commit128d4d89e98177996d1ff6e0b3d7a8a0c9b35929 (patch)
tree88b02d3bf972bac281d673e99f854303e0dd13ed /spec
parentcc1066db64a2a283a3d229b9bbb67c01716ca871 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/pipelines/tests_controller_spec.rb2
-rw-r--r--spec/features/groups/show_spec.rb25
-rw-r--r--spec/frontend/ci_lint/mock_data.js11
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js77
-rw-r--r--spec/frontend/google_cloud/components/errors/gcp_error_spec.js34
-rw-r--r--spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js33
-rw-r--r--spec/frontend/google_cloud/components/google_cloud_menu_spec.js40
-rw-r--r--spec/frontend/google_cloud/components/home_spec.js66
-rw-r--r--spec/frontend/google_cloud/components/incubation_banner_spec.js21
-rw-r--r--spec/frontend/google_cloud/components/revoke_oauth_spec.js2
-rw-r--r--spec/frontend/google_cloud/configuration/panel_spec.js65
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js (renamed from spec/frontend/google_cloud/components/cloudsql/create_instance_form_spec.js)4
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js (renamed from spec/frontend/google_cloud/components/cloudsql/instance_table_spec.js)4
-rw-r--r--spec/frontend/google_cloud/databases/panel_spec.js36
-rw-r--r--spec/frontend/google_cloud/databases/service_table_spec.js (renamed from spec/frontend/google_cloud/components/databases/service_table_spec.js)4
-rw-r--r--spec/frontend/google_cloud/deployments/panel_spec.js46
-rw-r--r--spec/frontend/google_cloud/deployments/service_table_spec.js (renamed from spec/frontend/google_cloud/components/deployments_service_table_spec.js)4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/form_spec.js (renamed from spec/frontend/google_cloud/components/gcp_regions_form_spec.js)4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/list_spec.js (renamed from spec/frontend/google_cloud/components/gcp_regions_list_spec.js)4
-rw-r--r--spec/frontend/google_cloud/service_accounts/form_spec.js (renamed from spec/frontend/google_cloud/components/service_accounts_form_spec.js)4
-rw-r--r--spec/frontend/google_cloud/service_accounts/list_spec.js (renamed from spec/frontend/google_cloud/components/service_accounts_list_spec.js)4
-rw-r--r--spec/frontend/groups/components/group_item_spec.js101
-rw-r--r--spec/frontend/groups/components/groups_spec.js72
-rw-r--r--spec/frontend/groups/mock_data.js1
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js22
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js78
-rw-r--r--spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js251
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js1
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js2
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb308
-rw-r--r--spec/models/project_setting_spec.rb11
-rw-r--r--spec/requests/api/graphql/crm/contacts_spec.rb69
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb37
-rw-r--r--spec/requests/projects/google_cloud/configuration_controller_spec.rb141
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb135
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb27
-rw-r--r--spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb16
-rw-r--r--spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb12
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb67
-rw-r--r--spec/requests/projects/google_cloud_controller_spec.rb178
-rw-r--r--spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb2
-rw-r--r--spec/services/groups/transfer_service_spec.rb6
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb2
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