diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md index 019032c8b98907296871604b1d380cf2aaec2bf1..817f22debbce841ddda4801434461b5e8c6ecd20 100644 --- a/doc/administration/audit_event_streaming.md +++ b/doc/administration/audit_event_streaming.md @@ -30,7 +30,7 @@ Event streaming destinations receive **all** audit event data, which could inclu Users with at least the Owner role for a group can add event streaming destinations for it: 1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Security & Compliance > Audit events** +1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the main area, select **Streams** tab. - When the destination list is empty, select **Add stream** to show the section for adding destinations. - When the destination list is not empty, select **{plus}** to show the section for adding destinations. @@ -77,7 +77,7 @@ Users with at least the Owner role for a group can list event streaming destinat Users with at least the Owner role for a group can list event streaming destinations: 1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Security & Compliance > Audit events** +1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the main area, select **Streams** tab. ### Use the API @@ -116,7 +116,7 @@ When the last destination is successfully deleted, event streaming is disabled f Users with at least the Owner role for a group can delete event streaming destinations. 1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Security & Compliance > Audit events** +1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the main area, select **Streams** tab. 1. Select **{remove}** at the right side of each item. @@ -185,7 +185,7 @@ not available. The UI for this feature is not ready for production use. Users with at least the Owner role for a group can add event streaming destinations and custom HTTP headers for it: 1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Security & Compliance > Audit events** +1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the main area, select **Streams** tab. - When the destination list is empty, select **Add stream** to show the section for adding destinations. - When the destination list is not empty, select **{plus}** to show the section for adding destinations. @@ -221,7 +221,7 @@ mutation { ### Deleting custom HTTP headers Group owners can remove a HTTP header using the GraphQL `auditEventsStreamingHeadersDestroy` mutation. You can retrieve the header ID -by [listing all the custom headers](#list-all-custom-headers-with-the-api) on the group. +by [listing all the custom headers](#list-all-custom-headers) on the group. ```graphql mutation { @@ -233,7 +233,11 @@ mutation { The header is deleted if the returned `errors` object is empty. -### List all custom headers with the API +### List all custom headers + +List all custom HTTP headers with the API or GitLab UI. + +#### Use the API You can list all custom headers for a top-level group as well as their value and ID using the GraphQL `externalAuditEventDestinations` query. The ID value returned by this query is what you need to pass to the `deletion` mutation. @@ -259,6 +263,22 @@ query { } ``` +#### Use the GitLab UI + +FLAG: +On self-managed GitLab, by default the UI for this feature is not available. To make it available per group, ask an administrator to +[enable the feature flag](../administration/feature_flags.md) named `custom_headers_streaming_audit_events_ui`. On GitLab.com, the UI for this feature is +not available. The UI for this feature is not ready for production use. + +Users with at least the Owner role for a group can add event streaming destinations and custom HTTP headers for it: + +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Security & Compliance > Audit events**. +1. On the main area, select **Streams** tab. +1. Select **{pencil}** at the right side of an item. +1. A read-only view of the items custom headers is shown. To track progress on adding editing functionality, see the [relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925). +1. Select **Cancel** to close the read-only view. + ## Verify event authenticity > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345424) in GitLab 14.8. diff --git a/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue b/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue index 973a8f657dda1be450da02505c8c074e507a59e1..2aa588f9d3f366eca7ab19f11bfe8055f090ab66 100644 --- a/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue +++ b/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue @@ -100,7 +100,7 @@ export default { {{ destinationsCount }} -
+
@@ -111,6 +111,7 @@ export default { :key="item.id" :item="item" @delete="refreshDestinations" + @updated="onAddedDestination" />
diff --git a/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue b/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue index ac88d5ed7a779918432ce294cffb66f1d0a7cf22..8a32e1f98d342480d852aae0204c1da3d4c5bc14 100644 --- a/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue +++ b/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue @@ -9,6 +9,7 @@ import { GlSprintf, GlTableLite, } from '@gitlab/ui'; +import { isEmpty } from 'lodash'; import * as Sentry from '@sentry/browser'; import { thWidthPercent } from '~/lib/utils/table_utility'; import externalAuditEventDestinationCreate from '../../graphql/create_external_destination.mutation.graphql'; @@ -19,6 +20,7 @@ import { createBlankHeader, } from '../../constants'; import deleteExternalDestination from '../../graphql/delete_external_destination.mutation.graphql'; +import { mapItemHeadersToFormData } from '../../utils'; const { CREATING_ERROR } = AUDIT_STREAMS_NETWORK_ERRORS; @@ -37,6 +39,13 @@ export default { GlTableLite, }, inject: ['groupPath', 'showStreamsHeaders', 'maxHeaders'], + props: { + item: { + type: Object, + required: false, + default: () => ({}), + }, + }, data() { return { destinationUrl: '', @@ -63,8 +72,37 @@ export default { return this.headers.some((header) => this.isHeaderFilled(header)); }, isSubmitButtonDisabled() { - return !this.destinationUrl || this.hasHeaderValidationErrors || this.hasMissingKeyValuePairs; + return ( + this.isEditing || + !this.destinationUrl || + this.hasHeaderValidationErrors || + this.hasMissingKeyValuePairs + ); + }, + isEditing() { + return !isEmpty(this.item); + }, + addButtonName() { + return this.isEditing + ? ADD_STREAM_EDITOR_I18N.SAVE_BUTTON_NAME + : ADD_STREAM_EDITOR_I18N.ADD_BUTTON_NAME; }, + addButtonText() { + return this.isEditing + ? ADD_STREAM_EDITOR_I18N.SAVE_BUTTON_TEXT + : ADD_STREAM_EDITOR_I18N.ADD_BUTTON_TEXT; + }, + }, + mounted() { + const existingHeaders = mapItemHeadersToFormData(this.item, { disabled: true }); + + if (existingHeaders.length > 0) { + this.headers = existingHeaders; + } else if (this.isEditing) { + this.$set(this.headers, 0, { ...this.headers[0], disabled: true }); + } + + this.destinationUrl = this.item.destinationUrl; }, methods: { clearError(index) { @@ -237,7 +275,7 @@ export default { { key: 'active', label: ADD_STREAM_EDITOR_I18N.TABLE_COLUMN_ACTIVE_LABEL, - thClass: `${thClasses} ${thWidthPercent(5)}`, + thClass: `${thClasses} ${thWidthPercent(10)}`, tdClass: tdClasses, }, { @@ -251,7 +289,7 @@ export default { diff --git a/ee/app/assets/javascripts/audit_events/constants.js b/ee/app/assets/javascripts/audit_events/constants.js index 481ac6ab7506c524647962e2c639c6bce0cbe80c..2c0cffdc43fd32ee1153217ffe3345a0afd5e168 100644 --- a/ee/app/assets/javascripts/audit_events/constants.js +++ b/ee/app/assets/javascripts/audit_events/constants.js @@ -75,7 +75,9 @@ export const STREAM_COUNT_ICON_ALT = s__('AuditStreams|Stream count icon'); export const STREAM_ITEMS_I18N = { VERIFICATION_TOKEN_TOOLTIP: s__('AuditStreams|Verification token'), + EDIT_BUTTON_LABEL: s__('AuditStreams|Edit %{link}'), DELETE_BUTTON_LABEL: s__('AuditStreams|Delete %{link}'), + EDIT_BUTTON_TOOLTIP: __('Edit'), DELETE_BUTTON_TOOLTIP: __('Delete'), }; @@ -96,6 +98,8 @@ export const ADD_STREAM_EDITOR_I18N = { MAXIMUM_HEADERS_TEXT: s__('AuditStreams|Maximum of %{number} HTTP headers has been reached.'), ADD_BUTTON_TEXT: __('Add'), ADD_BUTTON_NAME: s__('AuditStreams|Add external stream destination'), + SAVE_BUTTON_TEXT: __('Save'), + SAVE_BUTTON_NAME: s__('AuditStreams|Save external stream destination'), CANCEL_BUTTON_TEXT: __('Cancel'), CANCEL_BUTTON_NAME: s__('AuditStreams|Cancel editing'), }; diff --git a/ee/app/assets/javascripts/audit_events/graphql/get_external_destinations.query.graphql b/ee/app/assets/javascripts/audit_events/graphql/get_external_destinations.query.graphql index 0dc51810202e64697ee09dc3ad2b76e3dbdfdd15..9e6322130d29d0c4ea91a06e860567bf7d3959e9 100644 --- a/ee/app/assets/javascripts/audit_events/graphql/get_external_destinations.query.graphql +++ b/ee/app/assets/javascripts/audit_events/graphql/get_external_destinations.query.graphql @@ -6,6 +6,13 @@ query getExternalDestinations($fullPath: ID!) { id destinationUrl verificationToken + headers { + nodes { + key + value + id + } + } } } } diff --git a/ee/app/assets/javascripts/audit_events/utils.js b/ee/app/assets/javascripts/audit_events/utils.js index c232724b7b02e847681c1beae63ad5638f47a404..68113a6065201e1a2d8a03bcd5f3e67bb8407d89 100644 --- a/ee/app/assets/javascripts/audit_events/utils.js +++ b/ee/app/assets/javascripts/audit_events/utils.js @@ -1,5 +1,11 @@ import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; -import { AVAILABLE_TOKEN_TYPES, AUDIT_FILTER_CONFIGS, ENTITY_TYPES } from './constants'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { + AVAILABLE_TOKEN_TYPES, + AUDIT_FILTER_CONFIGS, + ENTITY_TYPES, + createBlankHeader, +} from './constants'; import { parseUsername, displayUsername } from './token_utils'; export const getTypeFromEntityType = (entityType) => { @@ -58,3 +64,21 @@ export const createAuditEventSearchQuery = ({ filterValue, startDate, endDate, s return params; }; + +export const mapItemHeadersToFormData = (item, settings = {}) => { + const headers = item?.headers?.nodes || []; + + return ( + headers + .map(({ id, key, value }) => ({ + ...createBlankHeader(), + id, + name: key, + value, + ...settings, + })) + // Sort the headers so they appear in the order they were created + // The GraphQL endpoint returns them in the reverse order of this + .sort((a, b) => getIdFromGraphQLId(a.id) - getIdFromGraphQLId(b.id)) + ); +}; diff --git a/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap b/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap index f3fe2508270e10a6003e4c24031dba6d739d1aa6..a39ef6b397e7bc103cf2579ed17f936650173ccc 100644 --- a/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap +++ b/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap @@ -1,18 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StreamItem render should render correctly 1`] = ` +exports[`StreamItem editing should render correctly 1`] = `
  • -
    https://api.gitlab.com -
    + -
    +
    + +
    + +
    +
  • +`; + +exports[`StreamItem render should render correctly 1`] = ` +
  • +
    + + https://api.gitlab.com + + + + + Verification token: + + + id5hzCbERzSkQ82tAs16tH5Y + + + +
    + + + + + + + +
    + +
  • `; diff --git a/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js b/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js index a0eadcf9f1ec7bc17de6719c8d14e7de1c06b7bb..5797d014816f091aa7134ce8aa76019e8d561748 100644 --- a/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js +++ b/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js @@ -20,6 +20,8 @@ import { destinationDeleteMutationPopulator, destinationHeaderCreateMutationPopulator, groupPath, + mockExternalDestinations, + mockExternalDestinationHeader, } from '../../mock_data'; const localVue = createLocalVue(); @@ -33,6 +35,7 @@ describe('StreamDestinationEditor', () => { const createComponent = ( mountFn = shallowMountExtended, provide = {}, + propsData = {}, apolloHandlers = [ [ externalAuditEventDestinationCreate, @@ -48,6 +51,7 @@ describe('StreamDestinationEditor', () => { maxHeaders, ...provide, }, + propsData, apolloProvider: mockApollo, localVue, }); @@ -100,6 +104,7 @@ describe('StreamDestinationEditor', () => { it('should render the destination URL input', () => { expect(findDestinationUrlFormGroup().exists()).toBe(true); + expect(findDestinationUrl().props('disabled')).toBe(undefined); expect(findDestinationUrl().attributes('placeholder')).toBe( ADD_STREAM_EDITOR_I18N.DESTINATION_URL_PLACEHOLDER, ); @@ -139,7 +144,7 @@ describe('StreamDestinationEditor', () => { describe('add destination event without headers', () => { it('should emit add event after destination added', async () => { - createComponent(shallowMountExtended, {}, [ + createComponent(shallowMountExtended, {}, {}, [ [ externalAuditEventDestinationCreate, jest.fn().mockResolvedValue(destinationCreateMutationPopulator()), @@ -156,7 +161,7 @@ describe('StreamDestinationEditor', () => { it('should not emit add destination event and reports error when server returns error', async () => { const errorMsg = 'Destination hosts limit exceeded'; - createComponent(shallowMountExtended, {}, [ + createComponent(shallowMountExtended, {}, {}, [ [ externalAuditEventDestinationCreate, jest.fn().mockResolvedValue(destinationCreateMutationPopulator([errorMsg])), @@ -175,7 +180,7 @@ describe('StreamDestinationEditor', () => { it('should not emit add destination event and reports error when network error occurs', async () => { const sentryError = new Error('Network error'); const sentryCaptureExceptionSpy = jest.spyOn(Sentry, 'captureException'); - createComponent(shallowMountExtended, {}, [ + createComponent(shallowMountExtended, {}, {}, [ [externalAuditEventDestinationCreate, jest.fn().mockRejectedValue(sentryError)], ]); @@ -192,7 +197,7 @@ describe('StreamDestinationEditor', () => { describe('add destination event with headers', () => { it('should emit add event after destination and headers are added', async () => { - createComponent(mountExtended, { showStreamsHeaders: true }, [ + createComponent(mountExtended, { showStreamsHeaders: true }, {}, [ [ externalAuditEventDestinationCreate, jest.fn().mockResolvedValue(destinationCreateMutationPopulator()), @@ -218,7 +223,7 @@ describe('StreamDestinationEditor', () => { .fn() .mockResolvedValue(destinationHeaderCreateMutationPopulator()); - createComponent(mountExtended, { showStreamsHeaders: true }, [ + createComponent(mountExtended, { showStreamsHeaders: true }, {}, [ [ externalAuditEventDestinationCreate, jest.fn().mockResolvedValue(destinationCreateMutationPopulator()), @@ -244,7 +249,7 @@ describe('StreamDestinationEditor', () => { it('should not emit add destination event and reports error when server returns error while adding headers', async () => { const errorMsg = 'Destination hosts limit exceeded'; - createComponent(mountExtended, { showStreamsHeaders: true }, [ + createComponent(mountExtended, { showStreamsHeaders: true }, {}, [ [ externalAuditEventDestinationCreate, jest.fn().mockResolvedValue(destinationCreateMutationPopulator()), @@ -276,7 +281,7 @@ describe('StreamDestinationEditor', () => { it('should not emit add destination event and reports error when network error occurs while adding headers', async () => { const sentryError = new Error('Network error'); const sentryCaptureExceptionSpy = jest.spyOn(Sentry, 'captureException'); - createComponent(mountExtended, { showStreamsHeaders: true }, [ + createComponent(mountExtended, { showStreamsHeaders: true }, {}, [ [ externalAuditEventDestinationCreate, jest.fn().mockResolvedValue(destinationCreateMutationPopulator()), @@ -425,4 +430,39 @@ describe('StreamDestinationEditor', () => { ); }); }); + + describe('when editing an existing destination', () => { + const item = { + ...mockExternalDestinations[0], + headers: { nodes: [mockExternalDestinationHeader(), mockExternalDestinationHeader()] }, + }; + + beforeEach(() => { + createComponent(mountExtended, { showStreamsHeaders: true }, { item }); + }); + + it('disables the destination URL field', () => { + expect(findDestinationUrl().element.value).toBe(mockExternalDestinations[0].destinationUrl); + expect(findDestinationUrl().attributes('disabled')).toBe('disabled'); + }); + + it('disables the save button', () => { + expect(findAddBtn().attributes('disabled')).toBe('disabled'); + }); + + it('changes the save button text', () => { + expect(findAddBtn().attributes('name')).toBe(ADD_STREAM_EDITOR_I18N.SAVE_BUTTON_NAME); + expect(findAddBtn().text()).toBe(ADD_STREAM_EDITOR_I18N.SAVE_BUTTON_TEXT); + }); + + it.each([0, 1])('disables the header inputs for row %i', (i) => { + expect(findHeaderNameInput(i).attributes('disabled')).toBe('disabled'); + expect(findHeaderValueInput(i).attributes('disabled')).toBe('disabled'); + }); + + it.each([0, 1])('sets the header input values for row %i', (i) => { + expect(findHeaderNameInput(i).element.value).toBe(item.headers.nodes[i].key); + expect(findHeaderValueInput(i).element.value).toBe(item.headers.nodes[i].value); + }); + }); }); diff --git a/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js b/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js index e8c11faeee2a2c84fcc96b7699a3fac4c472e352..e0f7e7a1eb421915143b3c38541216d1503a2316 100644 --- a/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js +++ b/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js @@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import deleteExternalDestination from 'ee/audit_events/graphql/delete_external_destination.mutation.graphql'; import { AUDIT_STREAMS_NETWORK_ERRORS } from 'ee/audit_events/constants'; import StreamItem from 'ee/audit_events/components/stream/stream_item.vue'; +import StreamDestinationEditor from 'ee/audit_events/components/stream/stream_destination_editor.vue'; import { destinationDeleteMutationPopulator, mockExternalDestinations } from '../../mock_data'; jest.mock('~/flash'); @@ -37,7 +38,9 @@ describe('StreamItem', () => { }); }; - const findButton = () => wrapper.findComponent(GlButton); + const findEditButton = () => wrapper.findByTestId('edit-btn'); + const findDeleteButton = () => wrapper.findByTestId('delete-btn'); + const findEditor = () => wrapper.findComponent(StreamDestinationEditor); afterEach(() => { wrapper.destroy(); @@ -45,17 +48,23 @@ describe('StreamItem', () => { }); describe('render', () => { - it('should render correctly', () => { + beforeEach(() => { createComponent(); + }); + it('should render correctly', () => { expect(wrapper.element).toMatchSnapshot(); }); + + it('should not show the editor', () => { + expect(findEditor().exists()).toBe(false); + }); }); describe('events', () => { it('should emit delete with item id', async () => { createComponent(); - const button = findButton(); + const button = findDeleteButton(); await button.trigger('click'); expect(button.props('loading')).toBe(true); @@ -73,7 +82,7 @@ describe('StreamItem', () => { .fn() .mockResolvedValue(destinationDeleteMutationPopulator([errorMsg])); createComponent(deleteExternalDestinationErrorSpy); - const button = findButton(); + const button = findDeleteButton(); await button.trigger('click'); expect(button.props('loading')).toBe(true); @@ -90,7 +99,7 @@ describe('StreamItem', () => { it('should not emit delete when network error occurs', async () => { const error = new Error('Network error'); createComponent(jest.fn().mockRejectedValue(error)); - const button = findButton(); + const button = findDeleteButton(); await button.trigger('click'); expect(button.props('loading')).toBe(true); @@ -106,4 +115,35 @@ describe('StreamItem', () => { }); }); }); + + describe('editing', () => { + beforeEach(() => { + createComponent(); + findEditButton().trigger('click'); + }); + + it('should render correctly', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('should pass the item to the editor', () => { + expect(findEditor().exists()).toBe(true); + expect(findEditor().props('item')).toStrictEqual(mockExternalDestinations[0]); + }); + + it('should emit the updated event when the editor fires its added event', async () => { + findEditor().vm.$emit('added'); + await waitForPromises(); + + expect(wrapper.emitted('updated')).toBeDefined(); + expect(findEditor().exists()).toBe(false); + }); + + it('should close the editor when the editor fires its cancel event', async () => { + findEditor().vm.$emit('cancel'); + await waitForPromises(); + + expect(findEditor().exists()).toBe(false); + }); + }); }); diff --git a/ee/spec/frontend/audit_events/mock_data.js b/ee/spec/frontend/audit_events/mock_data.js index 04d27827cec1970eb3b30b67ff6d7a2a2383caf5..809a1fea43f481ad6339894482caf757436ee21c 100644 --- a/ee/spec/frontend/audit_events/mock_data.js +++ b/ee/spec/frontend/audit_events/mock_data.js @@ -36,17 +36,27 @@ export default () => [ ]; export const mockExternalDestinationUrl = 'https://api.gitlab.com'; - +export const mockExternalDestinationHeader = () => ({ + id: uniqueId('gid://gitlab/AuditEvents::Streaming::Header/'), + key: uniqueId('header-key-'), + value: uniqueId('header-value-'), +}); export const mockExternalDestinations = [ { id: 'test_id1', destinationUrl: mockExternalDestinationUrl, verificationToken: 'id5hzCbERzSkQ82tAs16tH5Y', + headers: { + nodes: [], + }, }, { id: 'test_id2', destinationUrl: 'https://apiv2.gitlab.com', verificationToken: 'JsSQtg86au6buRtX9j98sYa8', + headers: { + nodes: [], + }, }, ]; diff --git a/ee/spec/frontend/audit_events/utils_spec.js b/ee/spec/frontend/audit_events/utils_spec.js index 710a2d038759f8d08aa2abd19b879135a4799c27..35186b5f03f4107222905072a3696e9ff5aa890f 100644 --- a/ee/spec/frontend/audit_events/utils_spec.js +++ b/ee/spec/frontend/audit_events/utils_spec.js @@ -3,7 +3,9 @@ import { getEntityTypeFromType, parseAuditEventSearchQuery, createAuditEventSearchQuery, + mapItemHeadersToFormData, } from 'ee/audit_events/utils'; +import { mockExternalDestinationHeader } from './mock_data'; describe('Audit Event Utils', () => { describe('getTypeFromEntityType', () => { @@ -74,4 +76,99 @@ describe('Audit Event Utils', () => { }, ); }); + + describe('mapItemHeadersToFormData', () => { + const header1 = mockExternalDestinationHeader(); + const header2 = mockExternalDestinationHeader(); + const header3 = mockExternalDestinationHeader(); + + it.each([{}, { headers: {} }, { headers: { nodes: [] } }])( + 'returns an empty array when there are no headers', + (item) => { + expect(mapItemHeadersToFormData(item)).toEqual([]); + }, + ); + + it('returns the formatted headers', () => { + expect(mapItemHeadersToFormData({ headers: { nodes: [header1, header2] } })).toStrictEqual([ + { + id: header1.id, + name: header1.key, + value: header1.value, + active: true, + disabled: false, + deletionDisabled: true, + validationErrors: { name: '' }, + }, + { + id: header2.id, + name: header2.key, + value: header2.value, + active: true, + disabled: false, + deletionDisabled: true, + validationErrors: { name: '' }, + }, + ]); + }); + + it('applies the settings to each header when given', () => { + expect( + mapItemHeadersToFormData({ headers: { nodes: [header1, header2] } }, { disabled: true }), + ).toStrictEqual([ + { + id: header1.id, + name: header1.key, + value: header1.value, + active: true, + disabled: true, + deletionDisabled: true, + validationErrors: { name: '' }, + }, + { + id: header2.id, + name: header2.key, + value: header2.value, + active: true, + disabled: true, + deletionDisabled: true, + validationErrors: { name: '' }, + }, + ]); + }); + + it('sorts the headers by their ID', () => { + expect( + mapItemHeadersToFormData({ headers: { nodes: [header3, header1, header2] } }), + ).toStrictEqual([ + { + id: header1.id, + name: header1.key, + value: header1.value, + active: true, + disabled: false, + deletionDisabled: true, + validationErrors: { name: '' }, + }, + { + id: header2.id, + name: header2.key, + value: header2.value, + active: true, + disabled: false, + deletionDisabled: true, + validationErrors: { name: '' }, + }, + { + id: header3.id, + name: header3.key, + value: header3.value, + active: true, + disabled: false, + deletionDisabled: true, + validationErrors: { name: '' }, + }, + ]); + }); + }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 641aad5c9c38aa0d302718c8f9b1afecfbb2d464..58a2b53aa4e01f6eb4570115b54d54aaff433b11 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5334,12 +5334,18 @@ msgstr "" msgid "AuditStreams|Destinations receive all audit event data" msgstr "" +msgid "AuditStreams|Edit %{link}" +msgstr "" + msgid "AuditStreams|Header" msgstr "" msgid "AuditStreams|Maximum of %{number} HTTP headers has been reached." msgstr "" +msgid "AuditStreams|Save external stream destination" +msgstr "" + msgid "AuditStreams|Setup streaming for audit events" msgstr ""