/* eslint no-param-reassign: "off" */ import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import GfmAutoComplete, { escape, membersBeforeSave, highlighter, CONTACT_STATE_ACTIVE, CONTACTS_ADD_COMMAND, CONTACTS_REMOVE_COMMAND, } from 'ee_else_ce/gfm_auto_complete'; import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import '~/lib/utils/jquery_at_who'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import AjaxCache from '~/lib/utils/ajax_cache'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { eventlistenersMockDefaultMap, crmContactsMock, } from 'ee_else_ce_jest/gfm_auto_complete/mock_data'; describe('escape', () => { it.each` xssPayload | escapedPayload ${''} | ${'<script>alert(1)</script>'} ${'%3Cscript%3E alert(1) %3C%2Fscript%3E'} | ${'<script> alert(1) </script>'} ${'%253Cscript%253E alert(1) %253C%252Fscript%253E'} | ${'<script> alert(1) </script>'} `( 'escapes the input string correctly accounting for multiple encoding', ({ xssPayload, escapedPayload }) => { expect(escape(xssPayload)).toBe(escapedPayload); }, ); }); describe('GfmAutoComplete', () => { const fetchDataMock = { fetchData: jest.fn() }; let gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call(fetchDataMock); let atwhoInstance; let sorterValue; let filterValue; describe('DefaultOptions.filter', () => { let items; beforeEach(() => { jest.spyOn(fetchDataMock, 'fetchData'); jest.spyOn($.fn.atwho.default.callbacks, 'filter').mockImplementation(() => {}); }); describe('assets loading', () => { beforeEach(() => { atwhoInstance = { setting: {}, $inputor: 'inputor', at: '[vulnerability:' }; items = ['loading']; filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, '', items); }); it('should call the fetchData function without query', () => { expect(fetchDataMock.fetchData).toHaveBeenCalledWith('inputor', '[vulnerability:'); }); it('should not call the default atwho filter', () => { expect($.fn.atwho.default.callbacks.filter).not.toHaveBeenCalled(); }); it('should return the passed unfiltered items', () => { expect(filterValue).toEqual(items); }); }); describe('backend filtering', () => { beforeEach(() => { atwhoInstance = { setting: {}, $inputor: 'inputor', at: '[vulnerability:' }; items = []; }); describe('when previous query is different from current one', () => { beforeEach(() => { gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ previousQuery: 'oldquery', ...fetchDataMock, }); filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, 'newquery', items); }); it('should call the fetchData function with query', () => { expect(fetchDataMock.fetchData).toHaveBeenCalledWith( 'inputor', '[vulnerability:', 'newquery', ); }); it('should not call the default atwho filter', () => { expect($.fn.atwho.default.callbacks.filter).not.toHaveBeenCalled(); }); it('should return the passed unfiltered items', () => { expect(filterValue).toEqual(items); }); }); describe('when previous query is not different from current one', () => { beforeEach(() => { gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ previousQuery: 'oldquery', ...fetchDataMock, }); filterValue = gfmAutoCompleteCallbacks.filter.call( atwhoInstance, 'oldquery', items, 'searchKey', ); }); it('should not call the fetchData function', () => { expect(fetchDataMock.fetchData).not.toHaveBeenCalled(); }); it('should call the default atwho filter', () => { expect($.fn.atwho.default.callbacks.filter).toHaveBeenCalledWith( 'oldquery', items, 'searchKey', ); }); }); }); }); describe('fetchData', () => { const { fetchData } = GfmAutoComplete.prototype; let mock; beforeEach(() => { mock = new MockAdapter(axios); jest.spyOn(axios, 'get'); jest.spyOn(AjaxCache, 'retrieve'); }); afterEach(() => { mock.restore(); }); describe('already loading data', () => { beforeEach(() => { const context = { isLoadingData: { '[vulnerability:': true }, dataSources: {}, cachedData: {}, }; fetchData.call(context, {}, '[vulnerability:', ''); }); it('should not call either axios nor AjaxCache', () => { expect(axios.get).not.toHaveBeenCalled(); expect(AjaxCache.retrieve).not.toHaveBeenCalled(); }); }); describe('backend filtering', () => { describe('data is not in cache', () => { let context; beforeEach(() => { context = { isLoadingData: { '[vulnerability:': false }, dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' }, cachedData: {}, }; }); it('should call axios with query', () => { fetchData.call(context, {}, '[vulnerability:', 'query'); expect(axios.get).toHaveBeenCalledWith('vulnerabilities_autocomplete_url', { params: { search: 'query' }, }); }); it.each([HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR])( 'should set the loading state', async (responseStatus) => { mock.onGet('vulnerabilities_autocomplete_url').replyOnce(responseStatus); fetchData.call(context, {}, '[vulnerability:', 'query'); expect(context.isLoadingData['[vulnerability:']).toBe(true); await waitForPromises(); expect(context.isLoadingData['[vulnerability:']).toBe(false); }, ); }); describe('data is in cache', () => { beforeEach(() => { const context = { isLoadingData: { '[vulnerability:': false }, dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' }, cachedData: { '[vulnerability:': [{}] }, }; fetchData.call(context, {}, '[vulnerability:', 'query'); }); it('should anyway call axios with query ignoring cache', () => { expect(axios.get).toHaveBeenCalledWith('vulnerabilities_autocomplete_url', { params: { search: 'query' }, }); }); }); }); describe('frontend filtering', () => { describe('data is not in cache', () => { beforeEach(() => { const context = { isLoadingData: { '#': false }, dataSources: { issues: 'issues_autocomplete_url' }, cachedData: {}, }; fetchData.call(context, {}, '#', 'query'); }); it('should call AjaxCache', () => { expect(AjaxCache.retrieve).toHaveBeenCalledWith('issues_autocomplete_url', true); }); }); describe('data is in cache', () => { beforeEach(() => { const context = { isLoadingData: { '#': false }, dataSources: { issues: 'issues_autocomplete_url' }, cachedData: { '#': [{}] }, loadData: () => {}, }; fetchData.call(context, {}, '#', 'query'); }); it('should not call AjaxCache', () => { expect(AjaxCache.retrieve).not.toHaveBeenCalled(); }); }); }); }); describe('DefaultOptions.sorter', () => { describe('assets loading', () => { let items; beforeEach(() => { jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true); atwhoInstance = { setting: {} }; items = []; sorterValue = gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, '', items); }); it('should disable highlightFirst', () => { expect(atwhoInstance.setting.highlightFirst).toBe(false); }); it('should return the passed unfiltered items', () => { expect(sorterValue).toEqual(items); }); }); describe('assets finished loading', () => { beforeEach(() => { jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(false); jest.spyOn($.fn.atwho.default.callbacks, 'sorter').mockImplementation(() => {}); }); it('should enable highlightFirst if alwaysHighlightFirst is set', () => { atwhoInstance = { setting: { alwaysHighlightFirst: true } }; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); it('should enable highlightFirst if a query is present', () => { atwhoInstance = { setting: {} }; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query'); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); it('should call the default atwho sorter', () => { atwhoInstance = { setting: {} }; const query = 'query'; const items = []; const searchKey = 'searchKey'; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey); }); }); }); describe('DefaultOptions.beforeInsert', () => { const beforeInsert = (context, value) => gfmAutoCompleteCallbacks.beforeInsert.call(context, value); beforeEach(() => { atwhoInstance = { setting: { skipSpecialCharacterTest: false } }; }); it('should not quote if value only contains alphanumeric charecters', () => { expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1'); expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1'); }); it('should quote if value contains any non-alphanumeric characters', () => { expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"'); expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"'); }); it('should quote integer labels', () => { expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"'); }); it('escapes Markdown strikethroughs when needed', () => { expect(beforeInsert(atwhoInstance, '~a~bug')).toEqual('~"a~bug"'); expect(beforeInsert(atwhoInstance, '~a~~bug~~')).toEqual('~"a\\~~bug\\~~"'); }); it('escapes Markdown emphasis when needed', () => { expect(beforeInsert(atwhoInstance, '~a_bug_')).toEqual('~a_bug\\_'); expect(beforeInsert(atwhoInstance, '~a _bug_')).toEqual('~"a \\_bug\\_"'); expect(beforeInsert(atwhoInstance, '~a*bug*')).toEqual('~"a\\*bug\\*"'); expect(beforeInsert(atwhoInstance, '~a *bug*')).toEqual('~"a \\*bug\\*"'); }); it('escapes Markdown code spans when needed', () => { expect(beforeInsert(atwhoInstance, '~a`bug`')).toEqual('~"a\\`bug\\`"'); expect(beforeInsert(atwhoInstance, '~a `bug`')).toEqual('~"a \\`bug\\`"'); }); }); describe('DefaultOptions.matcher', () => { const defaultMatcher = (context, flag, subtext) => gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext); const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%', '$']; const otherFlags = ['/', ':']; const flags = flagsUseDefaultMatcher.concat(otherFlags); const flagsHash = flags.reduce((hash, el) => { hash[el] = null; return hash; }, {}); beforeEach(() => { atwhoInstance = { setting: {}, app: { controllers: flagsHash } }; }); const minLen = 1; const maxLen = 20; const argumentSize = [minLen, maxLen / 2, maxLen]; const allowedSymbols = [ '', 'a', 'n', 'z', 'A', 'Z', 'N', '0', '5', '9', 'А', 'а', 'Я', 'я', '.', "'", '-', '_', ]; const jointAllowedSymbols = allowedSymbols.join(''); describe('should match regular symbols', () => { flagsUseDefaultMatcher.forEach((flag) => { allowedSymbols.forEach((symbol) => { argumentSize.forEach((size) => { const query = new Array(size + 1).join(symbol); const subtext = flag + query; it(`matches argument "${flag}" with query "${subtext}"`, () => { expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(query); }); }); }); it(`matches combination of allowed symbols for flag "${flag}"`, () => { const subtext = flag + jointAllowedSymbols; expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(jointAllowedSymbols); }); }); }); describe('should not match special sequences', () => { const shouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); const shouldNotBePrependedBy = ['`']; flagsUseDefaultMatcher.forEach((atSign) => { shouldNotBeFollowedBy.forEach((followedSymbol) => { const seq = atSign + followedSymbol; it(`should not match ${JSON.stringify(seq)}`, () => { expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); }); }); shouldNotBePrependedBy.forEach((prependedSymbol) => { const seq = prependedSymbol + atSign; it(`should not match "${seq}"`, () => { expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); }); }); }); }); }); describe('DefaultOptions.highlighter', () => { beforeEach(() => { atwhoInstance = { setting: {} }; }); it('should return li if no query is given', () => { const liTag = '
'; const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag); expect(highlightedTag).toEqual(liTag); }); it('should highlight search query in li element', () => { const liTag = '