Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/environments')
-rw-r--r--spec/frontend/environments/edit_environment_spec.js19
-rw-r--r--spec/frontend/environments/environment_flux_resource_selector_spec.js178
-rw-r--r--spec/frontend/environments/environment_folder_spec.js17
-rw-r--r--spec/frontend/environments/environment_form_spec.js234
-rw-r--r--spec/frontend/environments/graphql/mock_data.js9
-rw-r--r--spec/frontend/environments/graphql/resolvers/base_spec.js (renamed from spec/frontend/environments/graphql/resolvers_spec.js)223
-rw-r--r--spec/frontend/environments/graphql/resolvers/flux_spec.js140
-rw-r--r--spec/frontend/environments/graphql/resolvers/kubernetes_spec.js238
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js17
-rw-r--r--spec/frontend/environments/kubernetes_status_bar_spec.js274
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js35
11 files changed, 1038 insertions, 346 deletions
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index 93fe9ed9400..b55bbb34c65 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -7,7 +7,7 @@ import EditEnvironment from '~/environments/components/edit_environment.vue';
import { createAlert } from '~/alert';
import { visitUrl } from '~/lib/utils/url_utility';
import getEnvironment from '~/environments/graphql/queries/environment.query.graphql';
-import getEnvironmentWithNamespace from '~/environments/graphql/queries/environment_with_namespace.graphql';
+import getEnvironmentWithFluxResource from '~/environments/graphql/queries/environment_with_flux_resource.query.graphql';
import updateEnvironment from '~/environments/graphql/mutations/update_environment.mutation.graphql';
import { __ } from '~/locale';
import createMockApollo from '../__helpers__/mock_apollo_helper';
@@ -21,6 +21,7 @@ const environment = {
externalUrl: 'https://foo.example.com',
clusterAgent: null,
kubernetesNamespace: null,
+ fluxResourcePath: null,
};
const resolvedEnvironment = { project: { id: '1', environment } };
const environmentUpdateSuccess = {
@@ -43,7 +44,7 @@ describe('~/environments/components/edit.vue', () => {
let wrapper;
const getEnvironmentQuery = jest.fn().mockResolvedValue({ data: resolvedEnvironment });
- const getEnvironmentWithNamespaceQuery = jest
+ const getEnvironmentWithFluxResourceQuery = jest
.fn()
.mockResolvedValue({ data: resolvedEnvironment });
@@ -59,7 +60,7 @@ describe('~/environments/components/edit.vue', () => {
const mocks = [
[getEnvironment, getEnvironmentQuery],
- [getEnvironmentWithNamespace, getEnvironmentWithNamespaceQuery],
+ [getEnvironmentWithFluxResource, getEnvironmentWithFluxResourceQuery],
[updateEnvironment, mutationHandler],
];
@@ -68,14 +69,14 @@ describe('~/environments/components/edit.vue', () => {
const createWrapperWithApollo = async ({
mutationHandler = updateEnvironmentSuccess,
- kubernetesNamespaceForEnvironment = false,
+ fluxResourceForEnvironment = false,
} = {}) => {
wrapper = mountExtended(EditEnvironment, {
propsData: { environment: {} },
provide: {
...provide,
glFeatures: {
- kubernetesNamespaceForEnvironment,
+ fluxResourceForEnvironment,
},
},
apolloProvider: createMockApolloProvider(mutationHandler),
@@ -170,10 +171,10 @@ describe('~/environments/components/edit.vue', () => {
});
});
- describe('when `kubernetesNamespaceForEnvironment` is enabled', () => {
- it('calls the `getEnvironmentWithNamespace` query', () => {
- createWrapperWithApollo({ kubernetesNamespaceForEnvironment: true });
- expect(getEnvironmentWithNamespaceQuery).toHaveBeenCalled();
+ describe('when `fluxResourceForEnvironment` is enabled', () => {
+ it('calls the `getEnvironmentWithFluxResource` query', () => {
+ createWrapperWithApollo({ fluxResourceForEnvironment: true });
+ expect(getEnvironmentWithFluxResourceQuery).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/environments/environment_flux_resource_selector_spec.js b/spec/frontend/environments/environment_flux_resource_selector_spec.js
new file mode 100644
index 00000000000..ba3375c731f
--- /dev/null
+++ b/spec/frontend/environments/environment_flux_resource_selector_spec.js
@@ -0,0 +1,178 @@
+import { GlCollapsibleListbox, GlAlert } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { s__ } from '~/locale';
+import EnvironmentFluxResourceSelector from '~/environments/components/environment_flux_resource_selector.vue';
+import createMockApollo from '../__helpers__/mock_apollo_helper';
+import { mockKasTunnelUrl } from './mock_data';
+
+const configuration = {
+ basePath: mockKasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: {
+ 'GitLab-Agent-Id': 1,
+ },
+ withCredentials: true,
+ },
+};
+const namespace = 'my-namespace';
+
+const DEFAULT_PROPS = {
+ configuration,
+ namespace,
+ fluxResourcePath: '',
+};
+
+describe('~/environments/components/form.vue', () => {
+ let wrapper;
+
+ const kustomizationItem = {
+ apiVersion: 'kustomize.toolkit.fluxcd.io/v1beta1',
+ metadata: { name: 'kustomization', namespace },
+ };
+ const helmReleaseItem = {
+ apiVersion: 'helm.toolkit.fluxcd.io/v2beta1',
+ metadata: { name: 'helm-release', namespace },
+ };
+
+ const getKustomizationsQueryResult = jest.fn().mockReturnValue([kustomizationItem]);
+
+ const getHelmReleasesQueryResult = jest.fn().mockReturnValue([helmReleaseItem]);
+
+ const createWrapper = ({
+ propsData = {},
+ kustomizationsQueryResult = null,
+ helmReleasesQueryResult = null,
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ const mockResolvers = {
+ Query: {
+ fluxKustomizations: kustomizationsQueryResult || getKustomizationsQueryResult,
+ fluxHelmReleases: helmReleasesQueryResult || getHelmReleasesQueryResult,
+ },
+ };
+
+ return shallowMount(EnvironmentFluxResourceSelector, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...propsData,
+ },
+ apolloProvider: createMockApollo([], mockResolvers),
+ });
+ };
+
+ const findFluxResourceSelector = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ describe('default', () => {
+ const kustomizationValue = `${kustomizationItem.apiVersion}/namespaces/${kustomizationItem.metadata.namespace}/kustomizations/${kustomizationItem.metadata.name}`;
+ const helmReleaseValue = `${helmReleaseItem.apiVersion}/namespaces/${helmReleaseItem.metadata.namespace}/helmreleases/${helmReleaseItem.metadata.name}`;
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('renders flux resource selector', () => {
+ expect(findFluxResourceSelector().exists()).toBe(true);
+ });
+
+ it('requests the flux resources', async () => {
+ await waitForPromises();
+
+ expect(getKustomizationsQueryResult).toHaveBeenCalled();
+ expect(getHelmReleasesQueryResult).toHaveBeenCalled();
+ });
+
+ it('sets the loading prop while fetching the list', async () => {
+ expect(findFluxResourceSelector().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findFluxResourceSelector().props('loading')).toBe(false);
+ });
+
+ it('renders a list of available flux resources', async () => {
+ await waitForPromises();
+
+ expect(findFluxResourceSelector().props('items')).toEqual([
+ {
+ text: s__('Environments|Kustomizations'),
+ options: [{ value: kustomizationValue, text: kustomizationItem.metadata.name }],
+ },
+ {
+ text: s__('Environments|HelmReleases'),
+ options: [{ value: helmReleaseValue, text: helmReleaseItem.metadata.name }],
+ },
+ ]);
+ });
+
+ it('filters the flux resources list on user search', async () => {
+ await waitForPromises();
+ findFluxResourceSelector().vm.$emit('search', 'kustomization');
+ await nextTick();
+
+ expect(findFluxResourceSelector().props('items')).toEqual([
+ {
+ text: s__('Environments|Kustomizations'),
+ options: [{ value: kustomizationValue, text: kustomizationItem.metadata.name }],
+ },
+ ]);
+ });
+
+ it('emits changes to the fluxResourcePath', () => {
+ findFluxResourceSelector().vm.$emit('select', kustomizationValue);
+
+ expect(wrapper.emitted('change')).toEqual([[kustomizationValue]]);
+ });
+ });
+
+ describe('when environment has an associated flux resource path', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ propsData: { fluxResourcePath: 'path/to/flux/resource/name/default' },
+ });
+ });
+
+ it('updates flux resource selector with the name of the associated flux resource', () => {
+ expect(findFluxResourceSelector().props('toggleText')).toBe('default');
+ });
+ });
+
+ describe('on error', () => {
+ const error = new Error('Error from the cluster_client API');
+
+ it('renders an alert with both resource types mentioned when both queries failed', async () => {
+ wrapper = createWrapper({
+ kustomizationsQueryResult: jest.fn().mockRejectedValueOnce(error),
+ helmReleasesQueryResult: jest.fn().mockRejectedValueOnce(error),
+ });
+ await waitForPromises();
+
+ expect(findAlert().text()).toContain(
+ s__(
+ 'Environments|Unable to access the following resources from this environment. Check your authorization on the following and try again',
+ ),
+ );
+ expect(findAlert().text()).toContain('Kustomization');
+ expect(findAlert().text()).toContain('HelmRelease');
+ });
+
+ it('renders an alert with only failed resource type mentioned when one query failed', async () => {
+ wrapper = createWrapper({
+ kustomizationsQueryResult: jest.fn().mockRejectedValueOnce(error),
+ });
+ await waitForPromises();
+
+ expect(findAlert().text()).toContain(
+ s__(
+ 'Environments|Unable to access the following resources from this environment. Check your authorization on the following and try again',
+ ),
+ );
+ expect(findAlert().text()).toContain('Kustomization');
+ expect(findAlert().text()).not.toContain('HelmRelease');
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index 65c16697d44..1973613897d 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -74,8 +74,6 @@ describe('~/environments/components/environments_folder.vue', () => {
beforeEach(() => {
collapse = wrapper.findComponent(GlCollapse);
icons = wrapper.findAllComponents(GlIcon);
- jest.spyOn(wrapper.vm.$apollo.queries.folder, 'startPolling');
- jest.spyOn(wrapper.vm.$apollo.queries.folder, 'stopPolling');
});
it('is collapsed by default', () => {
@@ -88,8 +86,12 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(link.exists()).toBe(false);
});
- it('opens on click', async () => {
+ it('opens on click and starts polling', async () => {
+ expect(environmentFolderMock).toHaveBeenCalledTimes(1);
+
await button.trigger('click');
+ jest.advanceTimersByTime(2000);
+ await waitForPromises();
const link = findLink();
@@ -100,7 +102,7 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(folderName.classes('gl-font-weight-bold')).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
- expect(wrapper.vm.$apollo.queries.folder.startPolling).toHaveBeenCalledWith(2000);
+ expect(environmentFolderMock).toHaveBeenCalledTimes(2);
});
it('displays all environments when opened', async () => {
@@ -117,12 +119,15 @@ describe('~/environments/components/environments_folder.vue', () => {
it('stops polling on click', async () => {
await button.trigger('click');
- expect(wrapper.vm.$apollo.queries.folder.startPolling).toHaveBeenCalledWith(2000);
+ jest.advanceTimersByTime(2000);
+ await waitForPromises();
+
+ expect(environmentFolderMock).toHaveBeenCalledTimes(2);
const collapseButton = wrapper.findByRole('button', { name: __('Collapse') });
await collapseButton.trigger('click');
- expect(wrapper.vm.$apollo.queries.folder.stopPolling).toHaveBeenCalled();
+ expect(environmentFolderMock).toHaveBeenCalledTimes(2);
});
});
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index 803207bcce8..1b80b596db7 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentForm from '~/environments/components/environment_form.vue';
import getUserAuthorizedAgents from '~/environments/graphql/queries/user_authorized_agents.query.graphql';
+import EnvironmentFluxResourceSelector from '~/environments/components/environment_flux_resource_selector.vue';
import createMockApollo from '../__helpers__/mock_apollo_helper';
import { mockKasTunnelUrl } from './mock_data';
@@ -25,6 +26,16 @@ const userAccessAuthorizedAgents = [
{ agent: { id: '2', name: 'agent-2' } },
];
+const configuration = {
+ basePath: mockKasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: {
+ 'GitLab-Agent-Id': 2,
+ },
+ withCredentials: true,
+ },
+};
+
describe('~/environments/components/form.vue', () => {
let wrapper;
@@ -44,7 +55,7 @@ describe('~/environments/components/form.vue', () => {
const createWrapperWithApollo = ({
propsData = {},
- kubernetesNamespaceForEnvironment = false,
+ fluxResourceForEnvironment = false,
queryResult = null,
} = {}) => {
Vue.use(VueApollo);
@@ -73,7 +84,7 @@ describe('~/environments/components/form.vue', () => {
provide: {
...PROVIDE,
glFeatures: {
- kubernetesNamespaceForEnvironment,
+ fluxResourceForEnvironment,
},
},
propsData: {
@@ -87,6 +98,7 @@ describe('~/environments/components/form.vue', () => {
const findAgentSelector = () => wrapper.findByTestId('agent-selector');
const findNamespaceSelector = () => wrapper.findByTestId('namespace-selector');
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findFluxResourceSelector = () => wrapper.findComponent(EnvironmentFluxResourceSelector);
const selectAgent = async () => {
findAgentSelector().vm.$emit('shown');
@@ -290,132 +302,159 @@ describe('~/environments/components/form.vue', () => {
await selectAgent();
expect(wrapper.emitted('change')).toEqual([
- [{ name: '', externalUrl: '', clusterAgentId: '2', kubernetesNamespace: null }],
+ [
+ {
+ name: '',
+ externalUrl: '',
+ clusterAgentId: '2',
+ kubernetesNamespace: null,
+ fluxResourcePath: null,
+ },
+ ],
]);
});
});
describe('namespace selector', () => {
- it("doesn't render namespace selector if `kubernetesNamespaceForEnvironment` feature flag is disabled", () => {
+ beforeEach(() => {
wrapper = createWrapperWithApollo();
+ });
+
+ it("doesn't render namespace selector by default", () => {
expect(findNamespaceSelector().exists()).toBe(false);
});
- describe('when `kubernetesNamespaceForEnvironment` feature flag is enabled', () => {
- beforeEach(() => {
- wrapper = createWrapperWithApollo({
- kubernetesNamespaceForEnvironment: true,
- });
+ describe('when the agent was selected', () => {
+ beforeEach(async () => {
+ await selectAgent();
});
- it("doesn't render namespace selector by default", () => {
- expect(findNamespaceSelector().exists()).toBe(false);
+ it('renders namespace selector', () => {
+ expect(findNamespaceSelector().exists()).toBe(true);
});
- describe('when the agent was selected', () => {
- beforeEach(async () => {
- await selectAgent();
- });
+ it('requests the kubernetes namespaces with the correct configuration', async () => {
+ await waitForPromises();
- it('renders namespace selector', () => {
- expect(findNamespaceSelector().exists()).toBe(true);
- });
+ expect(getNamespacesQueryResult).toHaveBeenCalledWith(
+ {},
+ { configuration },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
- it('requests the kubernetes namespaces with the correct configuration', async () => {
- const configuration = {
- basePath: mockKasTunnelUrl.replace(/\/$/, ''),
- baseOptions: {
- headers: {
- 'GitLab-Agent-Id': 2,
- },
- withCredentials: true,
- },
- };
+ it('sets the loading prop while fetching the list', async () => {
+ expect(findNamespaceSelector().props('loading')).toBe(true);
- await waitForPromises();
+ await waitForPromises();
- expect(getNamespacesQueryResult).toHaveBeenCalledWith(
- {},
- { configuration },
- expect.anything(),
- expect.anything(),
- );
- });
+ expect(findNamespaceSelector().props('loading')).toBe(false);
+ });
- it('sets the loading prop while fetching the list', async () => {
- expect(findNamespaceSelector().props('loading')).toBe(true);
+ it('renders a list of available namespaces', async () => {
+ await waitForPromises();
- await waitForPromises();
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { text: 'default', value: 'default' },
+ { text: 'agent', value: 'agent' },
+ ]);
+ });
- expect(findNamespaceSelector().props('loading')).toBe(false);
- });
+ it('filters the namespaces list on user search', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('search', 'default');
- it('renders a list of available namespaces', async () => {
- await waitForPromises();
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { value: 'default', text: 'default' },
+ ]);
+ });
- expect(findNamespaceSelector().props('items')).toEqual([
- { text: 'default', value: 'default' },
- { text: 'agent', value: 'agent' },
- ]);
- });
+ it('updates namespace selector field with the name of selected namespace', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
- it('filters the namespaces list on user search', async () => {
- await waitForPromises();
- await findNamespaceSelector().vm.$emit('search', 'default');
+ expect(findNamespaceSelector().props('toggleText')).toBe('agent');
+ });
- expect(findNamespaceSelector().props('items')).toEqual([
- { value: 'default', text: 'default' },
- ]);
- });
+ it('emits changes to the kubernetesNamespace', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
- it('updates namespace selector field with the name of selected namespace', async () => {
- await waitForPromises();
- await findNamespaceSelector().vm.$emit('select', 'agent');
+ expect(wrapper.emitted('change')[1]).toEqual([
+ { name: '', externalUrl: '', kubernetesNamespace: 'agent', fluxResourcePath: null },
+ ]);
+ });
- expect(findNamespaceSelector().props('toggleText')).toBe('agent');
- });
+ it('clears namespace selector when another agent was selected', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
- it('emits changes to the kubernetesNamespace', async () => {
- await waitForPromises();
- await findNamespaceSelector().vm.$emit('select', 'agent');
+ expect(findNamespaceSelector().props('toggleText')).toBe('agent');
+
+ await findAgentSelector().vm.$emit('select', '1');
+ expect(findNamespaceSelector().props('toggleText')).toBe(
+ EnvironmentForm.i18n.namespaceHelpText,
+ );
+ });
+ });
- expect(wrapper.emitted('change')[1]).toEqual([
- { name: '', externalUrl: '', kubernetesNamespace: 'agent' },
- ]);
+ describe('when cannot connect to the cluster', () => {
+ const error = new Error('Error from the cluster_client API');
+
+ beforeEach(async () => {
+ wrapper = createWrapperWithApollo({
+ queryResult: jest.fn().mockRejectedValueOnce(error),
});
- it('clears namespace selector when another agent was selected', async () => {
- await waitForPromises();
- await findNamespaceSelector().vm.$emit('select', 'agent');
+ await selectAgent();
+ await waitForPromises();
+ });
+
+ it("doesn't render the namespace selector", () => {
+ expect(findNamespaceSelector().exists()).toBe(false);
+ });
- expect(findNamespaceSelector().props('toggleText')).toBe('agent');
+ it('renders an alert', () => {
+ expect(findAlert().text()).toBe('Error from the cluster_client API');
+ });
+ });
+ });
- await findAgentSelector().vm.$emit('select', '1');
- expect(findNamespaceSelector().props('toggleText')).toBe(
- EnvironmentForm.i18n.namespaceHelpText,
- );
+ describe('flux resource selector', () => {
+ it("doesn't render if `fluxResourceForEnvironment` feature flag is disabled", () => {
+ wrapper = createWrapperWithApollo();
+ expect(findFluxResourceSelector().exists()).toBe(false);
+ });
+
+ describe('when `fluxResourceForEnvironment` feature flag is enabled', () => {
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo({
+ fluxResourceForEnvironment: true,
});
});
- describe('when cannot connect to the cluster', () => {
- const error = new Error('Error from the cluster_client API');
+ it("doesn't render flux resource selector by default", () => {
+ expect(findFluxResourceSelector().exists()).toBe(false);
+ });
+ describe('when the agent was selected', () => {
beforeEach(async () => {
- wrapper = createWrapperWithApollo({
- kubernetesNamespaceForEnvironment: true,
- queryResult: jest.fn().mockRejectedValueOnce(error),
- });
-
await selectAgent();
- await waitForPromises();
});
- it("doesn't render the namespace selector", () => {
- expect(findNamespaceSelector().exists()).toBe(false);
+ it("doesn't render flux resource selector", () => {
+ expect(findFluxResourceSelector().exists()).toBe(false);
});
- it('renders an alert', () => {
- expect(findAlert().text()).toBe('Error from the cluster_client API');
+ it('renders the flux resource selector when the namespace is selected', async () => {
+ await findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(findFluxResourceSelector().props()).toEqual({
+ namespace: 'agent',
+ fluxResourcePath: '',
+ configuration,
+ });
});
});
});
@@ -430,7 +469,6 @@ describe('~/environments/components/form.vue', () => {
beforeEach(() => {
wrapper = createWrapperWithApollo({
propsData: { environment: environmentWithAgent },
- kubernetesNamespaceForEnvironment: true,
});
});
@@ -463,7 +501,6 @@ describe('~/environments/components/form.vue', () => {
beforeEach(() => {
wrapper = createWrapperWithApollo({
propsData: { environment: environmentWithAgentAndNamespace },
- kubernetesNamespaceForEnvironment: true,
});
});
@@ -472,4 +509,25 @@ describe('~/environments/components/form.vue', () => {
expect(findNamespaceSelector().props('toggleText')).toBe('default');
});
});
+
+ describe('when environment has an associated flux resource', () => {
+ const fluxResourcePath = 'path/to/flux/resource';
+ const environmentWithAgentAndNamespace = {
+ ...DEFAULT_PROPS.environment,
+ clusterAgent: { id: '1', name: 'agent-1' },
+ clusterAgentId: '1',
+ kubernetesNamespace: 'default',
+ fluxResourcePath,
+ };
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo({
+ propsData: { environment: environmentWithAgentAndNamespace },
+ fluxResourceForEnvironment: true,
+ });
+ });
+
+ it('provides flux resource path to the flux resource selector component', () => {
+ expect(findFluxResourceSelector().props('fluxResourcePath')).toBe(fluxResourcePath);
+ });
+ });
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index c2eafa5f51e..fd97f19a6ab 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -914,3 +914,12 @@ export const k8sNamespacesMock = [
{ metadata: { name: 'default' } },
{ metadata: { name: 'agent' } },
];
+
+export const fluxKustomizationsMock = [
+ {
+ status: 'True',
+ type: 'Ready',
+ },
+];
+
+export const fluxResourcePathMock = 'path/to/flux/resource';
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers/base_spec.js
index be210ed619e..e01cf18c40d 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/base_spec.js
@@ -1,5 +1,4 @@
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';
@@ -12,17 +11,13 @@ import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.quer
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 { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
import {
environmentsApp,
resolvedEnvironmentsApp,
resolvedEnvironment,
folder,
resolvedFolder,
- k8sPodsMock,
- k8sServicesMock,
- k8sNamespacesMock,
-} from './mock_data';
+} from '../mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -32,14 +27,6 @@ describe('~/frontend/environments/graphql/resolvers', () => {
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);
@@ -156,215 +143,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
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('k8sNamespaces', () => {
- const mockNamespacesListFn = jest.fn().mockImplementation(() => {
- return Promise.resolve({
- data: {
- items: k8sNamespacesMock,
- },
- });
- });
-
- beforeEach(() => {
- jest
- .spyOn(CoreV1Api.prototype, 'listCoreV1Namespace')
- .mockImplementation(mockNamespacesListFn);
- });
-
- it('should request all namespaces from the cluster_client library', async () => {
- const namespaces = await mockResolvers.Query.k8sNamespaces(null, { configuration });
-
- expect(mockNamespacesListFn).toHaveBeenCalled();
-
- expect(namespaces).toEqual(k8sNamespacesMock);
- });
- it.each([
- ['Unauthorized', CLUSTER_AGENT_ERROR_MESSAGES.unauthorized],
- ['Forbidden', CLUSTER_AGENT_ERROR_MESSAGES.forbidden],
- ['Not found', CLUSTER_AGENT_ERROR_MESSAGES['not found']],
- ['Unknown', CLUSTER_AGENT_ERROR_MESSAGES.other],
- ])(
- 'should throw an error if the API call fails with the reason "%s"',
- async (reason, message) => {
- jest.spyOn(CoreV1Api.prototype, 'listCoreV1Namespace').mockRejectedValue({
- response: {
- data: {
- reason,
- },
- },
- });
-
- await expect(mockResolvers.Query.k8sNamespaces(null, { configuration })).rejects.toThrow(
- message,
- );
- },
- );
- });
describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
diff --git a/spec/frontend/environments/graphql/resolvers/flux_spec.js b/spec/frontend/environments/graphql/resolvers/flux_spec.js
new file mode 100644
index 00000000000..aa6f9e120f0
--- /dev/null
+++ b/spec/frontend/environments/graphql/resolvers/flux_spec.js
@@ -0,0 +1,140 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
+import { resolvers } from '~/environments/graphql/resolvers';
+import { fluxKustomizationsMock } from '../mock_data';
+
+describe('~/frontend/environments/graphql/resolvers', () => {
+ let mockResolvers;
+ let mock;
+
+ const configuration = {
+ basePath: 'kas-proxy/',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+ const namespace = 'default';
+ const environmentName = 'my-environment';
+
+ beforeEach(() => {
+ mockResolvers = resolvers();
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+
+ describe('fluxKustomizationStatus', () => {
+ const endpoint = `${configuration.basePath}/apis/kustomize.toolkit.fluxcd.io/v1beta1/namespaces/${namespace}/kustomizations/${environmentName}`;
+ const fluxResourcePath =
+ 'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app';
+ const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`;
+
+ it('should request Flux Kustomizations for the provided namespace via the Kubernetes API if the fluxResourcePath is not specified', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
+ });
+ it('should request Flux Kustomization for the provided fluxResourcePath via the Kubernetes API', async () => {
+ mock
+ .onGet(endpointWithFluxResourcePath, {
+ withCredentials: true,
+ headers: configuration.baseOptions.headers,
+ })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ fluxResourcePath,
+ });
+
+ expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxKustomizationsError = mockResolvers.Query.fluxKustomizationStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ await expect(fluxKustomizationsError).rejects.toThrow(apiError);
+ });
+ });
+
+ describe('fluxHelmReleaseStatus', () => {
+ const endpoint = `${configuration.basePath}/apis/helm.toolkit.fluxcd.io/v2beta1/namespaces/${namespace}/helmreleases/${environmentName}`;
+ const fluxResourcePath =
+ 'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
+ const endpointWithFluxResourcePath = `${configuration.basePath}/apis/${fluxResourcePath}`;
+
+ it('should request Flux Helm Releases via the Kubernetes API', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ });
+ it('should request Flux HelmRelease for the provided fluxResourcePath via the Kubernetes API', async () => {
+ mock
+ .onGet(endpointWithFluxResourcePath, {
+ withCredentials: true,
+ headers: configuration.baseOptions.headers,
+ })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ fluxResourcePath,
+ });
+
+ expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxHelmReleasesError = mockResolvers.Query.fluxHelmReleaseStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ await expect(fluxHelmReleasesError).rejects.toThrow(apiError);
+ });
+ });
+});
diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
new file mode 100644
index 00000000000..1d41fb11b14
--- /dev/null
+++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
@@ -0,0 +1,238 @@
+import MockAdapter from 'axios-mock-adapter';
+import { CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import axios from '~/lib/utils/axios_utils';
+import { resolvers } from '~/environments/graphql/resolvers';
+import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
+import { k8sPodsMock, k8sServicesMock, k8sNamespacesMock } from '../mock_data';
+
+describe('~/frontend/environments/graphql/resolvers', () => {
+ let mockResolvers;
+ let mock;
+
+ const configuration = {
+ basePath: 'kas-proxy/',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+ const namespace = 'default';
+
+ beforeEach(() => {
+ mockResolvers = resolvers();
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+
+ 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('k8sNamespaces', () => {
+ const mockNamespacesListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sNamespacesMock,
+ },
+ });
+ });
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1Namespace')
+ .mockImplementation(mockNamespacesListFn);
+ });
+
+ it('should request all namespaces from the cluster_client library', async () => {
+ const namespaces = await mockResolvers.Query.k8sNamespaces(null, { configuration });
+
+ expect(mockNamespacesListFn).toHaveBeenCalled();
+
+ expect(namespaces).toEqual(k8sNamespacesMock);
+ });
+ it.each([
+ ['Unauthorized', CLUSTER_AGENT_ERROR_MESSAGES.unauthorized],
+ ['Forbidden', CLUSTER_AGENT_ERROR_MESSAGES.forbidden],
+ ['Not found', CLUSTER_AGENT_ERROR_MESSAGES['not found']],
+ ['Unknown', CLUSTER_AGENT_ERROR_MESSAGES.other],
+ ])(
+ 'should throw an error if the API call fails with the reason "%s"',
+ async (reason, message) => {
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1Namespace').mockRejectedValue({
+ response: {
+ data: {
+ reason,
+ },
+ },
+ });
+
+ await expect(mockResolvers.Query.k8sNamespaces(null, { configuration })).rejects.toThrow(
+ message,
+ );
+ },
+ );
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 1c7ace00f48..aa7e2e9a3b7 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -6,12 +6,19 @@ import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info
import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
import KubernetesStatusBar from '~/environments/components/kubernetes_status_bar.vue';
-import { agent, kubernetesNamespace } from './graphql/mock_data';
+import {
+ agent,
+ kubernetesNamespace,
+ resolvedEnvironment,
+ fluxResourcePathMock,
+} from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
const propsData = {
clusterAgent: agent,
namespace: kubernetesNamespace,
+ environmentName: resolvedEnvironment.name,
+ fluxResourcePath: fluxResourcePathMock,
};
const provide = {
@@ -110,7 +117,13 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
});
it('renders kubernetes status bar', () => {
- expect(findKubernetesStatusBar().exists()).toBe(true);
+ expect(findKubernetesStatusBar().props()).toEqual({
+ clusterHealthStatus: 'success',
+ configuration,
+ namespace: kubernetesNamespace,
+ environmentName: resolvedEnvironment.name,
+ fluxResourcePath: fluxResourcePathMock,
+ });
});
});
diff --git a/spec/frontend/environments/kubernetes_status_bar_spec.js b/spec/frontend/environments/kubernetes_status_bar_spec.js
index 2ebb30e2766..5dec7ca5aac 100644
--- a/spec/frontend/environments/kubernetes_status_bar_spec.js
+++ b/spec/frontend/environments/kubernetes_status_bar_spec.js
@@ -1,20 +1,67 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlPopover, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import KubernetesStatusBar from '~/environments/components/kubernetes_status_bar.vue';
import {
CLUSTER_STATUS_HEALTHY_TEXT,
CLUSTER_STATUS_UNHEALTHY_TEXT,
+ SYNC_STATUS_BADGES,
} from '~/environments/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { s__ } from '~/locale';
+import { mockKasTunnelUrl } from './mock_data';
+
+Vue.use(VueApollo);
+
+const configuration = {
+ basePath: mockKasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ withCredentials: true,
+ },
+};
+const environmentName = 'environment_name';
describe('~/environments/components/kubernetes_status_bar.vue', () => {
let wrapper;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findHealthBadge = () => wrapper.findComponent(GlBadge);
+ const findHealthBadge = () => wrapper.findByTestId('health-badge');
+ const findSyncBadge = () => wrapper.findByTestId('sync-badge');
+ const findPopover = () => wrapper.findComponent(GlPopover);
+
+ const fluxKustomizationStatusQuery = jest.fn().mockReturnValue([]);
+ const fluxHelmReleaseStatusQuery = jest.fn().mockReturnValue([]);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomizationStatus: fluxKustomizationStatusQuery,
+ fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
- const createWrapper = ({ clusterHealthStatus = '' } = {}) => {
- wrapper = shallowMount(KubernetesStatusBar, {
- propsData: { clusterHealthStatus },
+ const createWrapper = ({
+ apolloProvider = createApolloProvider(),
+ clusterHealthStatus = '',
+ namespace = '',
+ fluxResourcePath = '',
+ } = {}) => {
+ wrapper = shallowMountExtended(KubernetesStatusBar, {
+ propsData: {
+ clusterHealthStatus,
+ configuration,
+ environmentName,
+ namespace,
+ fluxResourcePath,
+ },
+ apolloProvider,
+ stubs: { GlSprintf },
});
};
@@ -39,4 +86,219 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
},
);
});
+
+ describe('sync badge', () => {
+ describe('when no namespace is provided', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it("doesn't request Kustomizations and HelmReleases", () => {
+ expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled();
+ expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ });
+
+ it('renders sync status as Unavailable', () => {
+ expect(findSyncBadge().text()).toBe(s__('Deployment|Unavailable'));
+ });
+ });
+
+ describe('when flux resource path is provided', () => {
+ const namespace = 'my-namespace';
+ let fluxResourcePath;
+
+ describe('if the provided resource is a Kustomization', () => {
+ beforeEach(() => {
+ fluxResourcePath =
+ 'kustomize.toolkit.fluxcd.io/v1beta1/namespaces/my-namespace/kustomizations/app';
+
+ createWrapper({ namespace, fluxResourcePath });
+ });
+
+ it('requests the Kustomization resource status', () => {
+ expect(fluxKustomizationStatusQuery).toHaveBeenCalledWith(
+ {},
+ expect.objectContaining({
+ configuration,
+ namespace,
+ environmentName,
+ fluxResourcePath,
+ }),
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
+
+ it("doesn't request HelmRelease resource status", () => {
+ expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if the provided resource is a helmRelease', () => {
+ beforeEach(() => {
+ fluxResourcePath =
+ 'helm.toolkit.fluxcd.io/v2beta1/namespaces/my-namespace/helmreleases/app';
+
+ createWrapper({ namespace, fluxResourcePath });
+ });
+
+ it('requests the HelmRelease resource status', () => {
+ expect(fluxHelmReleaseStatusQuery).toHaveBeenCalledWith(
+ {},
+ expect.objectContaining({
+ configuration,
+ namespace,
+ environmentName,
+ fluxResourcePath,
+ }),
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
+
+ it("doesn't request Kustomization resource status", () => {
+ expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when namespace is provided', () => {
+ describe('with no Flux resources found', () => {
+ beforeEach(() => {
+ createWrapper({ namespace: 'my-namespace' });
+ });
+
+ it('requests Kustomizations', () => {
+ expect(fluxKustomizationStatusQuery).toHaveBeenCalled();
+ });
+
+ it('requests HelmReleases when there were no Kustomizations found', async () => {
+ await waitForPromises();
+
+ expect(fluxHelmReleaseStatusQuery).toHaveBeenCalled();
+ });
+
+ it('renders sync status as Unavailable when no Kustomizations and HelmReleases found', async () => {
+ await waitForPromises();
+
+ expect(findSyncBadge().text()).toBe(s__('Deployment|Unavailable'));
+ });
+ });
+
+ describe('with Flux Kustomizations available', () => {
+ const createApolloProviderWithKustomizations = ({
+ result = { status: 'True', type: 'Ready', message: '' },
+ } = {}) => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomizationStatus: jest.fn().mockReturnValue([result]),
+ fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ it("doesn't request HelmReleases when the Kustomizations were found", async () => {
+ createWrapper({
+ apolloProvider: createApolloProviderWithKustomizations(),
+ namespace: 'my-namespace',
+ });
+ await waitForPromises();
+
+ expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ status | type | badgeType
+ ${'True'} | ${'Stalled'} | ${'stalled'}
+ ${'True'} | ${'Reconciling'} | ${'reconciling'}
+ ${'True'} | ${'Ready'} | ${'reconciled'}
+ ${'False'} | ${'Ready'} | ${'failed'}
+ ${'True'} | ${'Unknown'} | ${'unknown'}
+ `(
+ 'renders $badgeType when status is $status and type is $type',
+ async ({ status, type, badgeType }) => {
+ createWrapper({
+ apolloProvider: createApolloProviderWithKustomizations({
+ result: { status, type, message: '' },
+ }),
+ namespace: 'my-namespace',
+ });
+ await waitForPromises();
+
+ const badge = SYNC_STATUS_BADGES[badgeType];
+
+ expect(findSyncBadge().text()).toBe(badge.text);
+ expect(findSyncBadge().props()).toMatchObject({
+ icon: badge.icon,
+ variant: badge.variant,
+ });
+ },
+ );
+
+ it.each`
+ status | type | message | popoverTitle | popoverText
+ ${'True'} | ${'Stalled'} | ${'stalled reason'} | ${s__('Deployment|Flux sync stalled')} | ${'stalled reason'}
+ ${'True'} | ${'Reconciling'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciling')}
+ ${'True'} | ${'Ready'} | ${''} | ${undefined} | ${s__('Deployment|Flux sync reconciled successfully')}
+ ${'False'} | ${'Ready'} | ${'failed reason'} | ${s__('Deployment|Flux sync failed')} | ${'failed reason'}
+ ${'True'} | ${'Unknown'} | ${''} | ${s__('Deployment|Flux sync status is unknown')} | ${s__('Deployment|Unable to detect state. %{linkStart}How are states detected?%{linkEnd}')}
+ `(
+ 'renders correct popover text when status is $status and type is $type',
+ async ({ status, type, message, popoverTitle, popoverText }) => {
+ createWrapper({
+ apolloProvider: createApolloProviderWithKustomizations({
+ result: { status, type, message },
+ }),
+ namespace: 'my-namespace',
+ });
+ await waitForPromises();
+
+ expect(findPopover().text()).toMatchInterpolatedText(popoverText);
+ expect(findPopover().props('title')).toBe(popoverTitle);
+ },
+ );
+ });
+
+ describe('when Flux API errored', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createApolloProviderWithErrors = () => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomizationStatus: jest.fn().mockRejectedValueOnce(error),
+ fluxHelmReleaseStatus: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ createWrapper({
+ apolloProvider: createApolloProviderWithErrors(),
+ namespace: 'my-namespace',
+ });
+ await waitForPromises();
+ });
+
+ it('renders sync badge as unavailable', () => {
+ const badge = SYNC_STATUS_BADGES.unavailable;
+
+ expect(findSyncBadge().text()).toBe(badge.text);
+ expect(findSyncBadge().props()).toMatchObject({
+ icon: badge.icon,
+ variant: badge.variant,
+ });
+ });
+
+ it('renders popover with an API error message', () => {
+ expect(findPopover().text()).toBe(error.message);
+ expect(findPopover().props('title')).toBe(
+ s__('Deployment|Flux sync status is unavailable'),
+ );
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 387bc31c9aa..bfcc4f4ebb6 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -13,8 +13,13 @@ import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import getEnvironmentClusterAgent from '~/environments/graphql/queries/environment_cluster_agent.query.graphql';
-import getEnvironmentClusterAgentWithNamespace from '~/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql';
-import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
+import getEnvironmentClusterAgentWithFluxResource from '~/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql';
+import {
+ resolvedEnvironment,
+ rolloutStatus,
+ agent,
+ fluxResourcePathMock,
+} from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
Vue.use(VueApollo);
@@ -22,7 +27,7 @@ Vue.use(VueApollo);
describe('~/environments/components/new_environment_item.vue', () => {
let wrapper;
let queryResponseHandler;
- let queryWithNamespaceResponseHandler;
+ let queryWithFluxResourceResponseHandler;
const projectPath = '/1';
@@ -33,26 +38,27 @@ describe('~/environments/components/new_environment_item.vue', () => {
id: '1',
environment: {
id: '1',
+ kubernetesNamespace: 'default',
clusterAgent,
},
},
},
};
queryResponseHandler = jest.fn().mockResolvedValue(response);
- queryWithNamespaceResponseHandler = jest.fn().mockResolvedValue({
+ queryWithFluxResourceResponseHandler = jest.fn().mockResolvedValue({
data: {
project: {
id: response.data.project.id,
environment: {
...response.data.project.environment,
- kubernetesNamespace: 'default',
+ fluxResourcePath: fluxResourcePathMock,
},
},
},
});
return createMockApollo([
[getEnvironmentClusterAgent, queryResponseHandler],
- [getEnvironmentClusterAgentWithNamespace, queryWithNamespaceResponseHandler],
+ [getEnvironmentClusterAgentWithFluxResource, queryWithFluxResourceResponseHandler],
]);
};
@@ -534,7 +540,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
describe('kubernetes overview', () => {
- it('should request agent data when the environment is visible if the feature flag is enabled', async () => {
+ it('should request agent data when the environment is visible', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
apolloProvider: createApolloProvider(agent),
@@ -548,12 +554,12 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
});
- it('should request agent data with kubernetes namespace when `kubernetesNamespaceForEnvironment` feature flag is enabled', async () => {
+ it('should request agent data with Flux resource when `fluxResourceForEnvironment` feature flag is enabled', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
provideData: {
glFeatures: {
- kubernetesNamespaceForEnvironment: true,
+ fluxResourceForEnvironment: true,
},
},
apolloProvider: createApolloProvider(agent),
@@ -561,7 +567,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
await expandCollapsedSection();
- expect(queryWithNamespaceResponseHandler).toHaveBeenCalledWith({
+ expect(queryWithFluxResourceResponseHandler).toHaveBeenCalledWith({
environmentName: resolvedEnvironment.name,
projectFullPath: projectPath,
});
@@ -578,15 +584,16 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(findKubernetesOverview().props()).toMatchObject({
clusterAgent: agent,
+ environmentName: resolvedEnvironment.name,
});
});
- it('should render with the namespace if `kubernetesNamespaceForEnvironment` feature flag is enabled and the environment has an agent associated', async () => {
+ it('should render with the namespace if `fluxResourceForEnvironment` feature flag is enabled and the environment has an agent associated', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
provideData: {
glFeatures: {
- kubernetesNamespaceForEnvironment: true,
+ fluxResourceForEnvironment: true,
},
},
apolloProvider: createApolloProvider(agent),
@@ -595,9 +602,11 @@ describe('~/environments/components/new_environment_item.vue', () => {
await expandCollapsedSection();
await waitForPromises();
- expect(findKubernetesOverview().props()).toMatchObject({
+ expect(findKubernetesOverview().props()).toEqual({
clusterAgent: agent,
+ environmentName: resolvedEnvironment.name,
namespace: 'default',
+ fluxResourcePath: fluxResourcePathMock,
});
});