From 48aff82709769b098321c738f3444b9bdaa694c6 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 21 Oct 2020 07:08:36 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-5-stable-ee --- spec/frontend/lib/dompurify_spec.js | 98 +++++++++++++++++++++ .../frontend/lib/utils/axios_startup_calls_spec.js | 49 ++++++++--- spec/frontend/lib/utils/datetime_utility_spec.js | 65 ++++++++++++++ spec/frontend/lib/utils/experimentation_spec.js | 20 +++++ spec/frontend/lib/utils/number_utility_spec.js | 11 +++ spec/frontend/lib/utils/text_markdown_spec.js | 99 ++++------------------ spec/frontend/lib/utils/url_utility_spec.js | 71 ++++++++++++++++ 7 files changed, 318 insertions(+), 95 deletions(-) create mode 100644 spec/frontend/lib/dompurify_spec.js create mode 100644 spec/frontend/lib/utils/experimentation_spec.js (limited to 'spec/frontend/lib') diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js new file mode 100644 index 00000000000..ee1971a4931 --- /dev/null +++ b/spec/frontend/lib/dompurify_spec.js @@ -0,0 +1,98 @@ +import { sanitize } from '~/lib/dompurify'; + +// GDK +const rootGon = { + sprite_file_icons: '/assets/icons-123a.svg', + sprite_icons: '/assets/icons-456b.svg', +}; + +// Production +const absoluteGon = { + sprite_file_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-123a.svg`, + sprite_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-456b.svg`, +}; + +const expectedSanitized = ''; + +const safeUrls = { + root: Object.values(rootGon).map(url => `${url}#ellipsis_h`), + absolute: Object.values(absoluteGon).map(url => `${url}#ellipsis_h`), +}; + +const unsafeUrls = [ + '/an/evil/url', + '../../../evil/url', + 'https://evil.url/assets/icons-123a.svg', + 'https://evil.url/assets/icons-456b.svg', + `https://evil.url/${rootGon.sprite_icons}`, + `https://evil.url/${rootGon.sprite_file_icons}`, + `https://evil.url/${absoluteGon.sprite_icons}`, + `https://evil.url/${absoluteGon.sprite_file_icons}`, +]; + +describe('~/lib/dompurify', () => { + let originalGon; + + it('uses local configuration when given', () => { + // As dompurify uses a "Persistent Configuration", it might + // ignore config, this check verifies we respect + // https://github.com/cure53/DOMPurify#persistent-configuration + expect(sanitize('
', { ALLOWED_TAGS: [] })).toBe(''); + expect(sanitize('', { ALLOWED_TAGS: [] })).toBe(''); + }); + + describe.each` + type | gon + ${'root'} | ${rootGon} + ${'absolute'} | ${absoluteGon} + `('when gon contains $type icon urls', ({ type, gon }) => { + beforeAll(() => { + originalGon = window.gon; + window.gon = gon; + }); + + afterAll(() => { + window.gon = originalGon; + }); + + it('allows no href attrs', () => { + const htmlHref = ``; + expect(sanitize(htmlHref)).toBe(htmlHref); + }); + + it.each(safeUrls[type])('allows safe URL %s', url => { + const htmlHref = ``; + expect(sanitize(htmlHref)).toBe(htmlHref); + + const htmlXlink = ``; + expect(sanitize(htmlXlink)).toBe(htmlXlink); + }); + + it.each(unsafeUrls)('sanitizes unsafe URL %s', url => { + const htmlHref = ``; + const htmlXlink = ``; + + expect(sanitize(htmlHref)).toBe(expectedSanitized); + expect(sanitize(htmlXlink)).toBe(expectedSanitized); + }); + }); + + describe('when gon does not contain icon urls', () => { + beforeAll(() => { + originalGon = window.gon; + window.gon = {}; + }); + + afterAll(() => { + window.gon = originalGon; + }); + + it.each([...safeUrls.root, ...safeUrls.absolute, ...unsafeUrls])('sanitizes URL %s', url => { + const htmlHref = ``; + const htmlXlink = ``; + + expect(sanitize(htmlHref)).toBe(expectedSanitized); + expect(sanitize(htmlXlink)).toBe(expectedSanitized); + }); + }); +}); diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js index e804cae7914..e12bf725560 100644 --- a/spec/frontend/lib/utils/axios_startup_calls_spec.js +++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js @@ -111,21 +111,44 @@ describe('setupAxiosStartupCalls', () => { }); }); - it('removes GitLab Base URL from startup call', async () => { - const oldGon = window.gon; - window.gon = { gitlab_url: 'https://example.org/gitlab' }; - - window.gl.startup_calls = { - '/startup': { - fetchCall: mockFetchCall(200), - }, - }; - setupAxiosStartupCalls(axios); + describe('startup call', () => { + let oldGon; + + beforeEach(() => { + oldGon = window.gon; + window.gon = { gitlab_url: 'https://example.org/gitlab' }; + }); + + afterEach(() => { + window.gon = oldGon; + }); - const { data } = await axios.get('https://example.org/gitlab/startup'); + it('removes GitLab Base URL from startup call', async () => { + window.gl.startup_calls = { + '/startup': { + fetchCall: mockFetchCall(200), + }, + }; + setupAxiosStartupCalls(axios); - expect(data).toEqual(STARTUP_JS_RESPONSE); + const { data } = await axios.get('https://example.org/gitlab/startup'); - window.gon = oldGon; + expect(data).toEqual(STARTUP_JS_RESPONSE); + }); + + it('sorts the params in the requested API url', async () => { + window.gl.startup_calls = { + '/startup?alpha=true&bravo=true': { + fetchCall: mockFetchCall(200), + }, + }; + setupAxiosStartupCalls(axios); + + // Use a full url instead of passing options = { params: { ... } } to axios.get + // to ensure the params are listed in the specified order. + const { data } = await axios.get('https://example.org/gitlab/startup?bravo=true&alpha=true'); + + expect(data).toEqual(STARTUP_JS_RESPONSE); + }); }); }); diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index 5b1fdea058b..b0b0b028761 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -69,6 +69,34 @@ describe('Date time utils', () => { }); }); + describe('formatDateAsMonth', () => { + it('should format dash cased date properly', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth(new Date('2020-06-28')); + + expect(formattedMonth).toBe('Jun'); + }); + + it('should format return the non-abbreviated month', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth(new Date('2020-07-28'), { + abbreviated: false, + }); + + expect(formattedMonth).toBe('July'); + }); + + it('should format date with slashes properly', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth(new Date('07/23/2016')); + + expect(formattedMonth).toBe('Jul'); + }); + + it('should format ISO date properly', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth('2016-07-23T00:00:00.559Z'); + + expect(formattedMonth).toBe('Jul'); + }); + }); + describe('formatDate', () => { it('should format date properly', () => { const formattedDate = datetimeUtility.formatDate(new Date('07/23/2016')); @@ -654,6 +682,20 @@ describe('differenceInSeconds', () => { }); }); +describe('differenceInMonths', () => { + const startDateTime = new Date('2019-07-17T00:00:00.000Z'); + + it.each` + startDate | endDate | expected + ${startDateTime} | ${startDateTime} | ${0} + ${startDateTime} | ${new Date('2019-12-17T12:00:00.000Z')} | ${5} + ${startDateTime} | ${new Date('2021-02-18T00:00:00.000Z')} | ${19} + ${new Date('2021-02-18T00:00:00.000Z')} | ${startDateTime} | ${-19} + `('returns $expected for $endDate - $startDate', ({ startDate, endDate, expected }) => { + expect(datetimeUtility.differenceInMonths(startDate, endDate)).toBe(expected); + }); +}); + describe('differenceInMilliseconds', () => { const startDateTime = new Date('2019-07-17T00:00:00.000Z'); @@ -667,3 +709,26 @@ describe('differenceInMilliseconds', () => { expect(datetimeUtility.differenceInMilliseconds(startDate, endDate)).toBe(expected); }); }); + +describe('dateAtFirstDayOfMonth', () => { + const date = new Date('2019-07-16T12:00:00.000Z'); + + it('returns the date at the first day of the month', () => { + const startDate = datetimeUtility.dateAtFirstDayOfMonth(date); + const expectedStartDate = new Date('2019-07-01T12:00:00.000Z'); + + expect(startDate).toStrictEqual(expectedStartDate); + }); +}); + +describe('datesMatch', () => { + const date = new Date('2019-07-17T00:00:00.000Z'); + + it.each` + date1 | date2 | expected + ${date} | ${new Date('2019-07-17T00:00:00.000Z')} | ${true} + ${date} | ${new Date('2019-07-17T12:00:00.000Z')} | ${false} + `('returns $expected for $date1 matches $date2', ({ date1, date2, expected }) => { + expect(datetimeUtility.datesMatch(date1, date2)).toBe(expected); + }); +}); diff --git a/spec/frontend/lib/utils/experimentation_spec.js b/spec/frontend/lib/utils/experimentation_spec.js new file mode 100644 index 00000000000..2c5d2f89297 --- /dev/null +++ b/spec/frontend/lib/utils/experimentation_spec.js @@ -0,0 +1,20 @@ +import * as experimentUtils from '~/lib/utils/experimentation'; + +const TEST_KEY = 'abc'; + +describe('experiment Utilities', () => { + describe('isExperimentEnabled', () => { + it.each` + experiments | value + ${{ [TEST_KEY]: true }} | ${true} + ${{ [TEST_KEY]: false }} | ${false} + ${{ def: true }} | ${false} + ${{}} | ${false} + ${null} | ${false} + `('returns correct value of $value for experiments=$experiments', ({ experiments, value }) => { + window.gon = { experiments }; + + expect(experimentUtils.isExperimentEnabled(TEST_KEY)).toEqual(value); + }); + }); +}); diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js index 2f8f1092612..f600f2bcd55 100644 --- a/spec/frontend/lib/utils/number_utility_spec.js +++ b/spec/frontend/lib/utils/number_utility_spec.js @@ -1,5 +1,6 @@ import { formatRelevantDigits, + bytesToKB, bytesToKiB, bytesToMiB, bytesToGiB, @@ -54,6 +55,16 @@ describe('Number Utils', () => { }); }); + describe('bytesToKB', () => { + it.each` + input | output + ${1000} | ${1} + ${1024} | ${1.024} + `('returns $output KB for $input bytes', ({ input, output }) => { + expect(bytesToKB(input)).toBe(output); + }); + }); + describe('bytesToKiB', () => { it('calculates KiB for the given bytes', () => { expect(bytesToKiB(1024)).toEqual(1); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 1aaae80dcdf..43de195c702 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -13,6 +13,23 @@ describe('init markdown', () => { textArea.parentNode.removeChild(textArea); }); + describe('insertMarkdownText', () => { + it('will not error if selected text is a number', () => { + const selected = 2; + + insertMarkdownText({ + textArea, + text: '', + tag: '', + blockTag: null, + selected, + wrap: false, + }); + + expect(textArea.value).toBe(selected.toString()); + }); + }); + describe('textArea', () => { describe('without selection', () => { it('inserts the tag on an empty line', () => { @@ -251,88 +268,10 @@ describe('init markdown', () => { }); }); - describe('Ace Editor', () => { - let editor; - - beforeEach(() => { - editor = { - getSelectionRange: jest.fn().mockReturnValue({ - start: 0, - end: 0, - }), - getValue: jest.fn().mockReturnValue('this is text \n in two lines'), - insert: jest.fn(), - navigateLeft: jest.fn(), - }; - }); - - it('uses ace editor insert text when editor is passed in', () => { - insertMarkdownText({ - text: editor.getValue, - tag: '*', - blockTag: null, - selected: '', - wrap: false, - editor, - }); - - expect(editor.insert).toHaveBeenCalled(); - }); - - it('adds block tags on line above and below selection', () => { - const selected = 'this text \n is multiple \n lines'; - const text = `before \n ${selected} \n after`; - - insertMarkdownText({ - text, - tag: '', - blockTag: '***', - selected, - wrap: true, - editor, - }); - - expect(editor.insert).toHaveBeenCalledWith(`***\n${selected}\n***`); - }); - - it('uses ace editor to navigate back tag length when nothing is selected', () => { - insertMarkdownText({ - text: editor.getValue, - tag: '*', - blockTag: null, - selected: '', - wrap: true, - editor, - }); - - expect(editor.navigateLeft).toHaveBeenCalledWith(1); - }); - - it('ace editor does not navigate back when there is selected text', () => { - insertMarkdownText({ - text: editor.getValue, - tag: '*', - blockTag: null, - selected: 'foobar', - wrap: true, - editor, - }); - - expect(editor.navigateLeft).not.toHaveBeenCalled(); - }); - }); - describe('Editor Lite', () => { let editor; - let origGon; beforeEach(() => { - origGon = window.gon; - window.gon = { - features: { - monacoBlobs: true, - }, - }; editor = { getSelection: jest.fn().mockReturnValue({ startLineNumber: 1, @@ -347,10 +286,6 @@ describe('init markdown', () => { }; }); - afterEach(() => { - window.gon = origGon; - }); - it('replaces selected text', () => { insertMarkdownText({ text: editor.getValue, diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 869ae274a3f..0f9290e36b5 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -509,6 +509,20 @@ describe('URL utility', () => { }); }); + describe('isBlobUrl', () => { + it.each` + url | valid + ${undefined} | ${false} + ${'blob:http://gitlab.com/abcd'} | ${true} + ${'data:image/png;base64,abcdef'} | ${false} + ${'notaurl'} | ${false} + ${'../relative_url'} | ${false} + ${''} | ${false} + `('returns $valid for $url', ({ url, valid }) => { + expect(urlUtils.isBlobUrl(url)).toBe(valid); + }); + }); + describe('relativePathToAbsolute', () => { it.each` path | base | result @@ -664,6 +678,19 @@ describe('URL utility', () => { }); }); + describe('cleanLeadingSeparator', () => { + it.each` + path | expected + ${'/foo/bar'} | ${'foo/bar'} + ${'foo/bar'} | ${'foo/bar'} + ${'//foo/bar'} | ${'foo/bar'} + ${'/./foo/bar'} | ${'./foo/bar'} + ${''} | ${''} + `('$path becomes $expected', ({ path, expected }) => { + expect(urlUtils.cleanLeadingSeparator(path)).toBe(expected); + }); + }); + describe('joinPaths', () => { it.each` paths | expected @@ -688,6 +715,18 @@ describe('URL utility', () => { }); }); + describe('stripFinalUrlSegment', () => { + it.each` + path | expected + ${'http://fake.domain/twitter/typeahead-js/-/tags/v0.11.0'} | ${'http://fake.domain/twitter/typeahead-js/-/tags/'} + ${'http://fake.domain/bar/cool/-/nested/content'} | ${'http://fake.domain/bar/cool/-/nested/'} + ${'http://fake.domain/bar/cool?q="search"'} | ${'http://fake.domain/bar/'} + ${'http://fake.domain/bar/cool#link-to-something'} | ${'http://fake.domain/bar/'} + `('stripFinalUrlSegment $path => $expected', ({ path, expected }) => { + expect(urlUtils.stripFinalUrlSegment(path)).toBe(expected); + }); + }); + describe('escapeFileUrl', () => { it('encodes URL excluding the slashes', () => { expect(urlUtils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md'); @@ -787,4 +826,36 @@ describe('URL utility', () => { expect(urlUtils.getHTTPProtocol(url)).toBe(expectation); }); }); + + describe('stripPathTail', () => { + it.each` + path | expected + ${''} | ${''} + ${'index.html'} | ${''} + ${'/'} | ${'/'} + ${'/foo/bar'} | ${'/foo/'} + ${'/foo/bar/'} | ${'/foo/bar/'} + ${'/foo/bar/index.html'} | ${'/foo/bar/'} + `('strips the filename from $path => $expected', ({ path, expected }) => { + expect(urlUtils.stripPathTail(path)).toBe(expected); + }); + }); + + describe('getURLOrigin', () => { + it('when no url passed, returns correct origin from window location', () => { + const origin = 'https://foo.bar'; + + setWindowLocation({ origin }); + expect(urlUtils.getURLOrigin()).toBe(origin); + }); + + it.each` + url | expectation + ${'not-a-url'} | ${null} + ${'wss://example.com'} | ${'wss://example.com'} + ${'https://foo.bar/foo/bar'} | ${'https://foo.bar'} + `('returns correct origin for $url', ({ url, expectation }) => { + expect(urlUtils.getURLOrigin(url)).toBe(expectation); + }); + }); }); -- cgit v1.2.1