diff options
Diffstat (limited to 'spec/frontend/security_configuration/components/training_provider_list_spec.js')
-rw-r--r-- | spec/frontend/security_configuration/components/training_provider_list_spec.js | 200 |
1 files changed, 172 insertions, 28 deletions
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 578248e696f..18c9ada6bde 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -1,14 +1,26 @@ +import * as Sentry from '@sentry/browser'; import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import { + TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, + TRACK_TOGGLE_TRAINING_PROVIDER_LABEL, +} from '~/security_configuration/constants'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; +import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql'; +import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import waitForPromises from 'helpers/wait_for_promises'; import { + dismissUserCalloutResponse, + dismissUserCalloutErrorResponse, securityTrainingProviders, - createMockResolvers, + securityTrainingProvidersResponse, + updateSecurityTrainingProvidersResponse, + updateSecurityTrainingProvidersErrorResponse, testProjectPath, textProviderIds, } from '../mock_data'; @@ -19,14 +31,28 @@ describe('TrainingProviderList component', () => { let wrapper; let apolloProvider; - const createApolloProvider = ({ resolvers } = {}) => { - apolloProvider = createMockApollo([], createMockResolvers({ resolvers })); + const createApolloProvider = ({ handlers = [] } = {}) => { + const defaultHandlers = [ + [ + securityTrainingProvidersQuery, + jest.fn().mockResolvedValue(securityTrainingProvidersResponse), + ], + [ + configureSecurityTrainingProvidersMutation, + jest.fn().mockResolvedValue(updateSecurityTrainingProvidersResponse), + ], + ]; + + // make sure we don't have any duplicate handlers to avoid 'Request handler already defined for query` errors + const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])]; + + apolloProvider = createMockApollo(mergedHandlers); }; const createComponent = () => { wrapper = shallowMount(TrainingProviderList, { provide: { - projectPath: testProjectPath, + projectFullPath: testProjectPath, }, apolloProvider, }); @@ -42,27 +68,49 @@ describe('TrainingProviderList component', () => { const findLoader = () => wrapper.findComponent(GlSkeletonLoader); const findErrorAlert = () => wrapper.findComponent(GlAlert); - const toggleFirstProvider = () => findFirstToggle().vm.$emit('change'); + const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]); afterEach(() => { wrapper.destroy(); apolloProvider = null; }); - describe('with a successful response', () => { + describe('when loading', () => { beforeEach(() => { - createApolloProvider(); + const pendingHandler = () => new Promise(() => {}); + + createApolloProvider({ + handlers: [[securityTrainingProvidersQuery, pendingHandler]], + }); createComponent(); }); - describe('when loading', () => { - it('shows the loader', () => { - expect(findLoader().exists()).toBe(true); - }); + it('shows the loader', () => { + expect(findLoader().exists()).toBe(true); + }); - it('does not show the cards', () => { - expect(findCards().exists()).toBe(false); + it('does not show the cards', () => { + expect(findCards().exists()).toBe(false); + }); + }); + + describe('with a successful response', () => { + beforeEach(() => { + createApolloProvider({ + handlers: [ + [dismissUserCalloutMutation, jest.fn().mockResolvedValue(dismissUserCalloutResponse)], + ], + resolvers: { + Mutation: { + configureSecurityTrainingProviders: () => ({ + errors: [], + securityTrainingProviders: [], + }), + }, + }, }); + + createComponent(); }); describe('basic structure', () => { @@ -104,9 +152,9 @@ describe('TrainingProviderList component', () => { beforeEach(async () => { jest.spyOn(apolloProvider.defaultClient, 'mutate'); - await waitForMutationToBeLoaded(); + await waitForQueryToBeLoaded(); - toggleFirstProvider(); + await toggleFirstProvider(); }); it.each` @@ -124,10 +172,78 @@ describe('TrainingProviderList component', () => { expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith( expect.objectContaining({ mutation: configureSecurityTrainingProvidersMutation, - variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } }, + variables: { + input: { + providerId: textProviderIds[0], + isEnabled: true, + isPrimary: false, + projectPath: testProjectPath, + }, + }, }), ); }); + + it('dismisses the callout when the feature gets first enabled', async () => { + // wait for configuration update mutation to complete + await waitForMutationToBeLoaded(); + + // both the config and dismiss mutations have been called + expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledTimes(2); + expect(apolloProvider.defaultClient.mutate).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + mutation: dismissUserCalloutMutation, + variables: { + input: { + featureName: 'security_training_feature_promotion', + }, + }, + }), + ); + + toggleFirstProvider(); + await waitForMutationToBeLoaded(); + + // the config mutation has been called again but not the dismiss mutation + expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledTimes(3); + expect(apolloProvider.defaultClient.mutate).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + mutation: configureSecurityTrainingProvidersMutation, + }), + ); + }); + }); + + describe('metrics', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks when a provider gets toggled', () => { + expect(trackingSpy).not.toHaveBeenCalled(); + + toggleFirstProvider(); + + // Note: Ideally we also want to test that the tracking event is called correctly when a + // provider gets disabled, but that's a bit tricky to do with the current implementation + // Once https://gitlab.com/gitlab-org/gitlab/-/issues/348985 and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79492 + // are merged this will be much easer to do and should be tackled then. + expect(trackingSpy).toHaveBeenCalledWith(undefined, TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, { + property: securityTrainingProviders[0].id, + label: TRACK_TOGGLE_TRAINING_PROVIDER_LABEL, + extra: { + providerIsEnabled: true, + }, + }); + }); }); }); @@ -142,11 +258,7 @@ describe('TrainingProviderList component', () => { describe('when fetching training providers', () => { beforeEach(async () => { createApolloProvider({ - resolvers: { - Query: { - securityTrainingProviders: jest.fn().mockReturnValue(new Error()), - }, - }, + handlers: [[securityTrainingProvidersQuery, jest.fn().mockRejectedValue()]], }); createComponent(); @@ -165,10 +277,43 @@ describe('TrainingProviderList component', () => { describe('when storing training provider configurations', () => { beforeEach(async () => { createApolloProvider({ + handlers: [ + [ + configureSecurityTrainingProvidersMutation, + jest.fn().mockReturnValue(updateSecurityTrainingProvidersErrorResponse), + ], + ], + }); + createComponent(); + + await waitForQueryToBeLoaded(); + toggleFirstProvider(); + await waitForMutationToBeLoaded(); + }); + + it('shows an non-dismissible error alert', () => { + expectErrorAlertToExist(); + }); + + it('shows an error description', () => { + expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.configMutationErrorMessage); + }); + }); + + describe.each` + errorType | mutationHandler + ${'backend error'} | ${jest.fn().mockReturnValue(dismissUserCalloutErrorResponse)} + ${'network error'} | ${jest.fn().mockRejectedValue()} + `('when dismissing the callout and a "$errorType" happens', ({ mutationHandler }) => { + beforeEach(async () => { + jest.spyOn(Sentry, 'captureException').mockImplementation(); + + createApolloProvider({ + handlers: [[dismissUserCalloutMutation, mutationHandler]], resolvers: { Mutation: { configureSecurityTrainingProviders: () => ({ - errors: ['something went wrong!'], + errors: [], securityTrainingProviders: [], }), }, @@ -178,15 +323,14 @@ describe('TrainingProviderList component', () => { await waitForQueryToBeLoaded(); toggleFirstProvider(); - await waitForMutationToBeLoaded(); }); - it('shows an non-dismissible error alert', () => { - expectErrorAlertToExist(); - }); + it('logs the error to sentry', async () => { + expect(Sentry.captureException).not.toHaveBeenCalled(); - it('shows an error description', () => { - expect(findErrorAlert().text()).toBe(TrainingProviderList.i18n.configMutationErrorMessage); + await waitForMutationToBeLoaded(); + + expect(Sentry.captureException).toHaveBeenCalled(); }); }); }); |