summaryrefslogtreecommitdiff
path: root/spec/frontend/lib
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/lib')
-rw-r--r--spec/frontend/lib/dompurify_spec.js98
-rw-r--r--spec/frontend/lib/utils/axios_startup_calls_spec.js49
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js65
-rw-r--r--spec/frontend/lib/utils/experimentation_spec.js20
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js11
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js99
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js71
7 files changed, 318 insertions, 95 deletions
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 = '<svg><use></use></svg>';
+
+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('<br>', { ALLOWED_TAGS: [] })).toBe('');
+ expect(sanitize('<strong></strong>', { 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 = `<svg><use></use></svg>`;
+ expect(sanitize(htmlHref)).toBe(htmlHref);
+ });
+
+ it.each(safeUrls[type])('allows safe URL %s', url => {
+ const htmlHref = `<svg><use href="${url}"></use></svg>`;
+ expect(sanitize(htmlHref)).toBe(htmlHref);
+
+ const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
+ expect(sanitize(htmlXlink)).toBe(htmlXlink);
+ });
+
+ it.each(unsafeUrls)('sanitizes unsafe URL %s', url => {
+ const htmlHref = `<svg><use href="${url}"></use></svg>`;
+ const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
+
+ 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 = `<svg><use href="${url}"></use></svg>`;
+ const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
+
+ 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}
+ ${'<a></a>'} | ${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);
+ });
+ });
});