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.')], }); }); }); });