diff options
Diffstat (limited to 'spec/frontend/import_entities/import_groups/graphql')
4 files changed, 567 insertions, 0 deletions
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); + }); + }); + }); +}); |