summaryrefslogtreecommitdiff
path: root/spec/frontend/import_entities
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /spec/frontend/import_entities
parent4b1de649d0168371549608993deac953eb692019 (diff)
downloadgitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'spec/frontend/import_entities')
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_row_spec.js112
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js103
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js221
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js51
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js82
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js213
-rw-r--r--spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js59
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js249
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js169
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js398
-rw-r--r--spec/frontend/import_entities/import_projects/store/getters_spec.js135
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js319
-rw-r--r--spec/frontend/import_entities/import_projects/utils_spec.js69
13 files changed, 2180 insertions, 0 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
new file mode 100644
index 00000000000..d88a31a0e47
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
@@ -0,0 +1,112 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlLink, GlFormInput } from '@gitlab/ui';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
+import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
+import { STATUSES } from '~/import_entities/constants';
+import { availableNamespacesFixture } from '../graphql/fixtures';
+
+const getFakeGroup = status => ({
+ web_url: 'https://fake.host/',
+ full_path: 'fake_group_1',
+ full_name: 'fake_name_1',
+ import_target: {
+ target_namespace: 'root',
+ new_name: 'group1',
+ },
+ id: 1,
+ status,
+});
+
+describe('import table row', () => {
+ let wrapper;
+ let group;
+
+ const findByText = (cmp, text) => {
+ return wrapper.findAll(cmp).wrappers.find(node => node.text().indexOf(text) === 0);
+ };
+ const findImportButton = () => findByText(GlButton, 'Import');
+ const findNameInput = () => wrapper.find(GlFormInput);
+ const findNamespaceDropdown = () => wrapper.find(Select2Select);
+
+ const createComponent = props => {
+ wrapper = shallowMount(ImportTableRow, {
+ propsData: {
+ availableNamespaces: availableNamespacesFixture,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.NONE);
+ createComponent({ group });
+ });
+
+ it.each`
+ selector | sourceEvent | payload | event
+ ${findNamespaceDropdown} | ${'input'} | ${'demo'} | ${'update-target-namespace'}
+ ${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);
+ });
+ });
+
+ describe('when entity status is NONE', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.NONE);
+ createComponent({ group });
+ });
+
+ it('renders Import button', () => {
+ expect(findByText(GlButton, 'Import').exists()).toBe(true);
+ });
+
+ it('renders namespace dropdown as not disabled', () => {
+ expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined);
+ });
+ });
+
+ describe('when entity status is SCHEDULING', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.SCHEDULING);
+ createComponent({ group });
+ });
+
+ it('does not render Import button', () => {
+ expect(findByText(GlButton, 'Import')).toBe(undefined);
+ });
+
+ it('renders namespace dropdown as disabled', () => {
+ expect(findNamespaceDropdown().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when entity status is FINISHED', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.FINISHED);
+ createComponent({ group });
+ });
+
+ it('does not render Import button', () => {
+ expect(findByText(GlButton, 'Import')).toBe(undefined);
+ });
+
+ it('does not render namespace dropdown', () => {
+ expect(findNamespaceDropdown().exists()).toBe(false);
+ });
+
+ it('renders target as link', () => {
+ const TARGET_LINK = `${group.import_target.target_namespace}/${group.import_target.new_name}`;
+ expect(findByText(GlLink, TARGET_LINK).exists()).toBe(true);
+ });
+ });
+});
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
new file mode 100644
index 00000000000..0ca721cd951
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -0,0 +1,103 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
+import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
+import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
+import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
+import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
+import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
+
+import { STATUSES } from '~/import_entities/constants';
+
+import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('import table', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const createComponent = ({ bulkImportSourceGroups }) => {
+ apolloProvider = createMockApollo([], {
+ Query: {
+ availableNamespaces: () => availableNamespacesFixture,
+ bulkImportSourceGroups,
+ },
+ Mutation: {
+ setTargetNamespace: jest.fn(),
+ setNewName: jest.fn(),
+ importGroup: jest.fn(),
+ },
+ });
+
+ wrapper = shallowMount(ImportTable, {
+ localVue,
+ apolloProvider,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders loading icon while performing request', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => new Promise(() => {}),
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('does not renders loading icon when request is completed', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => [],
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+
+ 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: () => FAKE_GROUPS,
+ });
+ await waitForPromises();
+
+ expect(wrapper.findAll(ImportTableRow)).toHaveLength(FAKE_GROUPS.length);
+ });
+
+ describe('converts row events to mutation invocations', () => {
+ const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+
+ beforeEach(() => {
+ createComponent({
+ bulkImportSourceGroups: () => [FAKE_GROUP],
+ });
+ return waitForPromises();
+ });
+
+ it.each`
+ event | payload | mutation | variables
+ ${'update-target-namespace'} | ${'new-namespace'} | ${setTargetNamespaceMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace' }}
+ ${'update-new-name'} | ${'new-name'} | ${setNewNameMutation} | ${{ sourceGroupId: FAKE_GROUP.id, newName: 'new-name' }}
+ ${'import-group'} | ${undefined} | ${importGroupMutation} | ${{ sourceGroupId: FAKE_GROUP.id }}
+ `('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => {
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ wrapper.find(ImportTableRow).vm.$emit(event, payload);
+ await waitForPromises();
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables,
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..cacbe358a62
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -0,0 +1,221 @@
+import MockAdapter from 'axios-mock-adapter';
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import { createMockClient } from 'mock-apollo-client';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import {
+ clientTypenames,
+ createResolvers,
+} from '~/import_entities/import_groups/graphql/client_factory';
+import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
+import { STATUSES } from '~/import_entities/constants';
+
+import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
+import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
+import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
+import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
+import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
+import httpStatus from '~/lib/utils/http_status';
+import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
+
+jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
+ StatusPoller: jest.fn().mockImplementation(function mock() {
+ this.startPolling = jest.fn();
+ }),
+}));
+
+const FAKE_ENDPOINTS = {
+ status: '/fake_status_url',
+ availableNamespaces: '/fake_available_namespaces',
+ createBulkImport: '/fake_create_bulk_import',
+};
+
+describe('Bulk import resolvers', () => {
+ let axiosMockAdapter;
+ let client;
+
+ beforeEach(() => {
+ axiosMockAdapter = new MockAdapter(axios);
+ client = createMockClient({
+ cache: new InMemoryCache({
+ fragmentMatcher: { match: () => true },
+ addTypename: false,
+ }),
+ resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS }),
+ });
+ });
+
+ afterEach(() => {
+ axiosMockAdapter.restore();
+ });
+
+ describe('queries', () => {
+ describe('availableNamespaces', () => {
+ let results;
+
+ beforeEach(async () => {
+ axiosMockAdapter
+ .onGet(FAKE_ENDPOINTS.availableNamespaces)
+ .reply(httpStatus.OK, availableNamespacesFixture);
+
+ const response = await client.query({ query: availableNamespacesQuery });
+ results = response.data.availableNamespaces;
+ });
+
+ it('mirrors REST endpoint response fields', () => {
+ const extractRelevantFields = obj => ({ id: obj.id, full_path: obj.full_path });
+
+ expect(results.map(extractRelevantFields)).toStrictEqual(
+ availableNamespacesFixture.map(extractRelevantFields),
+ );
+ });
+ });
+
+ describe('bulkImportSourceGroups', () => {
+ let results;
+
+ beforeEach(async () => {
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
+ axiosMockAdapter
+ .onGet(FAKE_ENDPOINTS.availableNamespaces)
+ .reply(httpStatus.OK, availableNamespacesFixture);
+
+ const response = await client.query({ query: bulkImportSourceGroupsQuery });
+ results = response.data.bulkImportSourceGroups;
+ });
+
+ it('mirrors REST endpoint response fields', () => {
+ const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
+ expect(
+ results.every((r, idx) =>
+ MIRRORED_FIELDS.every(
+ field => r[field] === statusEndpointFixture.importable_data[idx][field],
+ ),
+ ),
+ ).toBe(true);
+ });
+
+ it('populates each result instance with status field default to none', () => {
+ expect(results.every(r => r.status === STATUSES.NONE)).toBe(true);
+ });
+
+ it('populates each result instance with import_target defaulted to first available namespace', () => {
+ expect(
+ results.every(
+ r => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
+ ),
+ ).toBe(true);
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ let results;
+ const GROUP_ID = 1;
+
+ beforeEach(() => {
+ client.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: {
+ bulkImportSourceGroups: [
+ {
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id: GROUP_ID,
+ status: STATUSES.NONE,
+ web_url: 'https://fake.host/1',
+ full_path: 'fake_group_1',
+ full_name: 'fake_name_1',
+ import_target: {
+ target_namespace: 'root',
+ new_name: 'group1',
+ },
+ },
+ ],
+ },
+ });
+
+ client
+ .watchQuery({
+ query: bulkImportSourceGroupsQuery,
+ fetchPolicy: 'cache-only',
+ })
+ .subscribe(({ data }) => {
+ results = data.bulkImportSourceGroups;
+ });
+ });
+
+ it('setTargetNamespaces updates group target namespace', async () => {
+ const NEW_TARGET_NAMESPACE = 'target';
+ await client.mutate({
+ mutation: setTargetNamespaceMutation,
+ variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE },
+ });
+
+ expect(results[0].import_target.target_namespace).toBe(NEW_TARGET_NAMESPACE);
+ });
+
+ it('setNewName updates group target name', async () => {
+ const NEW_NAME = 'new';
+ await client.mutate({
+ mutation: setNewNameMutation,
+ variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME },
+ });
+
+ expect(results[0].import_target.new_name).toBe(NEW_NAME);
+ });
+
+ describe('importGroup', () => {
+ it('sets status to SCHEDULING when request initiates', async () => {
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(() => new Promise(() => {}));
+
+ client.mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId: GROUP_ID },
+ });
+ await waitForPromises();
+
+ const { bulkImportSourceGroups: intermediateResults } = client.readQuery({
+ query: bulkImportSourceGroupsQuery,
+ });
+
+ expect(intermediateResults[0].status).toBe(STATUSES.SCHEDULING);
+ });
+
+ it('sets group status to STARTED when request completes', async () => {
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
+ await client.mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId: GROUP_ID },
+ });
+
+ expect(results[0].status).toBe(STATUSES.STARTED);
+ });
+
+ it('starts polling when request completes', async () => {
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
+ await client.mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId: GROUP_ID },
+ });
+ const [statusPoller] = StatusPoller.mock.instances;
+ expect(statusPoller.startPolling).toHaveBeenCalled();
+ });
+
+ it('resets status to NONE if request fails', async () => {
+ axiosMockAdapter
+ .onPost(FAKE_ENDPOINTS.createBulkImport)
+ .reply(httpStatus.INTERNAL_SERVER_ERROR);
+
+ client
+ .mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId: GROUP_ID },
+ })
+ .catch(() => {});
+ await waitForPromises();
+
+ expect(results[0].status).toBe(STATUSES.NONE);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
new file mode 100644
index 00000000000..62e9581bd2d
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -0,0 +1,51 @@
+import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
+
+export const generateFakeEntry = ({ id, status, ...rest }) => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ web_url: `https://fake.host/${id}`,
+ full_path: `fake_group_${id}`,
+ full_name: `fake_name_${id}`,
+ import_target: {
+ target_namespace: 'root',
+ new_name: `group${id}`,
+ },
+ id,
+ status,
+ ...rest,
+});
+
+export const statusEndpointFixture = {
+ importable_data: [
+ {
+ id: 2595438,
+ full_name: 'AutoBreakfast',
+ full_path: 'auto-breakfast',
+ web_url: 'https://gitlab.com/groups/auto-breakfast',
+ },
+ {
+ id: 4347861,
+ full_name: 'GitLab Data',
+ full_path: 'gitlab-data',
+ web_url: 'https://gitlab.com/groups/gitlab-data',
+ },
+ {
+ id: 5723700,
+ full_name: 'GitLab Services',
+ full_path: 'gitlab-services',
+ web_url: 'https://gitlab.com/groups/gitlab-services',
+ },
+ {
+ id: 349181,
+ full_name: 'GitLab-examples',
+ full_path: 'gitlab-examples',
+ web_url: 'https://gitlab.com/groups/gitlab-examples',
+ },
+ ],
+};
+
+export const availableNamespacesFixture = [
+ { id: 24, full_path: 'Commit451' },
+ { id: 22, full_path: 'gitlab-org' },
+ { id: 23, full_path: 'gnuwget' },
+ { id: 25, full_path: 'jashkenas' },
+];
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
new file mode 100644
index 00000000000..5940ea544ea
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
@@ -0,0 +1,82 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
+import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql';
+import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
+
+describe('SourceGroupsManager', () => {
+ let manager;
+ let client;
+
+ const getFakeGroup = () => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id: 5,
+ });
+
+ beforeEach(() => {
+ client = {
+ readFragment: jest.fn(),
+ writeFragment: jest.fn(),
+ };
+
+ manager = new SourceGroupsManager({ client });
+ });
+
+ it('finds item by group id', () => {
+ const ID = 5;
+
+ const FAKE_GROUP = getFakeGroup();
+ client.readFragment.mockReturnValue(FAKE_GROUP);
+ const group = manager.findById(ID);
+ expect(group).toBe(FAKE_GROUP);
+ expect(client.readFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ });
+ });
+
+ it('updates group with provided function', () => {
+ const UPDATED_GROUP = {};
+ const fn = jest.fn().mockReturnValue(UPDATED_GROUP);
+ manager.update(getFakeGroup(), fn);
+
+ expect(client.writeFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ data: UPDATED_GROUP,
+ });
+ });
+
+ it('updates group by id with provided function', () => {
+ const UPDATED_GROUP = {};
+ const fn = jest.fn().mockReturnValue(UPDATED_GROUP);
+ client.readFragment.mockReturnValue(getFakeGroup());
+ manager.updateById(getFakeGroup().id, fn);
+
+ expect(client.readFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ });
+
+ expect(client.writeFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ data: UPDATED_GROUP,
+ });
+ });
+
+ it('sets import status when group is provided', () => {
+ client.readFragment.mockReturnValue(getFakeGroup());
+
+ const NEW_STATUS = 'NEW_STATUS';
+ manager.setImportStatus(getFakeGroup(), NEW_STATUS);
+
+ expect(client.writeFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ data: {
+ ...getFakeGroup(),
+ status: NEW_STATUS,
+ },
+ });
+ });
+});
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
new file mode 100644
index 00000000000..8eb1ffb3cd0
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
@@ -0,0 +1,213 @@
+import { createMockClient } from 'mock-apollo-client';
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import createFlash from '~/flash';
+import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
+import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
+import { STATUSES } from '~/import_entities/constants';
+import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
+import { generateFakeEntry } from '../fixtures';
+
+jest.mock('~/flash');
+jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manager', () => ({
+ SourceGroupsManager: jest.fn().mockImplementation(function mock() {
+ this.setImportStatus = jest.fn();
+ }),
+}));
+
+const TEST_POLL_INTERVAL = 1000;
+
+describe('Bulk import status poller', () => {
+ let poller;
+ let clientMock;
+
+ const listQueryCacheCalls = () =>
+ clientMock.readQuery.mock.calls.filter(call => call[0].query === bulkImportSourceGroupsQuery);
+
+ beforeEach(() => {
+ clientMock = createMockClient({
+ cache: new InMemoryCache({
+ fragmentMatcher: { match: () => true },
+ }),
+ });
+
+ jest.spyOn(clientMock, 'readQuery');
+
+ poller = new StatusPoller({
+ client: clientMock,
+ interval: TEST_POLL_INTERVAL,
+ });
+ });
+
+ describe('general behavior', () => {
+ beforeEach(() => {
+ clientMock.cache.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: { bulkImportSourceGroups: [] },
+ });
+ });
+
+ it('does not perform polling when constructed', () => {
+ jest.runOnlyPendingTimers();
+ expect(listQueryCacheCalls()).toHaveLength(0);
+ });
+
+ it('immediately start polling when requested', async () => {
+ await poller.startPolling();
+ expect(listQueryCacheCalls()).toHaveLength(1);
+ });
+
+ it('constantly polls when started', async () => {
+ poller.startPolling();
+ expect(listQueryCacheCalls()).toHaveLength(1);
+
+ jest.advanceTimersByTime(TEST_POLL_INTERVAL);
+ expect(listQueryCacheCalls()).toHaveLength(2);
+
+ jest.advanceTimersByTime(TEST_POLL_INTERVAL);
+ expect(listQueryCacheCalls()).toHaveLength(3);
+ });
+
+ it('does not start polling when requested multiple times', async () => {
+ poller.startPolling();
+ expect(listQueryCacheCalls()).toHaveLength(1);
+
+ poller.startPolling();
+ expect(listQueryCacheCalls()).toHaveLength(1);
+ });
+
+ it('stops polling when requested', async () => {
+ poller.startPolling();
+ expect(listQueryCacheCalls()).toHaveLength(1);
+
+ poller.stopPolling();
+ jest.runOnlyPendingTimers();
+ expect(listQueryCacheCalls()).toHaveLength(1);
+ });
+
+ it('does not query server when list is empty', async () => {
+ jest.spyOn(clientMock, 'query');
+ poller.startPolling();
+ expect(clientMock.query).not.toHaveBeenCalled();
+ });
+ });
+
+ it('does not query server when no groups have STARTED status', async () => {
+ clientMock.cache.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: {
+ bulkImportSourceGroups: [STATUSES.NONE, STATUSES.FINISHED].map((status, idx) =>
+ generateFakeEntry({ status, id: idx }),
+ ),
+ },
+ });
+
+ jest.spyOn(clientMock, 'query');
+ poller.startPolling();
+ expect(clientMock.query).not.toHaveBeenCalled();
+ });
+
+ describe('when there are groups which have STARTED status', () => {
+ const TARGET_NAMESPACE = 'root';
+
+ const STARTED_GROUP_1 = {
+ status: STATUSES.STARTED,
+ id: 'started1',
+ import_target: {
+ target_namespace: TARGET_NAMESPACE,
+ new_name: 'group1',
+ },
+ };
+
+ const STARTED_GROUP_2 = {
+ status: STATUSES.STARTED,
+ id: 'started2',
+ import_target: {
+ target_namespace: TARGET_NAMESPACE,
+ new_name: 'group2',
+ },
+ };
+
+ const NOT_STARTED_GROUP = {
+ status: STATUSES.NONE,
+ id: 'not_started',
+ import_target: {
+ target_namespace: TARGET_NAMESPACE,
+ new_name: 'group3',
+ },
+ };
+
+ it('query server only for groups with STATUSES.STARTED', async () => {
+ clientMock.cache.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: {
+ bulkImportSourceGroups: [STARTED_GROUP_1, NOT_STARTED_GROUP, STARTED_GROUP_2].map(group =>
+ generateFakeEntry(group),
+ ),
+ },
+ });
+
+ clientMock.query = jest.fn().mockResolvedValue({ data: {} });
+ poller.startPolling();
+
+ expect(clientMock.query).toHaveBeenCalledTimes(1);
+ await waitForPromises();
+ const [[doc]] = clientMock.query.mock.calls;
+ const { selections } = doc.query.definitions[0].selectionSet;
+ expect(selections.every(field => field.name.value === 'group')).toBeTruthy();
+ expect(selections).toHaveLength(2);
+ expect(selections.map(sel => sel.arguments[0].value.value)).toStrictEqual([
+ `${TARGET_NAMESPACE}/${STARTED_GROUP_1.import_target.new_name}`,
+ `${TARGET_NAMESPACE}/${STARTED_GROUP_2.import_target.new_name}`,
+ ]);
+ });
+
+ it('updates statuses only for groups in response', async () => {
+ clientMock.cache.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: {
+ bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map(group =>
+ generateFakeEntry(group),
+ ),
+ },
+ });
+
+ clientMock.query = jest.fn().mockResolvedValue({ data: { group0: {} } });
+ poller.startPolling();
+ await waitForPromises();
+ const [managerInstance] = SourceGroupsManager.mock.instances;
+ expect(managerInstance.setImportStatus).toHaveBeenCalledTimes(1);
+ expect(managerInstance.setImportStatus).toHaveBeenCalledWith(
+ expect.objectContaining({ id: STARTED_GROUP_1.id }),
+ STATUSES.FINISHED,
+ );
+ });
+
+ describe('when error occurs', () => {
+ beforeEach(() => {
+ clientMock.cache.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: {
+ bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map(group =>
+ generateFakeEntry(group),
+ ),
+ },
+ });
+
+ clientMock.query = jest.fn().mockRejectedValue(new Error('dummy error'));
+ poller.startPolling();
+ return waitForPromises();
+ });
+
+ it('reports an error', () => {
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ it('continues polling', async () => {
+ jest.advanceTimersByTime(TEST_POLL_INTERVAL);
+ expect(listQueryCacheCalls()).toHaveLength(2);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
new file mode 100644
index 00000000000..8f8c01a8b81
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
@@ -0,0 +1,59 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+
+import { GlAlert } from '@gitlab/ui';
+import BitbucketStatusTable from '~/import_entities/import_projects/components/bitbucket_status_table.vue';
+import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
+
+const ImportProjectsTableStub = {
+ name: 'ImportProjectsTable',
+ template:
+ '<div><slot name="incompatible-repos-warning"></slot><slot name="actions"></slot></div>',
+};
+
+describe('BitbucketStatusTable', () => {
+ let wrapper;
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ function createComponent(propsData, importProjectsTableStub = true, slots) {
+ wrapper = shallowMount(BitbucketStatusTable, {
+ propsData,
+ stubs: {
+ ImportProjectsTable: importProjectsTableStub,
+ },
+ slots,
+ });
+ }
+
+ it('renders import table component', () => {
+ createComponent({ providerTitle: 'Test' });
+ expect(wrapper.find(ImportProjectsTable).exists()).toBe(true);
+ });
+
+ it('passes alert in incompatible-repos-warning slot', () => {
+ createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
+ });
+
+ it('passes actions slot to import project table component', () => {
+ const actionsSlotContent = 'DEMO';
+ createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
+ actions: actionsSlotContent,
+ });
+ expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent);
+ });
+
+ it('dismisses alert when requested', async () => {
+ createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
+ wrapper.find(GlAlert).vm.$emit('dismiss');
+ await nextTick();
+
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
new file mode 100644
index 00000000000..b4ac11b4404
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -0,0 +1,249 @@
+import { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
+import state from '~/import_entities/import_projects/store/state';
+import * as getters from '~/import_entities/import_projects/store/getters';
+import { STATUSES } from '~/import_entities/constants';
+import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
+import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
+
+describe('ImportProjectsTable', () => {
+ let wrapper;
+
+ const findFilterField = () =>
+ wrapper.find('input[data-qa-selector="githubish_import_filter_field"]');
+
+ const providerTitle = 'THE PROVIDER';
+ const providerRepo = {
+ importSource: {
+ id: 10,
+ sanitizedName: 'sanitizedName',
+ fullName: 'fullName',
+ },
+ importedProject: null,
+ };
+
+ const findImportAllButton = () =>
+ wrapper
+ .findAll(GlButton)
+ .filter(w => w.props().variant === 'success')
+ .at(0);
+ const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
+
+ const importAllFn = jest.fn();
+ const importAllModalShowFn = jest.fn();
+ const fetchReposFn = jest.fn();
+
+ function createComponent({
+ state: initialState,
+ getters: customGetters,
+ slots,
+ filterable,
+ paginatable,
+ } = {}) {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = new Vuex.Store({
+ state: { ...state(), ...initialState },
+ getters: {
+ ...getters,
+ ...customGetters,
+ },
+ actions: {
+ fetchRepos: fetchReposFn,
+ fetchJobs: jest.fn(),
+ fetchNamespaces: jest.fn(),
+ importAll: importAllFn,
+ stopJobsPolling: jest.fn(),
+ clearJobsEtagPoll: jest.fn(),
+ setFilter: jest.fn(),
+ },
+ });
+
+ wrapper = shallowMount(ImportProjectsTable, {
+ localVue,
+ store,
+ propsData: {
+ providerTitle,
+ filterable,
+ paginatable,
+ },
+ slots,
+ stubs: {
+ GlModal: { template: '<div>Modal!</div>', methods: { show: importAllModalShowFn } },
+ },
+ });
+ }
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ it('renders a loading icon while repos are loading', () => {
+ createComponent({ state: { isLoadingRepos: true } });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders a loading icon while namespaces are loading', () => {
+ createComponent({ state: { isLoadingNamespaces: true } });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders a table with provider repos', () => {
+ const repositories = [
+ { importSource: { id: 1 }, importedProject: null },
+ { importSource: { id: 2 }, importedProject: { importStatus: STATUSES.FINISHED } },
+ { importSource: { id: 3, incompatible: true }, importedProject: {} },
+ ];
+
+ createComponent({
+ state: { namespaces: [{ fullPath: 'path' }], repositories },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find('table').exists()).toBe(true);
+ expect(
+ wrapper
+ .findAll('th')
+ .filter(w => w.text() === `From ${providerTitle}`)
+ .exists(),
+ ).toBe(true);
+
+ expect(wrapper.findAll(ProviderRepoTableRow)).toHaveLength(repositories.length);
+ });
+
+ it.each`
+ hasIncompatibleRepos | count | buttonText
+ ${false} | ${1} | ${'Import 1 repository'}
+ ${true} | ${1} | ${'Import 1 compatible repository'}
+ ${false} | ${5} | ${'Import 5 repositories'}
+ ${true} | ${5} | ${'Import 5 compatible repositories'}
+ `(
+ 'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos and repos count is $count',
+ ({ hasIncompatibleRepos, buttonText, count }) => {
+ createComponent({
+ state: {
+ providerRepos: [providerRepo],
+ },
+ getters: {
+ hasIncompatibleRepos: () => hasIncompatibleRepos,
+ importAllCount: () => count,
+ },
+ });
+
+ expect(findImportAllButton().text()).toBe(buttonText);
+ },
+ );
+
+ it('renders an empty state if there are no repositories available', () => {
+ createComponent({ state: { repositories: [] } });
+
+ expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
+ expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
+ });
+
+ it('opens confirmation modal when import all button is clicked', async () => {
+ createComponent({ state: { repositories: [providerRepo] } });
+
+ findImportAllButton().vm.$emit('click');
+ await nextTick();
+
+ expect(importAllModalShowFn).toHaveBeenCalled();
+ });
+
+ it('triggers importAll action when modal is confirmed', async () => {
+ createComponent({ state: { providerRepos: [providerRepo] } });
+
+ findImportAllModal().vm.$emit('ok');
+ await nextTick();
+
+ expect(importAllFn).toHaveBeenCalled();
+ });
+
+ it('shows loading spinner when import is in progress', () => {
+ createComponent({ getters: { isImportingAnyRepo: () => true } });
+
+ expect(findImportAllButton().props().loading).toBe(true);
+ });
+
+ it('renders filtering input field by default', () => {
+ createComponent();
+
+ expect(findFilterField().exists()).toBe(true);
+ });
+
+ it('does not render filtering input field when filterable is false', () => {
+ createComponent({ filterable: false });
+
+ expect(findFilterField().exists()).toBe(false);
+ });
+
+ describe('when paginatable is set to true', () => {
+ const pageInfo = { page: 1 };
+
+ beforeEach(() => {
+ createComponent({
+ state: {
+ namespaces: [{ fullPath: 'path' }],
+ pageInfo,
+ repositories: [
+ { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
+ ],
+ },
+ paginatable: true,
+ });
+ });
+
+ it('does not call fetchRepos on mount', () => {
+ expect(fetchReposFn).not.toHaveBeenCalled();
+ });
+
+ it('renders intersection observer component', () => {
+ expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
+ });
+
+ it('calls fetchRepos when intersection observer appears', async () => {
+ wrapper.find(GlIntersectionObserver).vm.$emit('appear');
+
+ await nextTick();
+
+ expect(fetchReposFn).toHaveBeenCalled();
+ });
+ });
+
+ it('calls fetchRepos on mount', () => {
+ createComponent();
+
+ expect(fetchReposFn).toHaveBeenCalled();
+ });
+
+ it.each`
+ hasIncompatibleRepos | shouldRenderSlot | action
+ ${false} | ${false} | ${'does not render'}
+ ${true} | ${true} | ${'render'}
+ `(
+ '$action incompatible-repos-warning slot if hasIncompatibleRepos is $hasIncompatibleRepos',
+ ({ hasIncompatibleRepos, shouldRenderSlot }) => {
+ const INCOMPATIBLE_TEXT = 'INCOMPATIBLE!';
+
+ createComponent({
+ getters: {
+ hasIncompatibleRepos: () => hasIncompatibleRepos,
+ },
+
+ slots: {
+ 'incompatible-repos-warning': INCOMPATIBLE_TEXT,
+ },
+ });
+
+ expect(wrapper.text().includes(INCOMPATIBLE_TEXT)).toBe(shouldRenderSlot);
+ },
+ );
+});
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
new file mode 100644
index 00000000000..aa003226050
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -0,0 +1,169 @@
+import { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlBadge } from '@gitlab/ui';
+import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
+import ImportStatus from '~/import_entities/components/import_status.vue';
+import { STATUSES } from '~/import_entities//constants';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
+
+describe('ProviderRepoTableRow', () => {
+ let wrapper;
+ const fetchImport = jest.fn();
+ const setImportTarget = jest.fn();
+ const fakeImportTarget = {
+ targetNamespace: 'target',
+ newName: 'newName',
+ };
+
+ const availableNamespaces = [
+ { text: 'Groups', children: [{ id: 'test', text: 'test' }] },
+ { text: 'Users', children: [{ id: 'root', text: 'root' }] },
+ ];
+
+ function initStore(initialState) {
+ const store = new Vuex.Store({
+ state: initialState,
+ getters: {
+ getImportTarget: () => () => fakeImportTarget,
+ },
+ actions: { fetchImport, setImportTarget },
+ });
+
+ return store;
+ }
+
+ const findImportButton = () => {
+ const buttons = wrapper.findAll('button').filter(node => node.text() === 'Import');
+
+ return buttons.length ? buttons.at(0) : buttons;
+ };
+
+ function mountComponent(props) {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+
+ wrapper = shallowMount(ProviderRepoTableRow, {
+ localVue,
+ store,
+ propsData: { availableNamespaces, ...props },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when rendering importable project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
+
+ it('renders empty import status', () => {
+ expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
+ });
+
+ it('renders a select2 namespace select', () => {
+ expect(wrapper.find(Select2Select).exists()).toBe(true);
+ expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
+ });
+
+ it('renders import button', () => {
+ expect(findImportButton().exists()).toBe(true);
+ });
+
+ it('imports repo when clicking import button', async () => {
+ findImportButton().trigger('click');
+
+ await nextTick();
+
+ const { calls } = fetchImport.mock;
+
+ expect(calls).toHaveLength(1);
+ expect(calls[0][1]).toBe(repo.importSource.id);
+ });
+ });
+
+ describe('when rendering imported project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ importStatus: STATUSES.FINISHED,
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
+
+ it('renders proper import status', () => {
+ expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
+ });
+
+ it('does not renders a namespace select', () => {
+ expect(wrapper.find(Select2Select).exists()).toBe(false);
+ });
+
+ it('does not render import button', () => {
+ expect(findImportButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when rendering incompatible project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ incompatible: true,
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
+
+ it('renders badge with error', () => {
+ expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
new file mode 100644
index 00000000000..5d4e73a17a3
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -0,0 +1,398 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ REQUEST_REPOS,
+ RECEIVE_REPOS_SUCCESS,
+ RECEIVE_REPOS_ERROR,
+ REQUEST_IMPORT,
+ RECEIVE_IMPORT_SUCCESS,
+ RECEIVE_IMPORT_ERROR,
+ RECEIVE_JOBS_SUCCESS,
+ REQUEST_NAMESPACES,
+ RECEIVE_NAMESPACES_SUCCESS,
+ RECEIVE_NAMESPACES_ERROR,
+ SET_PAGE,
+ SET_FILTER,
+} from '~/import_entities/import_projects/store/mutation_types';
+import actionsFactory from '~/import_entities/import_projects/store/actions';
+import { getImportTarget } from '~/import_entities/import_projects/store/getters';
+import state from '~/import_entities/import_projects/store/state';
+import { STATUSES } from '~/import_entities/constants';
+
+jest.mock('~/flash');
+
+const MOCK_ENDPOINT = `${TEST_HOST}/endpoint.json`;
+const endpoints = {
+ reposPath: MOCK_ENDPOINT,
+ importPath: MOCK_ENDPOINT,
+ jobsPath: MOCK_ENDPOINT,
+ namespacesPath: MOCK_ENDPOINT,
+};
+
+const {
+ clearJobsEtagPoll,
+ stopJobsPolling,
+ importAll,
+ fetchRepos,
+ fetchImport,
+ fetchJobs,
+ fetchNamespaces,
+ setFilter,
+} = actionsFactory({
+ endpoints,
+});
+
+describe('import_projects store actions', () => {
+ let localState;
+ const importRepoId = 1;
+ const otherImportRepoId = 2;
+ const defaultTargetNamespace = 'default';
+ const sanitizedName = 'sanitizedName';
+ const defaultImportTarget = { newName: sanitizedName, targetNamespace: defaultTargetNamespace };
+
+ beforeEach(() => {
+ localState = {
+ ...state(),
+ defaultTargetNamespace,
+ repositories: [
+ { importSource: { id: importRepoId, sanitizedName }, importStatus: STATUSES.NONE },
+ {
+ importSource: { id: otherImportRepoId, sanitizedName: 's2' },
+ importStatus: STATUSES.NONE,
+ },
+ {
+ importSource: { id: 3, sanitizedName: 's3', incompatible: true },
+ importStatus: STATUSES.NONE,
+ },
+ ],
+ provider: 'provider',
+ };
+
+ localState.getImportTarget = getImportTarget(localState);
+ });
+
+ describe('fetchRepos', () => {
+ let mock;
+ const payload = { imported_projects: [{}], provider_repos: [{}] };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+
+ return testAction(
+ fetchRepos,
+ null,
+ localState,
+ [
+ { type: SET_PAGE, payload: 1 },
+ { type: REQUEST_REPOS },
+ {
+ type: RECEIVE_REPOS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+
+ it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_ERROR and SET_PAGE again mutations on an unsuccessful request', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(500);
+
+ return testAction(
+ fetchRepos,
+ null,
+ localState,
+ [
+ { type: SET_PAGE, payload: 1 },
+ { type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 0 },
+ { type: RECEIVE_REPOS_ERROR },
+ ],
+ [],
+ );
+ });
+
+ it('includes page in url query params', async () => {
+ let requestedUrl;
+ mock.onGet().reply(config => {
+ requestedUrl = config.url;
+ return [200, payload];
+ });
+
+ const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
+
+ await testAction(fetchRepos, null, localStateWithPage, expect.any(Array), expect.any(Array));
+
+ expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
+ });
+
+ it('correctly updates current page on an unsuccessful request', () => {
+ mock.onGet(MOCK_ENDPOINT).reply(500);
+ const CURRENT_PAGE = 5;
+
+ return testAction(
+ fetchRepos,
+ null,
+ { ...localState, pageInfo: { page: CURRENT_PAGE } },
+ expect.arrayContaining([
+ { type: SET_PAGE, payload: CURRENT_PAGE + 1 },
+ { type: SET_PAGE, payload: CURRENT_PAGE },
+ ]),
+ [],
+ );
+ });
+
+ describe('when rate limited', () => {
+ it('commits RECEIVE_REPOS_ERROR and shows rate limited error message', async () => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(429);
+
+ await testAction(
+ fetchRepos,
+ null,
+ { ...localState, filter: 'filter' },
+ [
+ { type: SET_PAGE, payload: 1 },
+ { type: REQUEST_REPOS },
+ { type: SET_PAGE, payload: 0 },
+ { type: RECEIVE_REPOS_ERROR },
+ ],
+ [],
+ );
+
+ expect(createFlash).toHaveBeenCalledWith('Provider rate limit exceeded. Try again later');
+ });
+ });
+
+ describe('when filtered', () => {
+ it('fetches repos with filter applied', () => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
+
+ return testAction(
+ fetchRepos,
+ null,
+ { ...localState, filter: 'filter' },
+ [
+ { type: SET_PAGE, payload: 1 },
+ { type: REQUEST_REPOS },
+ {
+ type: RECEIVE_REPOS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+ });
+ });
+
+ describe('fetchImport', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('commits REQUEST_IMPORT and REQUEST_IMPORT_SUCCESS mutations on a successful request', () => {
+ const importedProject = { name: 'imported/project' };
+ mock.onPost(MOCK_ENDPOINT).reply(200, importedProject);
+
+ return testAction(
+ fetchImport,
+ importRepoId,
+ localState,
+ [
+ {
+ type: REQUEST_IMPORT,
+ payload: { repoId: importRepoId, importTarget: defaultImportTarget },
+ },
+ {
+ type: RECEIVE_IMPORT_SUCCESS,
+ payload: {
+ importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }),
+ repoId: importRepoId,
+ },
+ },
+ ],
+ [],
+ );
+ });
+
+ it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => {
+ mock.onPost(MOCK_ENDPOINT).reply(500);
+
+ await testAction(
+ fetchImport,
+ importRepoId,
+ localState,
+ [
+ {
+ type: REQUEST_IMPORT,
+ payload: { repoId: importRepoId, importTarget: defaultImportTarget },
+ },
+ { type: RECEIVE_IMPORT_ERROR, payload: importRepoId },
+ ],
+ [],
+ );
+
+ expect(createFlash).toHaveBeenCalledWith('Importing the project failed');
+ });
+
+ it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => {
+ const ERROR_MESSAGE = 'dummy';
+ mock.onPost(MOCK_ENDPOINT).reply(500, { errors: ERROR_MESSAGE });
+
+ await testAction(
+ fetchImport,
+ importRepoId,
+ localState,
+ [
+ {
+ type: REQUEST_IMPORT,
+ payload: { repoId: importRepoId, importTarget: defaultImportTarget },
+ },
+ { type: RECEIVE_IMPORT_ERROR, payload: importRepoId },
+ ],
+ [],
+ );
+
+ expect(createFlash).toHaveBeenCalledWith(`Importing the project failed: ${ERROR_MESSAGE}`);
+ });
+ });
+
+ describe('fetchJobs', () => {
+ let mock;
+ const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ stopJobsPolling();
+ clearJobsEtagPoll();
+ });
+
+ afterEach(() => mock.restore());
+
+ it('commits RECEIVE_JOBS_SUCCESS mutation on a successful request', async () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, updatedProjects);
+
+ await testAction(
+ fetchJobs,
+ null,
+ localState,
+ [
+ {
+ type: RECEIVE_JOBS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+
+ describe('when filtered', () => {
+ beforeEach(() => {
+ localState.filter = 'filter';
+ });
+
+ it('fetches realtime changes with filter applied', () => {
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, updatedProjects);
+
+ return testAction(
+ fetchJobs,
+ null,
+ localState,
+ [
+ {
+ type: RECEIVE_JOBS_SUCCESS,
+ payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+ });
+ });
+
+ describe('fetchNamespaces', () => {
+ let mock;
+ const namespaces = [{ full_name: 'test/ns1' }, { full_name: 'test_ns2' }];
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_SUCCESS on success', async () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, namespaces);
+
+ await testAction(
+ fetchNamespaces,
+ null,
+ localState,
+ [
+ { type: REQUEST_NAMESPACES },
+ {
+ type: RECEIVE_NAMESPACES_SUCCESS,
+ payload: convertObjectPropsToCamelCase(namespaces, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+
+ it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_ERROR and shows generic error message on an unsuccessful request', async () => {
+ mock.onGet(MOCK_ENDPOINT).reply(500);
+
+ await testAction(
+ fetchNamespaces,
+ null,
+ localState,
+ [{ type: REQUEST_NAMESPACES }, { type: RECEIVE_NAMESPACES_ERROR }],
+ [],
+ );
+
+ expect(createFlash).toHaveBeenCalledWith('Requesting namespaces failed');
+ });
+ });
+
+ describe('importAll', () => {
+ it('dispatches multiple fetchImport actions', async () => {
+ await testAction(
+ importAll,
+ null,
+ localState,
+ [],
+ [
+ { type: 'fetchImport', payload: importRepoId },
+ { type: 'fetchImport', payload: otherImportRepoId },
+ ],
+ );
+ });
+ });
+
+ describe('setFilter', () => {
+ it('dispatches sets the filter value and dispatches fetchRepos', async () => {
+ await testAction(
+ setFilter,
+ 'filteredRepo',
+ localState,
+ [{ type: SET_FILTER, payload: 'filteredRepo' }],
+ [{ type: 'fetchRepos' }],
+ );
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js
new file mode 100644
index 00000000000..f0ccffc19f2
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js
@@ -0,0 +1,135 @@
+import {
+ isLoading,
+ isImportingAnyRepo,
+ hasIncompatibleRepos,
+ hasImportableRepos,
+ importAllCount,
+ getImportTarget,
+} from '~/import_entities/import_projects/store/getters';
+import { STATUSES } from '~/import_entities/constants';
+import state from '~/import_entities/import_projects/store/state';
+
+const IMPORTED_REPO = {
+ importSource: {},
+ importedProject: { fullPath: 'some/path', importStatus: STATUSES.FINISHED },
+};
+
+const IMPORTABLE_REPO = {
+ importSource: { id: 'some-id', sanitizedName: 'sanitized' },
+ importedProject: null,
+};
+
+const INCOMPATIBLE_REPO = {
+ importSource: { incompatible: true },
+};
+
+describe('import_projects store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ it.each`
+ isLoadingRepos | isLoadingNamespaces | isLoadingValue
+ ${false} | ${false} | ${false}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ ${true} | ${true} | ${true}
+ `(
+ 'isLoading returns $isLoadingValue when isLoadingRepos is $isLoadingRepos and isLoadingNamespaces is $isLoadingNamespaces',
+ ({ isLoadingRepos, isLoadingNamespaces, isLoadingValue }) => {
+ Object.assign(localState, {
+ isLoadingRepos,
+ isLoadingNamespaces,
+ });
+
+ expect(isLoading(localState)).toBe(isLoadingValue);
+ },
+ );
+
+ it.each`
+ importStatus | value
+ ${STATUSES.NONE} | ${false}
+ ${STATUSES.SCHEDULING} | ${true}
+ ${STATUSES.SCHEDULED} | ${true}
+ ${STATUSES.STARTED} | ${true}
+ ${STATUSES.FINISHED} | ${false}
+ `(
+ 'isImportingAnyRepo returns $value when project with $importStatus status is available',
+ ({ importStatus, value }) => {
+ localState.repositories = [{ importedProject: { importStatus } }];
+
+ expect(isImportingAnyRepo(localState)).toBe(value);
+ },
+ );
+
+ it('isImportingAnyRepo returns false when project with no defined importStatus status is available', () => {
+ localState.repositories = [{ importSource: {} }];
+
+ expect(isImportingAnyRepo(localState)).toBe(false);
+ });
+
+ describe('hasIncompatibleRepos', () => {
+ it('returns true if there are any incompatible projects', () => {
+ localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
+
+ expect(hasIncompatibleRepos(localState)).toBe(true);
+ });
+
+ it('returns false if there are no incompatible projects', () => {
+ localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO];
+
+ expect(hasIncompatibleRepos(localState)).toBe(false);
+ });
+ });
+
+ describe('hasImportableRepos', () => {
+ it('returns true if there are any importable projects ', () => {
+ localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
+
+ expect(hasImportableRepos(localState)).toBe(true);
+ });
+
+ it('returns false if there are no importable projects', () => {
+ localState.repositories = [IMPORTED_REPO, INCOMPATIBLE_REPO];
+
+ expect(hasImportableRepos(localState)).toBe(false);
+ });
+ });
+
+ describe('importAllCount', () => {
+ it('returns count of available importable projects ', () => {
+ localState.repositories = [
+ IMPORTABLE_REPO,
+ IMPORTABLE_REPO,
+ IMPORTED_REPO,
+ INCOMPATIBLE_REPO,
+ ];
+
+ expect(importAllCount(localState)).toBe(2);
+ });
+ });
+
+ describe('getImportTarget', () => {
+ it('returns default value if no custom target available', () => {
+ localState.defaultTargetNamespace = 'default';
+ localState.repositories = [IMPORTABLE_REPO];
+
+ expect(getImportTarget(localState)(IMPORTABLE_REPO.importSource.id)).toStrictEqual({
+ newName: IMPORTABLE_REPO.importSource.sanitizedName,
+ targetNamespace: localState.defaultTargetNamespace,
+ });
+ });
+
+ it('returns custom import target if available', () => {
+ const fakeTarget = { newName: 'something', targetNamespace: 'ns' };
+ localState.repositories = [IMPORTABLE_REPO];
+ localState.customImportTargets[IMPORTABLE_REPO.importSource.id] = fakeTarget;
+
+ expect(getImportTarget(localState)(IMPORTABLE_REPO.importSource.id)).toStrictEqual(
+ fakeTarget,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
new file mode 100644
index 00000000000..8b7ddffe6f4
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -0,0 +1,319 @@
+import * as types from '~/import_entities/import_projects/store/mutation_types';
+import mutations from '~/import_entities/import_projects/store/mutations';
+import getInitialState from '~/import_entities/import_projects/store/state';
+import { STATUSES } from '~/import_entities/constants';
+
+describe('import_projects store mutations', () => {
+ let state;
+
+ const SOURCE_PROJECT = {
+ id: 1,
+ full_name: 'full/name',
+ sanitized_name: 'name',
+ provider_link: 'https://demo.link/full/name',
+ };
+ const IMPORTED_PROJECT = {
+ name: 'demo',
+ importSource: 'something',
+ providerLink: 'custom-link',
+ importStatus: 'status',
+ fullName: 'fullName',
+ };
+
+ describe(`${types.SET_FILTER}`, () => {
+ const NEW_VALUE = 'new-value';
+
+ beforeEach(() => {
+ state = {
+ filter: 'some-value',
+ repositories: ['some', ' repositories'],
+ pageInfo: { page: 1 },
+ };
+ mutations[types.SET_FILTER](state, NEW_VALUE);
+ });
+
+ it('removes current repositories list', () => {
+ expect(state.repositories.length).toBe(0);
+ });
+
+ it('resets current page to 0', () => {
+ expect(state.pageInfo.page).toBe(0);
+ });
+ });
+
+ describe(`${types.REQUEST_REPOS}`, () => {
+ it('sets repos loading flag to true', () => {
+ state = {};
+
+ mutations[types.REQUEST_REPOS](state);
+
+ expect(state.isLoadingRepos).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_REPOS_SUCCESS}`, () => {
+ describe('with legacy response format', () => {
+ describe('for imported projects', () => {
+ const response = {
+ importedProjects: [IMPORTED_PROJECT],
+ providerRepos: [],
+ };
+
+ it('recreates importSource from response', () => {
+ state = getInitialState();
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining({
+ fullName: IMPORTED_PROJECT.importSource,
+ sanitizedName: IMPORTED_PROJECT.name,
+ providerLink: IMPORTED_PROJECT.providerLink,
+ }),
+ );
+ });
+
+ it('passes project to importProject', () => {
+ state = getInitialState();
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(IMPORTED_PROJECT).toStrictEqual(
+ expect.objectContaining(state.repositories[0].importedProject),
+ );
+ });
+ });
+
+ describe('for importable projects', () => {
+ beforeEach(() => {
+ state = getInitialState();
+
+ const response = {
+ importedProjects: [],
+ providerRepos: [SOURCE_PROJECT],
+ };
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
+
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
+ });
+ });
+
+ describe('for incompatible projects', () => {
+ const response = {
+ importedProjects: [],
+ providerRepos: [],
+ incompatibleRepos: [SOURCE_PROJECT],
+ };
+
+ beforeEach(() => {
+ state = getInitialState();
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
+
+ it('sets incompatible flag', () => {
+ expect(state.repositories[0].importSource.incompatible).toBe(true);
+ });
+
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining(SOURCE_PROJECT),
+ );
+ });
+ });
+
+ it('sets repos loading flag to false', () => {
+ const response = {
+ importedProjects: [],
+ providerRepos: [],
+ };
+
+ state = getInitialState();
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.isLoadingRepos).toBe(false);
+ });
+ });
+
+ it('passes response as it is', () => {
+ const response = [];
+ state = getInitialState();
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.repositories).toStrictEqual(response);
+ });
+
+ it('sets repos loading flag to false', () => {
+ const response = [];
+
+ state = getInitialState();
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.isLoadingRepos).toBe(false);
+ });
+ });
+
+ describe(`${types.RECEIVE_REPOS_ERROR}`, () => {
+ it('sets repos loading flag to false', () => {
+ state = getInitialState();
+
+ mutations[types.RECEIVE_REPOS_ERROR](state);
+
+ expect(state.isLoadingRepos).toBe(false);
+ });
+ });
+
+ describe(`${types.REQUEST_IMPORT}`, () => {
+ beforeEach(() => {
+ const REPO_ID = 1;
+ const importTarget = { targetNamespace: 'ns', newName: 'name ' };
+ state = { repositories: [{ importSource: { id: REPO_ID } }] };
+
+ mutations[types.REQUEST_IMPORT](state, { repoId: REPO_ID, importTarget });
+ });
+
+ it(`sets status to ${STATUSES.SCHEDULING}`, () => {
+ expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.SCHEDULING);
+ });
+ });
+
+ describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => {
+ beforeEach(() => {
+ const REPO_ID = 1;
+ state = { repositories: [{ importSource: { id: REPO_ID } }] };
+
+ mutations[types.RECEIVE_IMPORT_SUCCESS](state, {
+ repoId: REPO_ID,
+ importedProject: IMPORTED_PROJECT,
+ });
+ });
+
+ it('sets import status', () => {
+ expect(state.repositories[0].importedProject.importStatus).toBe(
+ IMPORTED_PROJECT.importStatus,
+ );
+ });
+
+ it('sets imported project', () => {
+ expect(IMPORTED_PROJECT).toStrictEqual(
+ expect.objectContaining(state.repositories[0].importedProject),
+ );
+ });
+ });
+
+ describe(`${types.RECEIVE_IMPORT_ERROR}`, () => {
+ beforeEach(() => {
+ const REPO_ID = 1;
+ state = { repositories: [{ importSource: { id: REPO_ID } }] };
+
+ mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
+ });
+
+ it(`removes importedProject entry`, () => {
+ expect(state.repositories[0].importedProject).toBeNull();
+ });
+ });
+
+ describe(`${types.RECEIVE_JOBS_SUCCESS}`, () => {
+ it('updates import status of existing project', () => {
+ const repoId = 1;
+ state = {
+ repositories: [{ importedProject: { id: repoId }, importStatus: STATUSES.STARTED }],
+ };
+ const updatedProjects = [{ id: repoId, importStatus: STATUSES.FINISHED }];
+
+ mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
+
+ expect(state.repositories[0].importedProject.importStatus).toBe(
+ updatedProjects[0].importStatus,
+ );
+ });
+ });
+
+ describe(`${types.REQUEST_NAMESPACES}`, () => {
+ it('sets namespaces loading flag to true', () => {
+ state = {};
+
+ mutations[types.REQUEST_NAMESPACES](state);
+
+ expect(state.isLoadingNamespaces).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_NAMESPACES_SUCCESS}`, () => {
+ const response = [{ fullPath: 'some/path' }];
+
+ beforeEach(() => {
+ state = {};
+ mutations[types.RECEIVE_NAMESPACES_SUCCESS](state, response);
+ });
+
+ it('stores namespaces to state', () => {
+ expect(state.namespaces).toStrictEqual(response);
+ });
+
+ it('sets namespaces loading flag to false', () => {
+ expect(state.isLoadingNamespaces).toBe(false);
+ });
+ });
+
+ describe(`${types.RECEIVE_NAMESPACES_ERROR}`, () => {
+ it('sets namespaces loading flag to false', () => {
+ state = {};
+
+ mutations[types.RECEIVE_NAMESPACES_ERROR](state);
+
+ expect(state.isLoadingNamespaces).toBe(false);
+ });
+ });
+
+ describe(`${types.SET_IMPORT_TARGET}`, () => {
+ const PROJECT = {
+ id: 2,
+ sanitizedName: 'sanitizedName',
+ };
+
+ it('stores custom target if it differs from defaults', () => {
+ state = { customImportTargets: {}, repositories: [{ importSource: PROJECT }] };
+ const importTarget = { targetNamespace: 'ns', newName: 'name ' };
+
+ mutations[types.SET_IMPORT_TARGET](state, { repoId: PROJECT.id, importTarget });
+ expect(state.customImportTargets[PROJECT.id]).toBe(importTarget);
+ });
+
+ it('removes custom target if it is equal to defaults', () => {
+ const importTarget = { targetNamespace: 'ns', newName: 'name ' };
+ state = {
+ defaultTargetNamespace: 'default',
+ customImportTargets: {
+ [PROJECT.id]: importTarget,
+ },
+ repositories: [{ importSource: PROJECT }],
+ };
+
+ mutations[types.SET_IMPORT_TARGET](state, {
+ repoId: PROJECT.id,
+ importTarget: {
+ targetNamespace: state.defaultTargetNamespace,
+ newName: PROJECT.sanitizedName,
+ },
+ });
+
+ expect(state.customImportTargets[SOURCE_PROJECT.id]).toBeUndefined();
+ });
+ });
+
+ describe(`${types.SET_PAGE}`, () => {
+ it('sets page number', () => {
+ const NEW_PAGE = 4;
+ state = { pageInfo: { page: 5 } };
+
+ mutations[types.SET_PAGE](state, NEW_PAGE);
+ expect(state.pageInfo.page).toBe(NEW_PAGE);
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/utils_spec.js b/spec/frontend/import_entities/import_projects/utils_spec.js
new file mode 100644
index 00000000000..7d9c4b7137e
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/utils_spec.js
@@ -0,0 +1,69 @@
+import {
+ isProjectImportable,
+ isIncompatible,
+ getImportStatus,
+} from '~/import_entities/import_projects/utils';
+import { STATUSES } from '~/import_entities/constants';
+
+describe('import_projects utils', () => {
+ const COMPATIBLE_PROJECT = {
+ importSource: { incompatible: false },
+ };
+
+ const INCOMPATIBLE_PROJECT = {
+ importSource: { incompatible: true },
+ importedProject: null,
+ };
+
+ describe('isProjectImportable', () => {
+ it.each`
+ status | result
+ ${STATUSES.FINISHED} | ${false}
+ ${STATUSES.FAILED} | ${false}
+ ${STATUSES.SCHEDULED} | ${false}
+ ${STATUSES.STARTED} | ${false}
+ ${STATUSES.NONE} | ${true}
+ ${STATUSES.SCHEDULING} | ${false}
+ `('returns $result when project is compatible and status is $status', ({ status, result }) => {
+ expect(
+ isProjectImportable({
+ ...COMPATIBLE_PROJECT,
+ importedProject: { importStatus: status },
+ }),
+ ).toBe(result);
+ });
+
+ it('returns true if import status is not defined', () => {
+ expect(isProjectImportable({ importSource: {} })).toBe(true);
+ });
+
+ it('returns false if project is not compatible', () => {
+ expect(isProjectImportable(INCOMPATIBLE_PROJECT)).toBe(false);
+ });
+ });
+
+ describe('isIncompatible', () => {
+ it('returns true for incompatible project', () => {
+ expect(isIncompatible(INCOMPATIBLE_PROJECT)).toBe(true);
+ });
+
+ it('returns false for compatible project', () => {
+ expect(isIncompatible(COMPATIBLE_PROJECT)).toBe(false);
+ });
+ });
+
+ describe('getImportStatus', () => {
+ it('returns actual status when project status is provided', () => {
+ expect(
+ getImportStatus({
+ ...COMPATIBLE_PROJECT,
+ importedProject: { importStatus: STATUSES.FINISHED },
+ }),
+ ).toBe(STATUSES.FINISHED);
+ });
+
+ it('returns NONE as status if import status is not provided', () => {
+ expect(getImportStatus(COMPATIBLE_PROJECT)).toBe(STATUSES.NONE);
+ });
+ });
+});