debian-mirror-gitlab/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
2021-06-08 01:23:25 +05:30

462 lines
16 KiB
JavaScript

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,
createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory';
import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql';
import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
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();
}),
}));
const FAKE_ENDPOINTS = {
status: '/fake_status_url',
availableNamespaces: '/fake_available_namespaces',
createBulkImport: '/fake_create_bulk_import',
jobs: '/fake_jobs',
};
describe('Bulk import resolvers', () => {
let axiosMockAdapter;
let client;
const createClient = (extraResolverArgs) => {
return createMockClient({
cache: new InMemoryCache({
fragmentMatcher: { match: () => true },
addTypename: false,
}),
resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS, ...extraResolverArgs }),
});
};
beforeEach(() => {
axiosMockAdapter = new MockAdapter(axios);
client = createClient();
});
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('bulkImportSourceGroup', () => {
beforeEach(async () => {
axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
axiosMockAdapter
.onGet(FAKE_ENDPOINTS.availableNamespaces)
.reply(httpStatus.OK, availableNamespacesFixture);
return client.query({
query: bulkImportSourceGroupsQuery,
});
});
it('returns group', async () => {
const { id } = statusEndpointFixture.importable_data[0];
const {
data: { bulkImportSourceGroup: group },
} = await client.query({
query: bulkImportSourceGroupQuery,
variables: { id: id.toString() },
});
expect(group).toMatchObject(statusEndpointFixture.importable_data[0]);
});
});
describe('bulkImportSourceGroups', () => {
let results;
beforeEach(async () => {
axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
axiosMockAdapter
.onGet(FAKE_ENDPOINTS.availableNamespaces)
.reply(httpStatus.OK, availableNamespacesFixture);
});
it('respects cached import state when provided by group manager', async () => {
const FAKE_JOB_ID = '1';
const FAKE_STATUS = 'DEMO_STATUS';
const FAKE_IMPORT_TARGET = {
new_name: 'test-name',
target_namespace: 'test-namespace',
};
const TARGET_INDEX = 0;
const clientWithMockedManager = createClient({
GroupsManager: jest.fn().mockImplementation(() => ({
getImportStateFromStorageByGroupId(groupId) {
if (groupId === statusEndpointFixture.importable_data[TARGET_INDEX].id) {
return {
jobId: FAKE_JOB_ID,
importState: {
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).toStrictEqual(FAKE_IMPORT_TARGET);
expect(clientResults[TARGET_INDEX].progress.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 });
results = response.data.bulkImportSourceGroups.nodes;
});
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 default to none', () => {
expect(results.every((r) => r.progress.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);
});
it('starts polling when request completes', async () => {
const [statusPoller] = StatusPoller.mock.instances;
expect(statusPoller.startPolling).toHaveBeenCalled();
});
});
it.each`
variable | queryParam | value
${'filter'} | ${'filter'} | ${'demo'}
${'perPage'} | ${'per_page'} | ${30}
${'page'} | ${'page'} | ${3}
`(
'properly passes GraphQL variable $variable as REST $queryParam query parameter',
async ({ variable, queryParam, value }) => {
await client.query({
query: bulkImportSourceGroupsQuery,
variables: { [variable]: value },
});
const restCall = axiosMockAdapter.history.get.find(
(q) => q.url === FAKE_ENDPOINTS.status,
);
expect(restCall.params[queryParam]).toBe(value);
},
);
});
});
describe('mutations', () => {
const GROUP_ID = 1;
beforeEach(() => {
client.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
bulkImportSourceGroups: {
nodes: [
{
__typename: clientTypenames.BulkImportSourceGroup,
id: GROUP_ID,
progress: {
id: `test-${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',
},
validation_errors: [],
},
],
pageInfo: {
page: 1,
perPage: 20,
total: 37,
totalPages: 2,
},
},
},
});
});
it('setTargetNamespaces updates group target namespace', async () => {
const NEW_TARGET_NAMESPACE = 'target';
const {
data: {
setTargetNamespace: {
id: idInResponse,
import_target: { target_namespace: namespaceInResponse },
},
},
} = await client.mutate({
mutation: setTargetNamespaceMutation,
variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE },
});
expect(idInResponse).toBe(GROUP_ID);
expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE);
});
it('setNewName updates group target name', async () => {
const NEW_NAME = 'new';
const {
data: {
setNewName: {
id: idInResponse,
import_target: { new_name: nameInResponse },
},
},
} = await client.mutate({
mutation: setNewNameMutation,
variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME },
});
expect(idInResponse).toBe(GROUP_ID);
expect(nameInResponse).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: importGroupsMutation,
variables: { sourceGroupIds: [GROUP_ID] },
});
await waitForPromises();
const {
bulkImportSourceGroups: { nodes: intermediateResults },
} = client.readQuery({
query: bulkImportSourceGroupsQuery,
});
expect(intermediateResults[0].progress.status).toBe(STATUSES.SCHEDULING);
});
describe('when request completes', () => {
let results;
beforeEach(() => {
client
.watchQuery({
query: bulkImportSourceGroupsQuery,
fetchPolicy: 'cache-only',
})
.subscribe(({ data }) => {
results = data.bulkImportSourceGroups.nodes;
});
});
it('sets import status to CREATED when request completes', async () => {
axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
await client.mutate({
mutation: importGroupsMutation,
variables: { sourceGroupIds: [GROUP_ID] },
});
await waitForPromises();
expect(results[0].progress.status).toBe(STATUSES.CREATED);
});
it('resets status to NONE if request fails', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
.reply(httpStatus.INTERNAL_SERVER_ERROR);
client
.mutate({
mutation: [importGroupsMutation],
variables: { sourceGroupIds: [GROUP_ID] },
})
.catch(() => {});
await waitForPromises();
expect(results[0].progress.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: importGroupsMutation,
variables: { sourceGroupIds: [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: importGroupsMutation,
variables: { sourceGroupIds: [GROUP_ID] },
})
.catch(() => {});
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
});
});
it('setImportProgress updates group progress', async () => {
const NEW_STATUS = 'dummy';
const FAKE_JOB_ID = 5;
const {
data: {
setImportProgress: { progress },
},
} = await client.mutate({
mutation: setImportProgressMutation,
variables: { sourceGroupId: GROUP_ID, status: NEW_STATUS, jobId: FAKE_JOB_ID },
});
expect(progress).toMatchObject({
id: FAKE_JOB_ID,
status: NEW_STATUS,
});
});
it('updateImportStatus returns new status', async () => {
const NEW_STATUS = 'dummy';
const FAKE_JOB_ID = 5;
const {
data: { updateImportStatus: statusInResponse },
} = await client.mutate({
mutation: updateImportStatusMutation,
variables: { id: FAKE_JOB_ID, status: NEW_STATUS },
});
expect(statusInResponse).toMatchObject({
id: FAKE_JOB_ID,
status: NEW_STATUS,
});
});
it('addValidationError adds error to group', async () => {
const FAKE_FIELD = 'some-field';
const FAKE_MESSAGE = 'some-message';
const {
data: {
addValidationError: { validation_errors: validationErrors },
},
} = await client.mutate({
mutation: addValidationErrorMutation,
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
});
expect(validationErrors).toMatchObject([{ field: FAKE_FIELD, message: FAKE_MESSAGE }]);
});
it('removeValidationError removes error from group', async () => {
const FAKE_FIELD = 'some-field';
const FAKE_MESSAGE = 'some-message';
await client.mutate({
mutation: addValidationErrorMutation,
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
});
const {
data: {
removeValidationError: { validation_errors: validationErrors },
},
} = await client.mutate({
mutation: removeValidationErrorMutation,
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD },
});
expect(validationErrors).toMatchObject([]);
});
});
});