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
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 21:18:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 21:18:33 +0300
commitf64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch)
treea2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /spec/frontend/import_entities
parentbfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff)
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'spec/frontend/import_entities')
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_row_spec.js109
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js50
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js90
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js55
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js21
5 files changed, 286 insertions, 39 deletions
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
index cdef4b1ee62..7a83136e785 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
@@ -1,10 +1,15 @@
-import { GlButton, GlLink, GlFormInput } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { STATUSES } from '~/import_entities/constants';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
-import Select2Select from '~/vue_shared/components/select2_select.vue';
+import groupQuery from '~/import_entities/import_groups/graphql/queries/group.query.graphql';
import { availableNamespacesFixture } from '../graphql/fixtures';
+Vue.use(VueApollo);
+
const getFakeGroup = (status) => ({
web_url: 'https://fake.host/',
full_path: 'fake_group_1',
@@ -17,8 +22,12 @@ const getFakeGroup = (status) => ({
status,
});
+const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group';
+const EXISTING_GROUP_PATH = 'existing-path';
+
describe('import table row', () => {
let wrapper;
+ let apolloProvider;
let group;
const findByText = (cmp, text) => {
@@ -26,12 +35,27 @@ describe('import table row', () => {
};
const findImportButton = () => findByText(GlButton, 'Import');
const findNameInput = () => wrapper.find(GlFormInput);
- const findNamespaceDropdown = () => wrapper.find(Select2Select);
+ const findNamespaceDropdown = () => wrapper.find(GlDropdown);
const createComponent = (props) => {
+ apolloProvider = createMockApollo([
+ [
+ groupQuery,
+ ({ fullPath }) => {
+ const existingGroup =
+ fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_GROUP_PATH}`
+ ? { id: 1 }
+ : null;
+ return Promise.resolve({ data: { existingGroup } });
+ },
+ ],
+ ]);
+
wrapper = shallowMount(ImportTableRow, {
+ apolloProvider,
propsData: {
availableNamespaces: availableNamespacesFixture,
+ groupPathRegex: /.*/,
...props,
},
});
@@ -49,15 +73,24 @@ describe('import table row', () => {
});
it.each`
- selector | sourceEvent | payload | event
- ${findNamespaceDropdown} | ${'input'} | ${'demo'} | ${'update-target-namespace'}
- ${findNameInput} | ${'input'} | ${'demo'} | ${'update-new-name'}
- ${findImportButton} | ${'click'} | ${undefined} | ${'import-group'}
+ selector | sourceEvent | payload | event
+ ${findNameInput} | ${'input'} | ${'demo'} | ${'update-new-name'}
+ ${findImportButton} | ${'click'} | ${undefined} | ${'import-group'}
`('invokes $event', ({ selector, sourceEvent, payload, event }) => {
selector().vm.$emit(sourceEvent, payload);
expect(wrapper.emitted(event)).toBeDefined();
expect(wrapper.emitted(event)[0][0]).toBe(payload);
});
+
+ it('emits update-target-namespace when dropdown option is clicked', () => {
+ const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2);
+ const dropdownItemText = dropdownItem.text();
+
+ dropdownItem.vm.$emit('click');
+
+ expect(wrapper.emitted('update-target-namespace')).toBeDefined();
+ expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(dropdownItemText);
+ });
});
describe('when entity status is NONE', () => {
@@ -75,6 +108,34 @@ describe('import table row', () => {
});
});
+ it('renders only no parent option if available namespaces list is empty', () => {
+ createComponent({
+ group: getFakeGroup(STATUSES.NONE),
+ availableNamespaces: [],
+ });
+
+ const items = findNamespaceDropdown()
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((w) => w.text());
+
+ expect(items[0]).toBe('No parent');
+ expect(items).toHaveLength(1);
+ });
+
+ it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => {
+ createComponent({
+ group: getFakeGroup(STATUSES.NONE),
+ availableNamespaces: availableNamespacesFixture,
+ });
+
+ const [firstItem, ...rest] = findNamespaceDropdown()
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((w) => w.text());
+
+ expect(firstItem).toBe('No parent');
+ expect(rest).toHaveLength(availableNamespacesFixture.length);
+ });
+
describe('when entity status is SCHEDULING', () => {
beforeEach(() => {
group = getFakeGroup(STATUSES.SCHEDULING);
@@ -109,4 +170,38 @@ describe('import table row', () => {
expect(findByText(GlLink, TARGET_LINK).exists()).toBe(true);
});
});
+
+ describe('validations', () => {
+ it('Reports invalid group name when name is not matching regex', () => {
+ createComponent({
+ group: {
+ ...getFakeGroup(STATUSES.NONE),
+ import_target: {
+ target_namespace: 'root',
+ new_name: 'very`bad`name',
+ },
+ },
+ groupPathRegex: /^[a-zA-Z]+$/,
+ });
+
+ expect(wrapper.text()).toContain('Please choose a group URL with no special characters.');
+ });
+
+ it('Reports invalid group name if group already exists', async () => {
+ createComponent({
+ group: {
+ ...getFakeGroup(STATUSES.NONE),
+ import_target: {
+ target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
+ new_name: EXISTING_GROUP_PATH,
+ },
+ },
+ });
+
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
+ expect(wrapper.text()).toContain('Name already exists.');
+ });
+ });
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index dd734782169..496c5cda7c7 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -1,7 +1,15 @@
-import { GlEmptyState, GlLoadingIcon, GlSearchBoxByClick, GlSprintf } from '@gitlab/ui';
+import {
+ GlEmptyState,
+ GlLoadingIcon,
+ GlSearchBoxByClick,
+ GlSprintf,
+ GlDropdown,
+ GlDropdownItem,
+} from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { STATUSES } from '~/import_entities/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
@@ -16,13 +24,25 @@ import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtur
const localVue = createLocalVue();
localVue.use(VueApollo);
+const GlDropdownStub = stubComponent(GlDropdown, {
+ template: '<div><h1 ref="text"><slot name="button-content"></slot></h1><slot></slot></div>',
+});
+
describe('import table', () => {
let wrapper;
let apolloProvider;
+ const SOURCE_URL = 'https://demo.host';
const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ const FAKE_GROUPS = [
+ generateFakeEntry({ id: 1, status: STATUSES.NONE }),
+ generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
+ ];
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
+ const findPaginationDropdown = () => wrapper.findComponent(GlDropdown);
+ const findPaginationDropdownText = () => findPaginationDropdown().find({ ref: 'text' }).text();
+
const createComponent = ({ bulkImportSourceGroups }) => {
apolloProvider = createMockApollo([], {
Query: {
@@ -38,10 +58,12 @@ describe('import table', () => {
wrapper = shallowMount(ImportTable, {
propsData: {
- sourceUrl: 'https://demo.host',
+ groupPathRegex: /.*/,
+ sourceUrl: SOURCE_URL,
},
stubs: {
GlSprintf,
+ GlDropdown: GlDropdownStub,
},
localVue,
apolloProvider,
@@ -80,14 +102,10 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.find(GlEmptyState).props().title).toBe('No groups available for import');
+ expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import');
});
it('renders import row for each group in response', async () => {
- const FAKE_GROUPS = [
- generateFakeEntry({ id: 1, status: STATUSES.NONE }),
- generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
- ];
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
@@ -151,6 +169,20 @@ describe('import table', () => {
expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
});
+ it('renders pagination dropdown', () => {
+ expect(findPaginationDropdown().exists()).toBe(true);
+ });
+
+ it('updates page size when selected in Dropdown', async () => {
+ const otherOption = wrapper.findAllComponents(GlDropdownItem).at(1);
+ expect(otherOption.text()).toMatchInterpolatedText('50 items per page');
+
+ otherOption.vm.$emit('click');
+ await waitForPromises();
+
+ expect(findPaginationDropdownText()).toMatchInterpolatedText('50 items per page');
+ });
+
it('updates page when page change is requested', async () => {
const REQUESTED_PAGE = 2;
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
@@ -178,7 +210,7 @@ describe('import table', () => {
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
- expect(wrapper.text()).toContain('Showing 21-21 of 38');
+ expect(wrapper.text()).toContain('Showing 21-21 of 38 groups from');
});
});
@@ -224,7 +256,7 @@ describe('import table', () => {
findFilterInput().vm.$emit('submit', FILTER_VALUE);
await waitForPromises();
- expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo"');
+ expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo" from');
});
it('properly resets filter in graphql query when search box is cleared', async () => {
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index 4d3d2c41bbe..1feff861c1e 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -2,6 +2,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'mock-apollo-client';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import {
clientTypenames,
@@ -18,6 +19,7 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
+jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
StatusPoller: jest.fn().mockImplementation(function mock() {
this.startPolling = jest.fn();
@@ -35,15 +37,19 @@ describe('Bulk import resolvers', () => {
let axiosMockAdapter;
let client;
- beforeEach(() => {
- axiosMockAdapter = new MockAdapter(axios);
- client = createMockClient({
+ const createClient = (extraResolverArgs) => {
+ return createMockClient({
cache: new InMemoryCache({
fragmentMatcher: { match: () => true },
addTypename: false,
}),
- resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS }),
+ resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS, ...extraResolverArgs }),
});
+ };
+
+ beforeEach(() => {
+ axiosMockAdapter = new MockAdapter(axios);
+ client = createClient();
});
afterEach(() => {
@@ -82,6 +88,44 @@ describe('Bulk import resolvers', () => {
.reply(httpStatus.OK, availableNamespacesFixture);
});
+ it('respects cached import state when provided by group manager', async () => {
+ const FAKE_STATUS = 'DEMO_STATUS';
+ const FAKE_IMPORT_TARGET = {};
+ const TARGET_INDEX = 0;
+
+ const clientWithMockedManager = createClient({
+ GroupsManager: jest.fn().mockImplementation(() => ({
+ getImportStateFromStorageByGroupId(groupId) {
+ if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) {
+ return {
+ status: FAKE_STATUS,
+ importTarget: FAKE_IMPORT_TARGET,
+ };
+ }
+
+ return null;
+ },
+ })),
+ });
+
+ const clientResponse = await clientWithMockedManager.query({
+ query: bulkImportSourceGroupsQuery,
+ });
+ const clientResults = clientResponse.data.bulkImportSourceGroups.nodes;
+
+ expect(clientResults[TARGET_INDEX].import_target).toBe(FAKE_IMPORT_TARGET);
+ expect(clientResults[TARGET_INDEX].status).toBe(FAKE_STATUS);
+ });
+
+ it('populates each result instance with empty import_target when there are no available namespaces', async () => {
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.availableNamespaces).reply(httpStatus.OK, []);
+
+ const response = await client.query({ query: bulkImportSourceGroupsQuery });
+ results = response.data.bulkImportSourceGroups.nodes;
+
+ expect(results.every((r) => r.import_target.target_namespace === '')).toBe(true);
+ });
+
describe('when called', () => {
beforeEach(async () => {
const response = await client.query({ query: bulkImportSourceGroupsQuery });
@@ -220,14 +264,14 @@ describe('Bulk import resolvers', () => {
expect(intermediateResults[0].status).toBe(STATUSES.SCHEDULING);
});
- it('sets group status to STARTED when request completes', async () => {
+ it('sets import status to CREATED when request completes', async () => {
axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
await client.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
});
- expect(results[0].status).toBe(STATUSES.STARTED);
+ expect(results[0].status).toBe(STATUSES.CREATED);
});
it('resets status to NONE if request fails', async () => {
@@ -245,6 +289,40 @@ describe('Bulk import resolvers', () => {
expect(results[0].status).toBe(STATUSES.NONE);
});
+
+ it('shows default error message when server error is not provided', async () => {
+ axiosMockAdapter
+ .onPost(FAKE_ENDPOINTS.createBulkImport)
+ .reply(httpStatus.INTERNAL_SERVER_ERROR);
+
+ client
+ .mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId: GROUP_ID },
+ })
+ .catch(() => {});
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Importing the group failed' });
+ });
+
+ it('shows provided error message when error is included in backend response', async () => {
+ const CUSTOM_MESSAGE = 'custom message';
+
+ axiosMockAdapter
+ .onPost(FAKE_ENDPOINTS.createBulkImport)
+ .reply(httpStatus.INTERNAL_SERVER_ERROR, { error: CUSTOM_MESSAGE });
+
+ client
+ .mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId: GROUP_ID },
+ })
+ .catch(() => {});
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
+ });
});
});
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
index ca987ab3ab4..5baa201906a 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
@@ -1,11 +1,17 @@
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql';
-import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
+import {
+ KEY,
+ SourceGroupsManager,
+} from '~/import_entities/import_groups/graphql/services/source_groups_manager';
+
+const FAKE_SOURCE_URL = 'http://demo.host';
describe('SourceGroupsManager', () => {
let manager;
let client;
+ let storage;
const getFakeGroup = () => ({
__typename: clientTypenames.BulkImportSourceGroup,
@@ -17,8 +23,53 @@ describe('SourceGroupsManager', () => {
readFragment: jest.fn(),
writeFragment: jest.fn(),
};
+ storage = {
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ };
+
+ manager = new SourceGroupsManager({ client, storage, sourceUrl: FAKE_SOURCE_URL });
+ });
+
+ describe('storage management', () => {
+ const IMPORT_ID = 1;
+ const IMPORT_TARGET = { destination_name: 'demo', destination_namespace: 'foo' };
+ const STATUS = 'FAKE_STATUS';
+ const FAKE_GROUP = { id: 1, import_target: IMPORT_TARGET, status: STATUS };
+
+ it('loads state from storage on creation', () => {
+ expect(storage.getItem).toHaveBeenCalledWith(KEY);
+ });
+
+ it('saves to storage when import is starting', () => {
+ manager.startImport({
+ importId: IMPORT_ID,
+ group: FAKE_GROUP,
+ });
+ const storedObject = JSON.parse(storage.setItem.mock.calls[0][1]);
+ expect(Object.values(storedObject)[0]).toStrictEqual({
+ id: FAKE_GROUP.id,
+ importTarget: IMPORT_TARGET,
+ status: STATUS,
+ });
+ });
- manager = new SourceGroupsManager({ client });
+ it('saves to storage when import status is updated', () => {
+ const CHANGED_STATUS = 'changed';
+
+ manager.startImport({
+ importId: IMPORT_ID,
+ group: FAKE_GROUP,
+ });
+
+ manager.setImportStatusByImportId(IMPORT_ID, CHANGED_STATUS);
+ const storedObject = JSON.parse(storage.setItem.mock.calls[1][1]);
+ expect(Object.values(storedObject)[0]).toStrictEqual({
+ id: FAKE_GROUP.id,
+ importTarget: IMPORT_TARGET,
+ status: CHANGED_STATUS,
+ });
+ });
});
it('finds item by group id', () => {
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
index a5fc4e18a02..0d4809971ae 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
-import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
@@ -18,24 +17,21 @@ jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manage
}));
const FAKE_POLL_PATH = '/fake/poll/path';
-const CLIENT_MOCK = {};
describe('Bulk import status poller', () => {
let poller;
let mockAdapter;
+ let groupManager;
const getPollHistory = () => mockAdapter.history.get.filter((x) => x.url === FAKE_POLL_PATH);
beforeEach(() => {
mockAdapter = new MockAdapter(axios);
mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {});
- poller = new StatusPoller({ client: CLIENT_MOCK, pollPath: FAKE_POLL_PATH });
- });
-
- it('creates source group manager with proper client', () => {
- expect(SourceGroupsManager.mock.calls).toHaveLength(1);
- const [[{ client }]] = SourceGroupsManager.mock.calls;
- expect(client).toBe(CLIENT_MOCK);
+ groupManager = {
+ setImportStatusByImportId: jest.fn(),
+ };
+ poller = new StatusPoller({ groupManager, pollPath: FAKE_POLL_PATH });
});
it('creates poller with proper config', () => {
@@ -100,14 +96,9 @@ describe('Bulk import status poller', () => {
it('when success response arrives updates relevant group status', () => {
const FAKE_ID = 5;
const [[pollConfig]] = Poll.mock.calls;
- const [managerInstance] = SourceGroupsManager.mock.instances;
- managerInstance.findByImportId.mockReturnValue({ id: FAKE_ID });
pollConfig.successCallback({ data: [{ id: FAKE_ID, status_name: STATUSES.FINISHED }] });
- expect(managerInstance.setImportStatus).toHaveBeenCalledWith(
- expect.objectContaining({ id: FAKE_ID }),
- STATUSES.FINISHED,
- );
+ expect(groupManager.setImportStatusByImportId).toHaveBeenCalledWith(FAKE_ID, STATUSES.FINISHED);
});
});