debian-mirror-gitlab/spec/frontend/environments/graphql/resolvers_spec.js
2023-07-09 08:55:56 +05:30

457 lines
16 KiB
JavaScript

import MockAdapter from 'axios-mock-adapter';
import { CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql';
import environmentToStopQuery from '~/environments/graphql/queries/environment_to_stop.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql';
import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql';
import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql';
import { TEST_HOST } from 'helpers/test_constants';
import {
environmentsApp,
resolvedEnvironmentsApp,
resolvedEnvironment,
folder,
resolvedFolder,
k8sPodsMock,
k8sServicesMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
describe('~/frontend/environments/graphql/resolvers', () => {
let mockResolvers;
let mock;
let mockApollo;
let localState;
const configuration = {
basePath: 'kas-proxy/',
baseOptions: {
headers: { 'GitLab-Agent-Id': '1' },
},
};
const namespace = 'default';
beforeEach(() => {
mockResolvers = resolvers(ENDPOINT);
mock = new MockAdapter(axios);
mockApollo = createMockApollo();
localState = mockApollo.defaultClient.localState;
});
afterEach(() => {
mock.reset();
});
describe('environmentApp', () => {
it('should fetch environments and map them to frontend data', async () => {
const cache = { writeQuery: jest.fn() };
const scope = 'available';
const search = '';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search } })
.reply(HTTP_STATUS_OK, environmentsApp, {});
const app = await mockResolvers.Query.environmentApp(
null,
{ scope, page: 1, search },
{ cache },
);
expect(app).toEqual(resolvedEnvironmentsApp);
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
data: { interval: undefined },
});
});
it('should set the poll interval when there is one', async () => {
const cache = { writeQuery: jest.fn() };
const scope = 'stopped';
const interval = 3000;
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
.reply(HTTP_STATUS_OK, environmentsApp, {
'poll-interval': interval,
});
await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
data: { interval },
});
});
it('should set page info if there is any', async () => {
const cache = { writeQuery: jest.fn() };
const scope = 'stopped';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
.reply(HTTP_STATUS_OK, environmentsApp, {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '5',
});
await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pageInfoQuery,
data: {
pageInfo: {
total: 37,
perPage: 2,
previousPage: NaN,
totalPages: 5,
nextPage: 2,
page: 1,
__typename: 'LocalPageInfo',
},
},
});
});
it('should not set page info if there is none', async () => {
const cache = { writeQuery: jest.fn() };
const scope = 'stopped';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
.reply(HTTP_STATUS_OK, environmentsApp, {});
await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pageInfoQuery,
data: {
pageInfo: {
__typename: 'LocalPageInfo',
nextPage: NaN,
page: NaN,
perPage: NaN,
previousPage: NaN,
total: NaN,
totalPages: NaN,
},
},
});
});
});
describe('folder', () => {
it('should fetch the folder url passed to it', async () => {
mock
.onGet(ENDPOINT, { params: { per_page: 3, scope: 'available', search: '' } })
.reply(HTTP_STATUS_OK, folder);
const environmentFolder = await mockResolvers.Query.folder(null, {
environment: { folderPath: ENDPOINT },
scope: 'available',
search: '',
});
expect(environmentFolder).toEqual(resolvedFolder);
});
});
describe('k8sPods', () => {
const mockPodsListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
data: {
items: k8sPodsMock,
},
});
});
const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
beforeEach(() => {
jest
.spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
.mockImplementation(mockNamespacedPodsListFn);
jest
.spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
.mockImplementation(mockAllPodsListFn);
});
it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
expect(mockNamespacedPodsListFn).toHaveBeenCalledWith(namespace);
expect(mockAllPodsListFn).not.toHaveBeenCalled();
expect(pods).toEqual(k8sPodsMock);
});
it('should request all pods from the cluster_client library if namespace is not specified', async () => {
const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' });
expect(mockAllPodsListFn).toHaveBeenCalled();
expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
expect(pods).toEqual(k8sPodsMock);
});
it('should throw an error if the API call fails', async () => {
jest
.spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
.mockRejectedValue(new Error('API error'));
await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow(
'API error',
);
});
});
describe('k8sServices', () => {
const mockServicesListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
data: {
items: k8sServicesMock,
},
});
});
beforeEach(() => {
jest
.spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
.mockImplementation(mockServicesListFn);
});
it('should request services from the cluster_client library', async () => {
const services = await mockResolvers.Query.k8sServices(null, { configuration });
expect(mockServicesListFn).toHaveBeenCalled();
expect(services).toEqual(k8sServicesMock);
});
it('should throw an error if the API call fails', async () => {
jest
.spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
.mockRejectedValue(new Error('API error'));
await expect(mockResolvers.Query.k8sServices(null, { configuration })).rejects.toThrow(
'API error',
);
});
});
describe('k8sWorkloads', () => {
const emptyImplementation = jest.fn().mockImplementation(() => {
return Promise.resolve({
data: {
items: [],
},
});
});
const [
mockNamespacedDeployment,
mockNamespacedDaemonSet,
mockNamespacedStatefulSet,
mockNamespacedReplicaSet,
mockNamespacedJob,
mockNamespacedCronJob,
mockAllDeployment,
mockAllDaemonSet,
mockAllStatefulSet,
mockAllReplicaSet,
mockAllJob,
mockAllCronJob,
] = Array(12).fill(emptyImplementation);
const namespacedMocks = [
{ method: 'listAppsV1NamespacedDeployment', api: AppsV1Api, spy: mockNamespacedDeployment },
{ method: 'listAppsV1NamespacedDaemonSet', api: AppsV1Api, spy: mockNamespacedDaemonSet },
{ method: 'listAppsV1NamespacedStatefulSet', api: AppsV1Api, spy: mockNamespacedStatefulSet },
{ method: 'listAppsV1NamespacedReplicaSet', api: AppsV1Api, spy: mockNamespacedReplicaSet },
{ method: 'listBatchV1NamespacedJob', api: BatchV1Api, spy: mockNamespacedJob },
{ method: 'listBatchV1NamespacedCronJob', api: BatchV1Api, spy: mockNamespacedCronJob },
];
const allMocks = [
{ method: 'listAppsV1DeploymentForAllNamespaces', api: AppsV1Api, spy: mockAllDeployment },
{ method: 'listAppsV1DaemonSetForAllNamespaces', api: AppsV1Api, spy: mockAllDaemonSet },
{ method: 'listAppsV1StatefulSetForAllNamespaces', api: AppsV1Api, spy: mockAllStatefulSet },
{ method: 'listAppsV1ReplicaSetForAllNamespaces', api: AppsV1Api, spy: mockAllReplicaSet },
{ method: 'listBatchV1JobForAllNamespaces', api: BatchV1Api, spy: mockAllJob },
{ method: 'listBatchV1CronJobForAllNamespaces', api: BatchV1Api, spy: mockAllCronJob },
];
beforeEach(() => {
[...namespacedMocks, ...allMocks].forEach((workloadMock) => {
jest
.spyOn(workloadMock.api.prototype, workloadMock.method)
.mockImplementation(workloadMock.spy);
});
});
it('should request namespaced workload types from the cluster_client library if namespace is specified', async () => {
await mockResolvers.Query.k8sWorkloads(null, { configuration, namespace });
namespacedMocks.forEach((workloadMock) => {
expect(workloadMock.spy).toHaveBeenCalledWith(namespace);
});
});
it('should request all workload types from the cluster_client library if namespace is not specified', async () => {
await mockResolvers.Query.k8sWorkloads(null, { configuration, namespace: '' });
allMocks.forEach((workloadMock) => {
expect(workloadMock.spy).toHaveBeenCalled();
});
});
it('should pass fulfilled calls data if one of the API calls fail', async () => {
jest
.spyOn(AppsV1Api.prototype, 'listAppsV1DeploymentForAllNamespaces')
.mockRejectedValue(new Error('API error'));
await expect(
mockResolvers.Query.k8sWorkloads(null, { configuration }),
).resolves.toBeDefined();
});
it('should throw an error if all the API calls fail', async () => {
[...allMocks].forEach((workloadMock) => {
jest
.spyOn(workloadMock.api.prototype, workloadMock.method)
.mockRejectedValue(new Error('API error'));
});
await expect(mockResolvers.Query.k8sWorkloads(null, { configuration })).rejects.toThrow(
'API error',
);
});
});
describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
expect(client.writeQuery).toHaveBeenCalledWith({
query: isEnvironmentStoppingQuery,
variables: { environment },
data: { isEnvironmentStopping: true },
});
});
it('should set is stopping to false if stop fails', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
expect(client.writeQuery).toHaveBeenCalledWith({
query: isEnvironmentStoppingQuery,
variables: { environment },
data: { isEnvironmentStopping: false },
});
});
});
describe('rollbackEnvironment', () => {
it('should post to the retry environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.rollbackEnvironment(null, {
environment: { retryUrl: ENDPOINT },
});
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
});
});
describe('deleteEnvironment', () => {
it('should DELETE to the delete environment path', async () => {
mock.onDelete(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.deleteEnvironment(null, {
environment: { deletePath: ENDPOINT },
});
expect(mock.history.delete).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'delete' }),
);
});
});
describe('cancelAutoStop', () => {
it('should post to the auto stop path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.cancelAutoStop(null, { autoStopUrl: ENDPOINT });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
});
});
describe('setEnvironmentToRollback', () => {
it('should write the given environment to the cache', () => {
localState.client.writeQuery = jest.fn();
mockResolvers.Mutation.setEnvironmentToRollback(
null,
{ environment: resolvedEnvironment },
localState,
);
expect(localState.client.writeQuery).toHaveBeenCalledWith({
query: environmentToRollback,
data: { environmentToRollback: resolvedEnvironment },
});
});
});
describe('setEnvironmentToDelete', () => {
it('should write the given environment to the cache', () => {
localState.client.writeQuery = jest.fn();
mockResolvers.Mutation.setEnvironmentToDelete(
null,
{ environment: resolvedEnvironment },
localState,
);
expect(localState.client.writeQuery).toHaveBeenCalledWith({
query: environmentToDelete,
data: { environmentToDelete: resolvedEnvironment },
});
});
});
describe('setEnvironmentToStop', () => {
it('should write the given environment to the cache', () => {
localState.client.writeQuery = jest.fn();
mockResolvers.Mutation.setEnvironmentToStop(
null,
{ environment: resolvedEnvironment },
localState,
);
expect(localState.client.writeQuery).toHaveBeenCalledWith({
query: environmentToStopQuery,
data: { environmentToStop: resolvedEnvironment },
});
});
});
describe('action', () => {
it('should POST to the given path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] });
});
it('should return a nice error message on fail', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({
__typename: 'LocalEnvironmentErrors',
errors: [s__('Environments|An error occurred while making the request.')],
});
});
});
});