summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-30 15:12:24 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-30 15:12:24 +0000
commiteaec42f9e37fe51f9c53fa7079639ec9f4c40efc (patch)
tree2abbab9659bc8e043b2dbb9dcf797a5aab717767 /spec
parent4e8c8922da341914b9fd5570ec9ce7a29ffdfebd (diff)
downloadgitlab-ce-eaec42f9e37fe51f9c53fa7079639ec9f4c40efc.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/dashboard/activity_spec.rb6
-rw-r--r--spec/features/projects/new_project_spec.rb8
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js119
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js307
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js277
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js15
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/reference_cache_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/retry_spec.rb1
-rw-r--r--spec/models/ci/pipeline_spec.rb26
-rw-r--r--spec/models/concerns/vulnerability_finding_helpers_spec.rb27
-rw-r--r--spec/requests/import/url_controller_spec.rb45
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb68
-rw-r--r--spec/services/ci/stuck_builds/drop_running_service_spec.rb54
-rw-r--r--spec/services/import/validate_remote_git_endpoint_service_spec.rb88
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb21
17 files changed, 695 insertions, 409 deletions
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index e75e661b513..7390edc3c47 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -13,19 +13,19 @@ RSpec.describe 'Dashboard > Activity' do
it 'shows Your Projects' do
visit activity_dashboard_path
- expect(find('.top-area .nav-tabs li.active')).to have_content('Your projects')
+ expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Your projects')
end
it 'shows Starred Projects' do
visit activity_dashboard_path(filter: 'starred')
- expect(find('.top-area .nav-tabs li.active')).to have_content('Starred projects')
+ expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Starred projects')
end
it 'shows Followed Projects' do
visit activity_dashboard_path(filter: 'followed')
- expect(find('.top-area .nav-tabs li.active')).to have_content('Followed users')
+ expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Followed users')
end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 39f9d3b331b..dacbaa826a0 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -296,12 +296,16 @@ RSpec.describe 'New project', :js do
expect(git_import_instructions).to have_content 'Git repository URL'
end
- it 'reports error if repo URL does not end with .git' do
+ it 'reports error if repo URL is not a valid Git repository' do
+ stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(status: 200, body: "not-a-git-repo")
+
fill_in 'project_import_url', with: 'http://foo/bar'
# simulate blur event
find('body').click
- expect(page).to have_text('A repository URL usually ends in a .git suffix')
+ wait_for_requests
+
+ expect(page).to have_text('There is not a valid Git repository at this URL')
end
it 'keeps "Import project" tab open after form validation error' do
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 2537b8fb816..36b81b3eb28 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import getProjects from '~/analytics/shared/graphql/projects.query.graphql';
@@ -25,6 +26,17 @@ const projects = [
},
];
+const MockGlDropdown = stubComponent(GlDropdown, {
+ template: `
+ <div>
+ <div data-testid="vsa-highlighted-items">
+ <slot name="highlighted-items"></slot>
+ </div>
+ <div data-testid="vsa-default-items"><slot></slot></div>
+ </div>
+ `,
+});
+
const defaultMocks = {
$apollo: {
query: jest.fn().mockResolvedValue({
@@ -38,22 +50,32 @@ let spyQuery;
describe('ProjectsDropdownFilter component', () => {
let wrapper;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, stubs = {}) => {
spyQuery = defaultMocks.$apollo.query;
- wrapper = mount(ProjectsDropdownFilter, {
+ wrapper = mountExtended(ProjectsDropdownFilter, {
mocks: { ...defaultMocks },
propsData: {
groupId: 1,
groupNamespace: 'gitlab-org',
...props,
},
+ stubs,
});
};
+ const createWithMockDropdown = (props) => {
+ createComponent(props, { GlDropdown: MockGlDropdown });
+ return wrapper.vm.$nextTick();
+ };
+
afterEach(() => {
wrapper.destroy();
});
+ const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items');
+ const findHighlightedItemsTitle = () => wrapper.findByText('Selected');
+ const findClearAllButton = () => wrapper.findByText('Clear all');
+
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () =>
@@ -75,8 +97,19 @@ describe('ProjectsDropdownFilter component', () => {
const findDropdownFullPathAtIndex = (index) =>
findDropdownAtIndex(index).find('[data-testid="project-full-path"]');
- const selectDropdownItemAtIndex = (index) =>
+ const selectDropdownItemAtIndex = (index) => {
findDropdownAtIndex(index).find('button').trigger('click');
+ return wrapper.vm.$nextTick();
+ };
+
+ // NOTE: Selected items are now visually separated from unselected items
+ const findSelectedDropdownItems = () => findHighlightedItems().findAll(GlDropdownItem);
+
+ const findSelectedDropdownAtIndex = (index) => findSelectedDropdownItems().at(index);
+ const findSelectedButtonIdentIconAtIndex = (index) =>
+ findSelectedDropdownAtIndex(index).find('div.gl-avatar-identicon');
+ const findSelectedButtonAvatarItemAtIndex = (index) =>
+ findSelectedDropdownAtIndex(index).find('img.gl-avatar');
const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id);
@@ -109,7 +142,62 @@ describe('ProjectsDropdownFilter component', () => {
});
});
- describe('when passed a an array of defaultProject as prop', () => {
+ describe('highlighted items', () => {
+ const blockDefaultProps = { multiSelect: true };
+ beforeEach(() => {
+ createComponent(blockDefaultProps);
+ });
+
+ describe('with no project selected', () => {
+ it('does not render the highlighted items', async () => {
+ await createWithMockDropdown(blockDefaultProps);
+ expect(findSelectedDropdownItems().length).toBe(0);
+ });
+
+ it('does not render the highlighted items title', () => {
+ expect(findHighlightedItemsTitle().exists()).toBe(false);
+ });
+
+ it('does not render the clear all button', () => {
+ expect(findClearAllButton().exists()).toBe(false);
+ });
+ });
+
+ describe('with a selected project', () => {
+ beforeEach(async () => {
+ await selectDropdownItemAtIndex(0);
+ });
+
+ it('renders the highlighted items', async () => {
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(0);
+
+ expect(findSelectedDropdownItems().length).toBe(1);
+ });
+
+ it('renders the highlighted items title', () => {
+ expect(findHighlightedItemsTitle().exists()).toBe(true);
+ });
+
+ it('renders the clear all button', () => {
+ expect(findClearAllButton().exists()).toBe(true);
+ });
+
+ it('clears all selected items when the clear all button is clicked', async () => {
+ await selectDropdownItemAtIndex(1);
+
+ expect(wrapper.text()).toContain('2 projects selected');
+
+ findClearAllButton().trigger('click');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.text()).not.toContain('2 projects selected');
+ expect(wrapper.text()).toContain('Select projects');
+ });
+ });
+ });
+
+ describe('when passed an array of defaultProject as prop', () => {
beforeEach(() => {
createComponent({
defaultProjects: [projects[0]],
@@ -130,8 +218,9 @@ describe('ProjectsDropdownFilter component', () => {
});
describe('when multiSelect is false', () => {
+ const blockDefaultProps = { multiSelect: false };
beforeEach(() => {
- createComponent({ multiSelect: false });
+ createComponent(blockDefaultProps);
});
describe('displays the correct information', () => {
@@ -183,21 +272,19 @@ describe('ProjectsDropdownFilter component', () => {
});
it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => {
- selectDropdownItemAtIndex(0);
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(0);
- await wrapper.vm.$nextTick().then(() => {
- expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true);
- expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false);
- });
+ expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(true);
+ expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(false);
});
it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => {
- selectDropdownItemAtIndex(1);
+ await createWithMockDropdown(blockDefaultProps);
+ await selectDropdownItemAtIndex(1);
- await wrapper.vm.$nextTick().then(() => {
- expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false);
- expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true);
- });
+ expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(false);
+ expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index f2877a1f2a5..02f9451152f 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -1,28 +1,20 @@
-import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
-import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
+import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
+
import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
-import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+
import {
- PACKAGE_SETTINGS_HEADER,
- PACKAGE_SETTINGS_DESCRIPTION,
- PACKAGES_DOCS_PATH,
ERROR_UPDATING_SETTINGS,
SUCCESS_UPDATING_SETTINGS,
} from '~/packages_and_registries/settings/group/constants';
-import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
-import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
-import {
- groupPackageSettingsMock,
- groupPackageSettingsMutationMock,
- groupPackageSettingsMutationErrorMock,
-} from '../mock_data';
+import { groupPackageSettingsMock, packageSettings } from '../mock_data';
jest.mock('~/flash');
@@ -39,35 +31,18 @@ describe('Group Settings App', () => {
};
const mountComponent = ({
- provide = defaultProvide,
resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock),
- mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()),
- data = {},
} = {}) => {
localVue.use(VueApollo);
- const requestHandlers = [
- [getGroupPackagesSettingsQuery, resolver],
- [updateNamespacePackageSettings, mutationResolver],
- ];
+ const requestHandlers = [[getGroupPackagesSettingsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
localVue,
apolloProvider,
- provide,
- data() {
- return {
- ...data,
- };
- },
- stubs: {
- GlSprintf,
- SettingsBlock,
- MavenSettings,
- GenericSettings,
- },
+ provide: defaultProvide,
mocks: {
$toast: {
show,
@@ -84,271 +59,73 @@ describe('Group Settings App', () => {
wrapper.destroy();
});
- const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
- const findDescription = () => wrapper.find('[data-testid="description"');
- const findLink = () => wrapper.findComponent(GlLink);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findMavenSettings = () => wrapper.findComponent(MavenSettings);
- const findMavenDuplicatedSettings = () => findMavenSettings().findComponent(DuplicatesSettings);
- const findGenericSettings = () => wrapper.findComponent(GenericSettings);
- const findGenericDuplicatedSettings = () =>
- findGenericSettings().findComponent(DuplicatesSettings);
+ const findPackageSettings = () => wrapper.findComponent(PackagesSettings);
const waitForApolloQueryAndRender = async () => {
await waitForPromises();
- await wrapper.vm.$nextTick();
- };
-
- const emitSettingsUpdate = (override) => {
- findMavenDuplicatedSettings().vm.$emit('update', {
- mavenDuplicateExceptionRegex: ')',
- ...override,
- });
+ await nextTick();
};
- it('renders a settings block', () => {
- mountComponent();
-
- expect(findSettingsBlock().exists()).toBe(true);
- });
-
- it('passes the correct props to settings block', () => {
- mountComponent();
-
- expect(findSettingsBlock().props('defaultExpanded')).toBe(false);
- });
-
- it('has the correct header text', () => {
- mountComponent();
-
- expect(wrapper.text()).toContain(PACKAGE_SETTINGS_HEADER);
- });
-
- it('has the correct description text', () => {
- mountComponent();
-
- expect(findDescription().text()).toMatchInterpolatedText(PACKAGE_SETTINGS_DESCRIPTION);
- });
-
- it('has the correct link', () => {
- mountComponent();
-
- expect(findLink().attributes()).toMatchObject({
- href: PACKAGES_DOCS_PATH,
- target: '_blank',
- });
- expect(findLink().text()).toBe('Learn more.');
- });
-
- it('calls the graphql API with the proper variables', () => {
- const resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock);
- mountComponent({ resolver });
-
- expect(resolver).toHaveBeenCalledWith({
- fullPath: defaultProvide.groupPath,
- });
- });
-
- describe('maven settings', () => {
- it('exists', () => {
- mountComponent();
-
- expect(findMavenSettings().exists()).toBe(true);
- });
-
- it('assigns duplication allowness and exception props', async () => {
- mountComponent();
-
- expect(findMavenDuplicatedSettings().props('loading')).toBe(true);
-
- await waitForApolloQueryAndRender();
-
- const {
- mavenDuplicatesAllowed,
- mavenDuplicateExceptionRegex,
- } = groupPackageSettingsMock.data.group.packageSettings;
-
- expect(findMavenDuplicatedSettings().props()).toMatchObject({
- duplicatesAllowed: mavenDuplicatesAllowed,
- duplicateExceptionRegex: mavenDuplicateExceptionRegex,
- duplicateExceptionRegexError: '',
- loading: false,
- });
- });
-
- it('on update event calls the mutation', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
- mountComponent({ mutationResolver });
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- expect(mutationResolver).toHaveBeenCalledWith({
- input: { mavenDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
- });
- });
- });
-
- describe('generic settings', () => {
- it('exists', () => {
+ describe.each`
+ finder | entityProp | entityValue
+ ${findPackageSettings} | ${'packageSettings'} | ${packageSettings()}
+ `('settings blocks', ({ finder, entityProp, entityValue }) => {
+ beforeEach(() => {
mountComponent();
-
- expect(findGenericSettings().exists()).toBe(true);
+ return waitForApolloQueryAndRender();
});
- it('assigns duplication allowness and exception props', async () => {
- mountComponent();
-
- expect(findGenericDuplicatedSettings().props('loading')).toBe(true);
-
- await waitForApolloQueryAndRender();
-
- const {
- genericDuplicatesAllowed,
- genericDuplicateExceptionRegex,
- } = groupPackageSettingsMock.data.group.packageSettings;
-
- expect(findGenericDuplicatedSettings().props()).toMatchObject({
- duplicatesAllowed: genericDuplicatesAllowed,
- duplicateExceptionRegex: genericDuplicateExceptionRegex,
- duplicateExceptionRegexError: '',
- loading: false,
- });
+ it('renders the settings block', () => {
+ expect(finder().exists()).toBe(true);
});
- it('on update event calls the mutation', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
- mountComponent({ mutationResolver });
-
- await waitForApolloQueryAndRender();
-
- findMavenDuplicatedSettings().vm.$emit('update', {
- genericDuplicateExceptionRegex: ')',
- });
-
- expect(mutationResolver).toHaveBeenCalledWith({
- input: { genericDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ it('binds the correctProps', () => {
+ expect(finder().props()).toMatchObject({
+ isLoading: false,
+ [entityProp]: entityValue,
});
});
- });
-
- describe('settings update', () => {
- describe('success state', () => {
- it('shows a success alert', async () => {
- mountComponent();
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- await waitForPromises();
+ describe('success event', () => {
+ it('shows a success toast', () => {
+ finder().vm.$emit('success');
expect(show).toHaveBeenCalledWith(SUCCESS_UPDATING_SETTINGS);
});
- it('has an optimistic response', async () => {
- const mavenDuplicateExceptionRegex = 'latest[main]something';
- mountComponent();
-
- await waitForApolloQueryAndRender();
-
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe('');
-
- emitSettingsUpdate({ mavenDuplicateExceptionRegex });
-
- // wait for apollo to update the model with the optimistic response
- await wrapper.vm.$nextTick();
-
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe(
- mavenDuplicateExceptionRegex,
- );
-
- // wait for the call to resolve
- await waitForPromises();
-
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe(
- mavenDuplicateExceptionRegex,
- );
- });
- });
+ it('hides the error alert', async () => {
+ finder().vm.$emit('error');
+ await nextTick();
- describe('errors', () => {
- const verifyAlert = () => {
expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(ERROR_UPDATING_SETTINGS);
- expect(findAlert().props('variant')).toBe('warning');
- };
-
- it('mutation payload with root level errors', async () => {
- // note this is a complex test that covers all the path around errors that are shown in the form
- // it's one single it case, due to the expensive preparation and execution
- const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationErrorMock);
- mountComponent({ mutationResolver });
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- await waitForApolloQueryAndRender();
-
- // errors are bound to the component
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe(
- groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message,
- );
- // general error message is shown
+ finder().vm.$emit('success');
+ await nextTick();
- verifyAlert();
-
- emitSettingsUpdate();
-
- await wrapper.vm.$nextTick();
-
- // errors are reset on mutation call
- expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe('');
+ expect(findAlert().exists()).toBe(false);
});
+ });
- it.each`
- type | mutationResolver
- ${'local'} | ${jest.fn().mockResolvedValue(groupPackageSettingsMutationMock({ errors: ['foo'] }))}
- ${'network'} | ${jest.fn().mockRejectedValue()}
- `('mutation payload with $type error', async ({ mutationResolver }) => {
- mountComponent({ mutationResolver });
-
- await waitForApolloQueryAndRender();
-
- emitSettingsUpdate();
-
- await waitForPromises();
-
- verifyAlert();
+ describe('error event', () => {
+ beforeEach(() => {
+ finder().vm.$emit('error');
+ return nextTick();
});
- it('a successful request dismisses the alert', async () => {
- mountComponent({ data: { alertMessage: 'foo' } });
-
- await waitForApolloQueryAndRender();
-
+ it('shows an alert', () => {
expect(findAlert().exists()).toBe(true);
-
- emitSettingsUpdate();
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(false);
});
- it('dismiss event from alert dismiss it from the page', async () => {
- mountComponent({ data: { alertMessage: 'foo' } });
-
- await waitForApolloQueryAndRender();
+ it('alert has the right text', () => {
+ expect(findAlert().text()).toBe(ERROR_UPDATING_SETTINGS);
+ });
+ it('dismissing the alert removes it', async () => {
expect(findAlert().exists()).toBe(true);
findAlert().vm.$emit('dismiss');
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
new file mode 100644
index 00000000000..693af21e24a
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -0,0 +1,277 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
+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 waitForPromises from 'helpers/wait_for_promises';
+import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
+import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
+import component from '~/packages_and_registries/settings/group/components/packages_settings.vue';
+import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+import {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ PACKAGES_DOCS_PATH,
+} from '~/packages_and_registries/settings/group/constants';
+
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ packageSettings,
+ groupPackageSettingsMock,
+ groupPackageSettingsMutationMock,
+ groupPackageSettingsMutationErrorMock,
+} from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
+
+const localVue = createLocalVue();
+
+describe('Packages Settings', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const defaultProvide = {
+ defaultExpanded: false,
+ groupPath: 'foo_group_path',
+ };
+
+ const mountComponent = ({
+ mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()),
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[updateNamespacePackageSettings, mutationResolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(component, {
+ localVue,
+ apolloProvider,
+ provide: defaultProvide,
+ propsData: {
+ packageSettings: packageSettings(),
+ },
+ stubs: {
+ GlSprintf,
+ SettingsBlock,
+ MavenSettings,
+ GenericSettings,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findDescription = () => wrapper.findByTestId('description');
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findMavenSettings = () => wrapper.findComponent(MavenSettings);
+ const findMavenDuplicatedSettings = () => findMavenSettings().findComponent(DuplicatesSettings);
+ const findGenericSettings = () => wrapper.findComponent(GenericSettings);
+ const findGenericDuplicatedSettings = () =>
+ findGenericSettings().findComponent(DuplicatesSettings);
+
+ const fillApolloCache = () => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getGroupPackagesSettingsQuery,
+ variables: {
+ fullPath: defaultProvide.groupPath,
+ },
+ ...groupPackageSettingsMock,
+ });
+ };
+
+ const emitMavenSettingsUpdate = (override) => {
+ findMavenDuplicatedSettings().vm.$emit('update', {
+ mavenDuplicateExceptionRegex: ')',
+ ...override,
+ });
+ };
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('passes the correct props to settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().props('defaultExpanded')).toBe(false);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(PACKAGE_SETTINGS_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(PACKAGE_SETTINGS_DESCRIPTION);
+ });
+
+ it('has the correct link', () => {
+ mountComponent();
+
+ expect(findLink().attributes()).toMatchObject({
+ href: PACKAGES_DOCS_PATH,
+ target: '_blank',
+ });
+ expect(findLink().text()).toBe('Learn more.');
+ });
+
+ describe('maven settings', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findMavenSettings().exists()).toBe(true);
+ });
+
+ it('assigns duplication allowness and exception props', async () => {
+ mountComponent();
+
+ const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings();
+
+ expect(findMavenDuplicatedSettings().props()).toMatchObject({
+ duplicatesAllowed: mavenDuplicatesAllowed,
+ duplicateExceptionRegex: mavenDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
+ loading: false,
+ });
+ });
+
+ it('on update event calls the mutation', () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ emitMavenSettingsUpdate();
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { mavenDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
+ describe('generic settings', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findGenericSettings().exists()).toBe(true);
+ });
+
+ it('assigns duplication allowness and exception props', async () => {
+ mountComponent();
+
+ const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings();
+
+ expect(findGenericDuplicatedSettings().props()).toMatchObject({
+ duplicatesAllowed: genericDuplicatesAllowed,
+ duplicateExceptionRegex: genericDuplicateExceptionRegex,
+ duplicateExceptionRegexError: '',
+ loading: false,
+ });
+ });
+
+ it('on update event calls the mutation', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ findMavenDuplicatedSettings().vm.$emit('update', {
+ genericDuplicateExceptionRegex: ')',
+ });
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { genericDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
+ describe('settings update', () => {
+ describe('success state', () => {
+ it('emits a success event', async () => {
+ mountComponent();
+
+ fillApolloCache();
+ emitMavenSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('success')).toEqual([[]]);
+ });
+
+ it('has an optimistic response', () => {
+ const mavenDuplicateExceptionRegex = 'latest[main]something';
+ mountComponent();
+
+ fillApolloCache();
+
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe('');
+
+ emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex });
+
+ expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
+ ...packageSettings(),
+ mavenDuplicateExceptionRegex,
+ });
+ });
+ });
+
+ describe('errors', () => {
+ it('mutation payload with root level errors', async () => {
+ // note this is a complex test that covers all the path around errors that are shown in the form
+ // it's one single it case, due to the expensive preparation and execution
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationErrorMock);
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ emitMavenSettingsUpdate();
+
+ await waitForPromises();
+
+ // errors are bound to the component
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe(
+ groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message,
+ );
+
+ // general error message is shown
+
+ expect(wrapper.emitted('error')).toEqual([[]]);
+
+ emitMavenSettingsUpdate();
+
+ await wrapper.vm.$nextTick();
+
+ // errors are reset on mutation call
+ expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe('');
+ });
+
+ it.each`
+ type | mutationResolver
+ ${'local'} | ${jest.fn().mockResolvedValue(groupPackageSettingsMutationMock({ errors: ['foo'] }))}
+ ${'network'} | ${jest.fn().mockRejectedValue()}
+ `('mutation payload with $type error', async ({ mutationResolver }) => {
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+ emitMavenSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index 65119e288a1..917dbfddd64 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -1,12 +1,15 @@
+export const packageSettings = () => ({
+ mavenDuplicatesAllowed: true,
+ mavenDuplicateExceptionRegex: '',
+ genericDuplicatesAllowed: true,
+ genericDuplicateExceptionRegex: '',
+});
+
export const groupPackageSettingsMock = {
data: {
group: {
- packageSettings: {
- mavenDuplicatesAllowed: true,
- mavenDuplicateExceptionRegex: '',
- genericDuplicatesAllowed: true,
- genericDuplicateExceptionRegex: '',
- },
+ fullPath: 'foo_group_path',
+ packageSettings: packageSettings(),
},
},
};
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index 60ff15a88e0..e703bbc4927 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Banzai::CrossProjectReference do
let(:including_class) { Class.new.include(described_class).new }
- let(:reference_cache) { Banzai::Filter::References::ReferenceCache.new(including_class, {})}
+ let(:reference_cache) { Banzai::Filter::References::ReferenceCache.new(including_class, {}, {})}
before do
allow(including_class).to receive(:context).and_return({})
diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb
index c9404c381d3..dcd153da16a 100644
--- a/spec/lib/banzai/filter/references/reference_cache_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb
@@ -12,15 +12,48 @@ RSpec.describe Banzai::Filter::References::ReferenceCache do
let(:filter_class) { Banzai::Filter::References::IssueReferenceFilter }
let(:filter) { filter_class.new(doc, project: project) }
- let(:cache) { described_class.new(filter, { project: project }) }
+ let(:cache) { described_class.new(filter, { project: project }, result) }
+ let(:result) { {} }
describe '#load_references_per_parent' do
+ subject { cache.load_references_per_parent(filter.nodes) }
+
it 'loads references grouped per parent paths' do
- cache.load_references_per_parent(filter.nodes)
+ expect(doc).to receive(:to_html).and_call_original
+
+ subject
expect(cache.references_per_parent).to eq({ project.full_path => [issue1.iid, issue2.iid].to_set,
project2.full_path => [issue3.iid].to_set })
end
+
+ context 'when rendered_html is memoized' do
+ let(:result) { { rendered_html: 'html' } }
+
+ it 'reuses memoized rendered HTML when available' do
+ expect(doc).not_to receive(:to_html)
+
+ subject
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(reference_cache_memoization: false)
+ end
+
+ it 'ignores memoized rendered HTML' do
+ expect(doc).to receive(:to_html).and_call_original
+
+ subject
+ end
+ end
+ end
+
+ context 'when result is not available' do
+ let(:result) { nil }
+
+ it { expect { subject }.not_to raise_error }
+ end
end
describe '#load_parent_per_reference' do
@@ -47,7 +80,7 @@ RSpec.describe Banzai::Filter::References::ReferenceCache do
it 'does not have an N+1 query problem with cross projects' do
doc_single = Nokogiri::HTML.fragment("#1")
filter_single = filter_class.new(doc_single, project: project)
- cache_single = described_class.new(filter_single, { project: project })
+ cache_single = described_class.new(filter_single, { project: project }, {})
control_count = ActiveRecord::QueryRecorder.new do
cache_single.load_references_per_parent(filter_single.nodes)
diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
index e83e1326206..fc5999d59ac 100644
--- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb
+++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
@@ -24,6 +24,7 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do
"default for scheduler failure" | 1 | {} | :scheduler_failure | true
"quota is exceeded" | 0 | { max: 2 } | :ci_quota_exceeded | false
"no matching runner" | 0 | { max: 2 } | :no_matching_runner | false
+ "missing dependencies" | 0 | { max: 2 } | :missing_dependency_failure | false
end
with_them do
diff --git a/spec/lib/gitlab/ci/config/entry/retry_spec.rb b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
index b38387a437e..84ef5344a8b 100644
--- a/spec/lib/gitlab/ci/config/entry/retry_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
@@ -101,7 +101,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Retry do
api_failure
stuck_or_timeout_failure
runner_system_failure
- missing_dependency_failure
runner_unsupported
stale_schedule
job_execution_timeout
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 14c7f7f227b..a19acbd196d 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1221,32 +1221,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
%w(test success),
%w(deploy running)])
end
-
- context 'when commit status is retried' do
- let!(:old_commit_status) do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'success')
- end
-
- context 'when FF ci_remove_update_retried_from_process_pipeline is disabled' do
- before do
- stub_feature_flags(ci_remove_update_retried_from_process_pipeline: false)
-
- Ci::ProcessPipelineService
- .new(pipeline)
- .execute
- end
-
- it 'ignores the previous state' do
- expect(statuses).to eq([%w(build success),
- %w(test success),
- %w(deploy running)])
- end
- end
- end
end
context 'when there is a stage with warnings' do
diff --git a/spec/models/concerns/vulnerability_finding_helpers_spec.rb b/spec/models/concerns/vulnerability_finding_helpers_spec.rb
new file mode 100644
index 00000000000..023ecccb520
--- /dev/null
+++ b/spec/models/concerns/vulnerability_finding_helpers_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe VulnerabilityFindingHelpers do
+ let(:cls) do
+ Class.new do
+ include VulnerabilityFindingHelpers
+
+ attr_accessor :report_type
+
+ def initialize(report_type)
+ @report_type = report_type
+ end
+ end
+ end
+
+ describe '#requires_manual_resolution?' do
+ it 'returns false if the finding does not require manual resolution' do
+ expect(cls.new('sast').requires_manual_resolution?).to eq(false)
+ end
+
+ it 'returns true when the finding requires manual resolution' do
+ expect(cls.new('secret_detection').requires_manual_resolution?).to eq(true)
+ end
+ end
+end
diff --git a/spec/requests/import/url_controller_spec.rb b/spec/requests/import/url_controller_spec.rb
new file mode 100644
index 00000000000..63af5e8b469
--- /dev/null
+++ b/spec/requests/import/url_controller_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::UrlController do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'POST #validate' do
+ it 'reports success when service reports success status' do
+ allow_next_instance_of(Import::ValidateRemoteGitEndpointService) do |validate_endpoint_service|
+ allow(validate_endpoint_service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ post import_url_validate_path, params: { url: 'https://fake.repo' }
+
+ expect(json_response).to eq({ 'success' => true })
+ end
+
+ it 'exposes error message when service reports error' do
+ expect_next_instance_of(Import::ValidateRemoteGitEndpointService) do |validate_endpoint_service|
+ expect(validate_endpoint_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foobar'))
+ end
+
+ post import_url_validate_path, params: { url: 'https://fake.repo' }
+
+ expect(json_response).to eq({ 'success' => false, 'message' => 'foobar' })
+ end
+
+ context 'with an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to sign-in page' do
+ post import_url_validate_path
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index b5bf0adadaf..404e1bf7c87 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -10,11 +10,9 @@ RSpec.describe Ci::ProcessPipelineService do
end
let(:pipeline_processing_events_counter) { double(increment: true) }
- let(:legacy_update_jobs_counter) { double(increment: true) }
let(:metrics) do
- double(pipeline_processing_events_counter: pipeline_processing_events_counter,
- legacy_update_jobs_counter: legacy_update_jobs_counter)
+ double(pipeline_processing_events_counter: pipeline_processing_events_counter)
end
subject { described_class.new(pipeline) }
@@ -33,68 +31,4 @@ RSpec.describe Ci::ProcessPipelineService do
subject.execute
end
end
-
- describe 'updating a list of retried builds' do
- let!(:build_retried) { create_build('build') }
- let!(:build) { create_build('build') }
- let!(:test) { create_build('test') }
-
- context 'when FF ci_remove_update_retried_from_process_pipeline is enabled' do
- it 'does not update older builds as retried' do
- subject.execute
-
- expect(all_builds.latest).to contain_exactly(build, build_retried, test)
- expect(all_builds.retried).to be_empty
- end
- end
-
- context 'when FF ci_remove_update_retried_from_process_pipeline is disabled' do
- before do
- stub_feature_flags(ci_remove_update_retried_from_process_pipeline: false)
- end
-
- it 'returns unique statuses' do
- subject.execute
-
- expect(all_builds.latest).to contain_exactly(build, test)
- expect(all_builds.retried).to contain_exactly(build_retried)
- end
-
- it 'increments the counter' do
- expect(legacy_update_jobs_counter).to receive(:increment)
-
- subject.execute
- end
-
- it 'logs the project and pipeline id' do
- expect(Gitlab::AppJsonLogger).to receive(:info).with(event: 'update_retried_is_used',
- project_id: project.id,
- pipeline_id: pipeline.id)
-
- subject.execute
- end
-
- context 'when the previous build has already retried column true' do
- before do
- build_retried.update_columns(retried: true)
- end
-
- it 'does not increment the counter' do
- expect(legacy_update_jobs_counter).not_to receive(:increment)
-
- subject.execute
- end
- end
- end
-
- private
-
- def create_build(name, **opts)
- create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
- end
-
- def all_builds
- pipeline.builds.order(:stage_idx, :id)
- end
- end
end
diff --git a/spec/services/ci/stuck_builds/drop_running_service_spec.rb b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
index d2132914a02..439eb3b7724 100644
--- a/spec/services/ci/stuck_builds/drop_running_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
@@ -17,20 +17,47 @@ RSpec.describe Ci::StuckBuilds::DropRunningService do
job.update!(job_attributes)
end
- context 'when job is running' do
- let(:status) { 'running' }
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ shared_examples 'running builds' do
+ context 'when job is running' do
+ let(:status) { 'running' }
+ let(:outdated_time) { described_class::BUILD_RUNNING_OUTDATED_TIMEOUT.ago - 30.minutes }
+ let(:fresh_time) { described_class::BUILD_RUNNING_OUTDATED_TIMEOUT.ago + 30.minutes }
+
+ context 'when job is outdated' do
+ let(:created_at) { outdated_time }
+ let(:updated_at) { outdated_time }
+
+ it_behaves_like 'job is dropped'
+ end
- context 'when job was updated_at more than an hour ago' do
- let(:updated_at) { 2.hours.ago }
+ context 'when job is fresh' do
+ let(:created_at) { fresh_time }
+ let(:updated_at) { fresh_time }
- it_behaves_like 'job is dropped'
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when job freshly updated' do
+ let(:created_at) { outdated_time }
+ let(:updated_at) { fresh_time }
+
+ it_behaves_like 'job is unchanged'
+ end
end
+ end
- context 'when job was updated in less than 1 hour ago' do
- let(:updated_at) { 30.minutes.ago }
+ include_examples 'running builds'
- it_behaves_like 'job is unchanged'
+ context 'when ci_new_query_for_running_stuck_jobs flag is disabled' do
+ before do
+ stub_feature_flags(ci_new_query_for_running_stuck_jobs: false)
end
+
+ include_examples 'running builds'
end
%w(success skipped failed canceled scheduled pending).each do |status|
@@ -51,15 +78,4 @@ RSpec.describe Ci::StuckBuilds::DropRunningService do
end
end
end
-
- context 'for deleted project' do
- let(:status) { 'running' }
- let(:updated_at) { 2.days.ago }
-
- before do
- job.project.update!(pending_delete: true)
- end
-
- it_behaves_like 'job is dropped'
- end
end
diff --git a/spec/services/import/validate_remote_git_endpoint_service_spec.rb b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
new file mode 100644
index 00000000000..97c8a9f5dd4
--- /dev/null
+++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::ValidateRemoteGitEndpointService do
+ include StubRequests
+
+ let_it_be(:base_url) { 'http://demo.host/path' }
+ let_it_be(:endpoint_url) { "#{base_url}/info/refs?service=git-upload-pack" }
+ let_it_be(:error_message) { "#{base_url} is not a valid HTTP Git repository" }
+
+ describe '#execute' do
+ let(:valid_response) do
+ { status: 200,
+ body: '001e# service=git-upload-pack',
+ headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } }
+ end
+
+ it 'correctly handles URLs with fragment' do
+ allow(Gitlab::HTTP).to receive(:get)
+
+ described_class.new(url: "#{base_url}#somehash").execute
+
+ expect(Gitlab::HTTP).to have_received(:get).with(endpoint_url, basic_auth: nil, stream_body: true, follow_redirects: false)
+ end
+
+ context 'when receiving HTTP response' do
+ subject { described_class.new(url: base_url) }
+
+ it 'returns success when HTTP response is valid and contains correct payload' do
+ stub_full_request(endpoint_url, method: :get).to_return(valid_response)
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ end
+
+ it 'reports error when status code is not 200' do
+ stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ status: 301 }))
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(error_message)
+ end
+
+ it 'reports error when required header is missing' do
+ stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ headers: nil }))
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(error_message)
+ end
+
+ it 'reports error when body is in invalid format' do
+ stub_full_request(endpoint_url, method: :get).to_return(valid_response.merge({ body: 'invalid content' }))
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(error_message)
+ end
+
+ it 'reports error when exception is raised' do
+ stub_full_request(endpoint_url, method: :get).to_raise(SocketError.new('dummy message'))
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(error_message)
+ end
+ end
+
+ it 'passes basic auth when credentials are provided' do
+ allow(Gitlab::HTTP).to receive(:get)
+
+ described_class.new(url: "#{base_url}#somehash", user: 'user', password: 'password').execute
+
+ expect(Gitlab::HTTP).to have_received(:get).with(endpoint_url, basic_auth: { username: 'user', password: 'password' }, stream_body: true, follow_redirects: false)
+ end
+ end
+end
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 10068b9c508..b6cf78b9046 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -101,6 +101,27 @@ module FilteredSearchHelpers
end
end
+ # Same as `expect_tokens` but works with GlFilteredSearch
+ def expect_vue_tokens(tokens)
+ page.within '.gl-search-box-by-click .gl-filtered-search-scrollable' do
+ token_elements = page.all(:css, '.gl-filtered-search-token')
+
+ tokens.each_with_index do |token, index|
+ el = token_elements[index]
+
+ expect(el.find('.gl-filtered-search-token-type')).to have_content(token[:name])
+ expect(el.find('.gl-filtered-search-token-operator')).to have_content(token[:operator]) if token[:operator].present?
+ expect(el.find('.gl-filtered-search-token-data')).to have_content(token[:value]) if token[:value].present?
+
+ # gl-emoji content is blank when the emoji unicode is not supported
+ if token[:emoji_name].present?
+ selector = %(gl-emoji[data-name="#{token[:emoji_name]}"])
+ expect(el.find('.gl-filtered-search-token-data-content')).to have_css(selector)
+ end
+ end
+ end
+ end
+
def create_token(token_name, token_value = nil, symbol = nil, token_operator = '=')
{ name: token_name, operator: token_operator, value: "#{symbol}#{token_value}" }
end