diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-06 18:14:39 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-06 18:14:39 +0300 |
commit | 55242833f832095a6fcff00b1ccacbc5900ee52a (patch) | |
tree | 6e17b16638e60099533473b540fe8f635d2f25da /spec | |
parent | 7c31b0312ba0eae4e4ebe54125b13aa2ae5f5db4 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/controllers/invites_controller_spec.rb | 26 | ||||
-rw-r--r-- | spec/controllers/registrations_controller_spec.rb | 34 | ||||
-rw-r--r-- | spec/features/invites_spec.rb | 14 | ||||
-rw-r--r-- | spec/features/users/one_trust_csp_spec.rb | 17 | ||||
-rw-r--r-- | spec/frontend/editor/source_editor_markdown_ext_spec.js | 363 | ||||
-rw-r--r-- | spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js | 398 | ||||
-rw-r--r-- | spec/helpers/notify_helper_spec.rb | 22 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb | 97 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb | 94 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb | 85 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/pipeline/seed/build_spec.rb | 28 | ||||
-rw-r--r-- | spec/lib/gitlab/usage_data_spec.rb | 14 | ||||
-rw-r--r-- | spec/mailers/notify_spec.rb | 21 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 14 |
14 files changed, 732 insertions, 495 deletions
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index d4091461062..dc1fb0454df 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -120,29 +120,6 @@ RSpec.describe InvitesController do end end - context 'when it is part of the invite_email_from experiment' do - let(:extra_params) { { invite_type: 'initial_email', experiment_name: 'invite_email_from' } } - - it 'tracks the initial join click from email' do - experiment = double(track: true) - allow(controller).to receive(:experiment).with(:invite_email_from, actor: member).and_return(experiment) - - request - - expect(experiment).to have_received(:track).with(:join_clicked) - end - - context 'when member does not exist' do - let(:raw_invite_token) { '_bogus_token_' } - - it 'does not track the experiment' do - expect(controller).not_to receive(:experiment).with(:invite_email_from, actor: member) - - request - end - end - end - context 'when member does not exist' do let(:raw_invite_token) { '_bogus_token_' } @@ -170,9 +147,8 @@ RSpec.describe InvitesController do end context 'when it is not part of our invite email experiment' do - it 'does not track via experiment', :aggregate_failures do + it 'does not track via experiment' do expect(controller).not_to receive(:experiment).with(:invite_email_preview_text, actor: member) - expect(controller).not_to receive(:experiment).with(:invite_email_from, actor: member) request end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index baf500c2b57..9094d235366 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -227,40 +227,6 @@ RSpec.describe RegistrationsController do end end end - - context 'with the invite_email_preview_text experiment', :experiment do - let(:extra_session_params) { { invite_email_experiment_name: 'invite_email_from' } } - - context 'when member and invite_email_experiment_name exists from the session key value' do - it 'tracks the invite acceptance' do - expect(experiment(:invite_email_from)).to track(:accepted) - .with_context(actor: member) - .on_next_instance - - subject - end - end - - context 'when member does not exist from the session key value' do - let(:originating_member_id) { -1 } - - it 'does not track invite acceptance' do - expect(experiment(:invite_email_from)).not_to track(:accepted) - - subject - end - end - - context 'when invite_email_experiment_name does not exist from the session key value' do - let(:extra_session_params) { {} } - - it 'does not track invite acceptance' do - expect(experiment(:invite_email_from)).not_to track(:accepted) - - subject - end - end - end end context 'when invite email matches email used on registration' do diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index f9ab780d2d6..d2bf35166ac 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -240,20 +240,6 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do end end - context 'with invite email acceptance for the invite_email_from experiment', :experiment do - let(:extra_params) do - { invite_type: Emails::Members::INITIAL_INVITE, experiment_name: 'invite_email_from' } - end - - it 'tracks the accepted invite' do - expect(experiment(:invite_email_from)).to track(:accepted) - .with_context(actor: group_invite) - .on_next_instance - - fill_in_sign_up_form(new_user) - end - end - it 'signs up and redirects to the group activity page with all the project/groups invitation automatically accepted' do fill_in_sign_up_form(new_user) fill_in_welcome_form diff --git a/spec/features/users/one_trust_csp_spec.rb b/spec/features/users/one_trust_csp_spec.rb new file mode 100644 index 00000000000..382a0b4be6c --- /dev/null +++ b/spec/features/users/one_trust_csp_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'OneTrust content security policy' do + let(:user) { create(:user) } + + before do + stub_config(extra: { one_trust_id: SecureRandom.uuid }) + end + + it 'has proper Content Security Policy headers' do + visit root_path + + expect(response_headers['Content-Security-Policy']).to include('https://cdn.cookielaw.org https://*.onetrust.com') + end +end diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js index 4a53f870f6d..4a50d801296 100644 --- a/spec/frontend/editor/source_editor_markdown_ext_spec.js +++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js @@ -1,36 +1,20 @@ import MockAdapter from 'axios-mock-adapter'; -import { Range, Position, editor as monacoEditor } from 'monaco-editor'; -import waitForPromises from 'helpers/wait_for_promises'; -import { - EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS, - EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, - EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH, - EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS, - EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY, -} from '~/editor/constants'; +import { Range, Position } from 'monaco-editor'; import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext'; import SourceEditor from '~/editor/source_editor'; -import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; -import syntaxHighlight from '~/syntax_highlight'; - -jest.mock('~/syntax_highlight'); -jest.mock('~/flash'); describe('Markdown Extension for Source Editor', () => { let editor; let instance; let editorEl; - let panelSpy; let mockAxios; const previewMarkdownPath = '/gitlab/fooGroup/barProj/preview_markdown'; const firstLine = 'This is a'; const secondLine = 'multiline'; const thirdLine = 'string with some **markup**'; const text = `${firstLine}\n${secondLine}\n${thirdLine}`; - const plaintextPath = 'foo.txt'; const markdownPath = 'foo.md'; - const responseData = '<div>FooBar</div>'; const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => { const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn); @@ -42,11 +26,6 @@ describe('Markdown Extension for Source Editor', () => { const selectionToString = () => instance.getSelection().toString(); const positionToString = () => instance.getPosition().toString(); - const togglePreview = async () => { - instance.togglePreview(); - await waitForPromises(); - }; - beforeEach(() => { mockAxios = new MockAdapter(axios); setFixtures('<div id="editor" data-editor-loading></div>'); @@ -58,7 +37,6 @@ describe('Markdown Extension for Source Editor', () => { blobContent: text, }); instance.use(new EditorMarkdownExtension({ instance, previewMarkdownPath })); - panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel'); }); afterEach(() => { @@ -67,345 +45,6 @@ describe('Markdown Extension for Source Editor', () => { mockAxios.restore(); }); - it('sets up the instance', () => { - expect(instance.preview).toEqual({ - el: undefined, - action: expect.any(Object), - shown: false, - modelChangeListener: undefined, - }); - expect(instance.previewMarkdownPath).toBe(previewMarkdownPath); - }); - - describe('model language changes listener', () => { - let cleanupSpy; - let actionSpy; - - beforeEach(async () => { - cleanupSpy = jest.spyOn(instance, 'cleanup'); - actionSpy = jest.spyOn(instance, 'setupPreviewAction'); - await togglePreview(); - }); - - it('cleans up when switching away from markdown', () => { - expect(instance.cleanup).not.toHaveBeenCalled(); - expect(instance.setupPreviewAction).not.toHaveBeenCalled(); - - instance.updateModelLanguage(plaintextPath); - - expect(cleanupSpy).toHaveBeenCalled(); - expect(actionSpy).not.toHaveBeenCalled(); - }); - - it.each` - oldLanguage | newLanguage | setupCalledTimes - ${'plaintext'} | ${'markdown'} | ${1} - ${'markdown'} | ${'markdown'} | ${0} - ${'markdown'} | ${'plaintext'} | ${0} - ${'markdown'} | ${undefined} | ${0} - ${undefined} | ${'markdown'} | ${1} - `( - 'correctly handles re-enabling of the action when switching from $oldLanguage to $newLanguage', - ({ oldLanguage, newLanguage, setupCalledTimes } = {}) => { - expect(actionSpy).not.toHaveBeenCalled(); - instance.updateModelLanguage(oldLanguage); - instance.updateModelLanguage(newLanguage); - expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes); - }, - ); - }); - - describe('model change listener', () => { - let cleanupSpy; - let actionSpy; - - beforeEach(() => { - cleanupSpy = jest.spyOn(instance, 'cleanup'); - actionSpy = jest.spyOn(instance, 'setupPreviewAction'); - instance.togglePreview(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('does not do anything if there is no model', () => { - instance.setModel(null); - - expect(cleanupSpy).not.toHaveBeenCalled(); - expect(actionSpy).not.toHaveBeenCalled(); - }); - - it('cleans up the preview when the model changes', () => { - instance.setModel(monacoEditor.createModel('foo')); - expect(cleanupSpy).toHaveBeenCalled(); - }); - - it.each` - language | setupCalledTimes - ${'markdown'} | ${1} - ${'plaintext'} | ${0} - ${undefined} | ${0} - `( - 'correctly handles actions when the new model is $language', - ({ language, setupCalledTimes } = {}) => { - instance.setModel(monacoEditor.createModel('foo', language)); - - expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes); - }, - ); - }); - - describe('cleanup', () => { - beforeEach(async () => { - mockAxios.onPost().reply(200, { body: responseData }); - await togglePreview(); - }); - - it('disposes the modelChange listener and does not fetch preview on content changes', () => { - expect(instance.preview.modelChangeListener).toBeDefined(); - jest.spyOn(instance, 'fetchPreview'); - - instance.cleanup(); - instance.setValue('Foo Bar'); - jest.advanceTimersByTime(EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY); - - expect(instance.fetchPreview).not.toHaveBeenCalled(); - }); - - it('removes the contextual menu action', () => { - expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined(); - - instance.cleanup(); - - expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBe(null); - }); - - it('toggles the `shown` flag', () => { - expect(instance.preview.shown).toBe(true); - instance.cleanup(); - expect(instance.preview.shown).toBe(false); - }); - - it('toggles the panel only if the preview is visible', () => { - const { el: previewEl } = instance.preview; - const parentEl = previewEl.parentElement; - - expect(previewEl).toBeVisible(); - expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(true); - - instance.cleanup(); - expect(previewEl).toBeHidden(); - expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( - false, - ); - - instance.cleanup(); - expect(previewEl).toBeHidden(); - expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( - false, - ); - }); - - it('toggles the layout only if the preview is visible', () => { - const { width } = instance.getLayoutInfo(); - - expect(instance.preview.shown).toBe(true); - - instance.cleanup(); - - const { width: newWidth } = instance.getLayoutInfo(); - expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true); - - instance.cleanup(); - expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true); - }); - }); - - describe('fetchPreview', () => { - const fetchPreview = async () => { - instance.fetchPreview(); - await waitForPromises(); - }; - - let previewMarkdownSpy; - - beforeEach(() => { - previewMarkdownSpy = jest.fn().mockImplementation(() => [200, { body: responseData }]); - mockAxios.onPost(previewMarkdownPath).replyOnce((req) => previewMarkdownSpy(req)); - }); - - it('correctly fetches preview based on previewMarkdownPath', async () => { - await fetchPreview(); - - expect(previewMarkdownSpy).toHaveBeenCalledWith( - expect.objectContaining({ data: JSON.stringify({ text }) }), - ); - }); - - it('puts the fetched content into the preview DOM element', async () => { - instance.preview.el = editorEl.parentElement; - await fetchPreview(); - expect(instance.preview.el.innerHTML).toEqual(responseData); - }); - - it('applies syntax highlighting to the preview content', async () => { - instance.preview.el = editorEl.parentElement; - await fetchPreview(); - expect(syntaxHighlight).toHaveBeenCalled(); - }); - - it('catches the errors when fetching the preview', async () => { - mockAxios.onPost().reply(500); - - await fetchPreview(); - expect(createFlash).toHaveBeenCalled(); - }); - }); - - describe('setupPreviewAction', () => { - it('adds the contextual menu action', () => { - expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined(); - }); - - it('does not set up action if one already exists', () => { - jest.spyOn(instance, 'addAction').mockImplementation(); - - instance.setupPreviewAction(); - expect(instance.addAction).not.toHaveBeenCalled(); - }); - - it('toggles preview when the action is triggered', () => { - jest.spyOn(instance, 'togglePreview').mockImplementation(); - - expect(instance.togglePreview).not.toHaveBeenCalled(); - - const action = instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID); - action.run(); - - expect(instance.togglePreview).toHaveBeenCalled(); - }); - }); - - describe('togglePreview', () => { - beforeEach(() => { - mockAxios.onPost().reply(200, { body: responseData }); - }); - - it('toggles preview flag on instance', () => { - expect(instance.preview.shown).toBe(false); - - instance.togglePreview(); - expect(instance.preview.shown).toBe(true); - - instance.togglePreview(); - expect(instance.preview.shown).toBe(false); - }); - - describe('panel DOM element set up', () => { - it('sets up an element to contain the preview and stores it on instance', () => { - expect(instance.preview.el).toBeUndefined(); - - instance.togglePreview(); - - expect(instance.preview.el).toBeDefined(); - expect(instance.preview.el.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS)).toBe( - true, - ); - }); - - it('re-uses existing preview DOM element on repeated calls', () => { - instance.togglePreview(); - const origPreviewEl = instance.preview.el; - instance.togglePreview(); - - expect(instance.preview.el).toBe(origPreviewEl); - }); - - it('hides the preview DOM element by default', () => { - panelSpy.mockImplementation(); - instance.togglePreview(); - expect(instance.preview.el.style.display).toBe('none'); - }); - }); - - describe('preview layout setup', () => { - it('sets correct preview layout', () => { - jest.spyOn(instance, 'layout'); - const { width, height } = instance.getLayoutInfo(); - - instance.togglePreview(); - - expect(instance.layout).toHaveBeenCalledWith({ - width: width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH, - height, - }); - }); - }); - - describe('preview panel', () => { - it('toggles preview CSS class on the editor', () => { - expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( - false, - ); - instance.togglePreview(); - expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( - true, - ); - instance.togglePreview(); - expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( - false, - ); - }); - - it('toggles visibility of the preview DOM element', async () => { - await togglePreview(); - expect(instance.preview.el.style.display).toBe('block'); - await togglePreview(); - expect(instance.preview.el.style.display).toBe('none'); - }); - - describe('hidden preview DOM element', () => { - it('listens to model changes and re-fetches preview', async () => { - expect(mockAxios.history.post).toHaveLength(0); - await togglePreview(); - expect(mockAxios.history.post).toHaveLength(1); - - instance.setValue('New Value'); - await waitForPromises(); - expect(mockAxios.history.post).toHaveLength(2); - }); - - it('stores disposable listener for model changes', async () => { - expect(instance.preview.modelChangeListener).toBeUndefined(); - await togglePreview(); - expect(instance.preview.modelChangeListener).toBeDefined(); - }); - }); - - describe('already visible preview', () => { - beforeEach(async () => { - await togglePreview(); - mockAxios.resetHistory(); - }); - - it('does not re-fetch the preview', () => { - instance.togglePreview(); - expect(mockAxios.history.post).toHaveLength(0); - }); - - it('disposes the model change event listener', () => { - const disposeSpy = jest.fn(); - instance.preview.modelChangeListener = { - dispose: disposeSpy, - }; - instance.togglePreview(); - expect(disposeSpy).toHaveBeenCalled(); - }); - }); - }); - }); - describe('getSelectedText', () => { it('does not fail if there is no selection and returns the empty string', () => { jest.spyOn(instance, 'getSelection'); diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js new file mode 100644 index 00000000000..3d797073c05 --- /dev/null +++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js @@ -0,0 +1,398 @@ +import MockAdapter from 'axios-mock-adapter'; +import { editor as monacoEditor } from 'monaco-editor'; +import waitForPromises from 'helpers/wait_for_promises'; +import { + EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS, + EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, + EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH, + EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS, + EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY, +} from '~/editor/constants'; +import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext'; +import SourceEditor from '~/editor/source_editor'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import syntaxHighlight from '~/syntax_highlight'; + +jest.mock('~/syntax_highlight'); +jest.mock('~/flash'); + +describe('Markdown Live Preview Extension for Source Editor', () => { + let editor; + let instance; + let editorEl; + let panelSpy; + let mockAxios; + const previewMarkdownPath = '/gitlab/fooGroup/barProj/preview_markdown'; + const firstLine = 'This is a'; + const secondLine = 'multiline'; + const thirdLine = 'string with some **markup**'; + const text = `${firstLine}\n${secondLine}\n${thirdLine}`; + const plaintextPath = 'foo.txt'; + const markdownPath = 'foo.md'; + const responseData = '<div>FooBar</div>'; + + const togglePreview = async () => { + instance.togglePreview(); + await waitForPromises(); + }; + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + setFixtures('<div id="editor" data-editor-loading></div>'); + editorEl = document.getElementById('editor'); + editor = new SourceEditor(); + instance = editor.createInstance({ + el: editorEl, + blobPath: markdownPath, + blobContent: text, + }); + instance.use(new EditorMarkdownPreviewExtension({ instance, previewMarkdownPath })); + panelSpy = jest.spyOn(EditorMarkdownPreviewExtension, 'togglePreviewPanel'); + }); + + afterEach(() => { + instance.dispose(); + editorEl.remove(); + mockAxios.restore(); + }); + + it('sets up the instance', () => { + expect(instance.preview).toEqual({ + el: undefined, + action: expect.any(Object), + shown: false, + modelChangeListener: undefined, + }); + expect(instance.previewMarkdownPath).toBe(previewMarkdownPath); + }); + + describe('model language changes listener', () => { + let cleanupSpy; + let actionSpy; + + beforeEach(async () => { + cleanupSpy = jest.spyOn(instance, 'cleanup'); + actionSpy = jest.spyOn(instance, 'setupPreviewAction'); + await togglePreview(); + }); + + it('cleans up when switching away from markdown', () => { + expect(instance.cleanup).not.toHaveBeenCalled(); + expect(instance.setupPreviewAction).not.toHaveBeenCalled(); + + instance.updateModelLanguage(plaintextPath); + + expect(cleanupSpy).toHaveBeenCalled(); + expect(actionSpy).not.toHaveBeenCalled(); + }); + + it.each` + oldLanguage | newLanguage | setupCalledTimes + ${'plaintext'} | ${'markdown'} | ${1} + ${'markdown'} | ${'markdown'} | ${0} + ${'markdown'} | ${'plaintext'} | ${0} + ${'markdown'} | ${undefined} | ${0} + ${undefined} | ${'markdown'} | ${1} + `( + 'correctly handles re-enabling of the action when switching from $oldLanguage to $newLanguage', + ({ oldLanguage, newLanguage, setupCalledTimes } = {}) => { + expect(actionSpy).not.toHaveBeenCalled(); + instance.updateModelLanguage(oldLanguage); + instance.updateModelLanguage(newLanguage); + expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes); + }, + ); + }); + + describe('model change listener', () => { + let cleanupSpy; + let actionSpy; + + beforeEach(() => { + cleanupSpy = jest.spyOn(instance, 'cleanup'); + actionSpy = jest.spyOn(instance, 'setupPreviewAction'); + instance.togglePreview(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('does not do anything if there is no model', () => { + instance.setModel(null); + + expect(cleanupSpy).not.toHaveBeenCalled(); + expect(actionSpy).not.toHaveBeenCalled(); + }); + + it('cleans up the preview when the model changes', () => { + instance.setModel(monacoEditor.createModel('foo')); + expect(cleanupSpy).toHaveBeenCalled(); + }); + + it.each` + language | setupCalledTimes + ${'markdown'} | ${1} + ${'plaintext'} | ${0} + ${undefined} | ${0} + `( + 'correctly handles actions when the new model is $language', + ({ language, setupCalledTimes } = {}) => { + instance.setModel(monacoEditor.createModel('foo', language)); + + expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes); + }, + ); + }); + + describe('cleanup', () => { + beforeEach(async () => { + mockAxios.onPost().reply(200, { body: responseData }); + await togglePreview(); + }); + + it('disposes the modelChange listener and does not fetch preview on content changes', () => { + expect(instance.preview.modelChangeListener).toBeDefined(); + jest.spyOn(instance, 'fetchPreview'); + + instance.cleanup(); + instance.setValue('Foo Bar'); + jest.advanceTimersByTime(EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY); + + expect(instance.fetchPreview).not.toHaveBeenCalled(); + }); + + it('removes the contextual menu action', () => { + expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined(); + + instance.cleanup(); + + expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBe(null); + }); + + it('toggles the `shown` flag', () => { + expect(instance.preview.shown).toBe(true); + instance.cleanup(); + expect(instance.preview.shown).toBe(false); + }); + + it('toggles the panel only if the preview is visible', () => { + const { el: previewEl } = instance.preview; + const parentEl = previewEl.parentElement; + + expect(previewEl).toBeVisible(); + expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(true); + + instance.cleanup(); + expect(previewEl).toBeHidden(); + expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( + false, + ); + + instance.cleanup(); + expect(previewEl).toBeHidden(); + expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( + false, + ); + }); + + it('toggles the layout only if the preview is visible', () => { + const { width } = instance.getLayoutInfo(); + + expect(instance.preview.shown).toBe(true); + + instance.cleanup(); + + const { width: newWidth } = instance.getLayoutInfo(); + expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true); + + instance.cleanup(); + expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true); + }); + }); + + describe('fetchPreview', () => { + const fetchPreview = async () => { + instance.fetchPreview(); + await waitForPromises(); + }; + + let previewMarkdownSpy; + + beforeEach(() => { + previewMarkdownSpy = jest.fn().mockImplementation(() => [200, { body: responseData }]); + mockAxios.onPost(previewMarkdownPath).replyOnce((req) => previewMarkdownSpy(req)); + }); + + it('correctly fetches preview based on previewMarkdownPath', async () => { + await fetchPreview(); + + expect(previewMarkdownSpy).toHaveBeenCalledWith( + expect.objectContaining({ data: JSON.stringify({ text }) }), + ); + }); + + it('puts the fetched content into the preview DOM element', async () => { + instance.preview.el = editorEl.parentElement; + await fetchPreview(); + expect(instance.preview.el.innerHTML).toEqual(responseData); + }); + + it('applies syntax highlighting to the preview content', async () => { + instance.preview.el = editorEl.parentElement; + await fetchPreview(); + expect(syntaxHighlight).toHaveBeenCalled(); + }); + + it('catches the errors when fetching the preview', async () => { + mockAxios.onPost().reply(500); + + await fetchPreview(); + expect(createFlash).toHaveBeenCalled(); + }); + }); + + describe('setupPreviewAction', () => { + it('adds the contextual menu action', () => { + expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined(); + }); + + it('does not set up action if one already exists', () => { + jest.spyOn(instance, 'addAction').mockImplementation(); + + instance.setupPreviewAction(); + expect(instance.addAction).not.toHaveBeenCalled(); + }); + + it('toggles preview when the action is triggered', () => { + jest.spyOn(instance, 'togglePreview').mockImplementation(); + + expect(instance.togglePreview).not.toHaveBeenCalled(); + + const action = instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID); + action.run(); + + expect(instance.togglePreview).toHaveBeenCalled(); + }); + }); + + describe('togglePreview', () => { + beforeEach(() => { + mockAxios.onPost().reply(200, { body: responseData }); + }); + + it('toggles preview flag on instance', () => { + expect(instance.preview.shown).toBe(false); + + instance.togglePreview(); + expect(instance.preview.shown).toBe(true); + + instance.togglePreview(); + expect(instance.preview.shown).toBe(false); + }); + + describe('panel DOM element set up', () => { + it('sets up an element to contain the preview and stores it on instance', () => { + expect(instance.preview.el).toBeUndefined(); + + instance.togglePreview(); + + expect(instance.preview.el).toBeDefined(); + expect(instance.preview.el.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS)).toBe( + true, + ); + }); + + it('re-uses existing preview DOM element on repeated calls', () => { + instance.togglePreview(); + const origPreviewEl = instance.preview.el; + instance.togglePreview(); + + expect(instance.preview.el).toBe(origPreviewEl); + }); + + it('hides the preview DOM element by default', () => { + panelSpy.mockImplementation(); + instance.togglePreview(); + expect(instance.preview.el.style.display).toBe('none'); + }); + }); + + describe('preview layout setup', () => { + it('sets correct preview layout', () => { + jest.spyOn(instance, 'layout'); + const { width, height } = instance.getLayoutInfo(); + + instance.togglePreview(); + + expect(instance.layout).toHaveBeenCalledWith({ + width: width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH, + height, + }); + }); + }); + + describe('preview panel', () => { + it('toggles preview CSS class on the editor', () => { + expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( + false, + ); + instance.togglePreview(); + expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( + true, + ); + instance.togglePreview(); + expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe( + false, + ); + }); + + it('toggles visibility of the preview DOM element', async () => { + await togglePreview(); + expect(instance.preview.el.style.display).toBe('block'); + await togglePreview(); + expect(instance.preview.el.style.display).toBe('none'); + }); + + describe('hidden preview DOM element', () => { + it('listens to model changes and re-fetches preview', async () => { + expect(mockAxios.history.post).toHaveLength(0); + await togglePreview(); + expect(mockAxios.history.post).toHaveLength(1); + + instance.setValue('New Value'); + await waitForPromises(); + expect(mockAxios.history.post).toHaveLength(2); + }); + + it('stores disposable listener for model changes', async () => { + expect(instance.preview.modelChangeListener).toBeUndefined(); + await togglePreview(); + expect(instance.preview.modelChangeListener).toBeDefined(); + }); + }); + + describe('already visible preview', () => { + beforeEach(async () => { + await togglePreview(); + mockAxios.resetHistory(); + }); + + it('does not re-fetch the preview', () => { + instance.togglePreview(); + expect(mockAxios.history.post).toHaveLength(0); + }); + + it('disposes the model change event listener', () => { + const disposeSpy = jest.fn(); + instance.preview.modelChangeListener = { + dispose: disposeSpy, + }; + instance.togglePreview(); + expect(disposeSpy).toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/spec/helpers/notify_helper_spec.rb b/spec/helpers/notify_helper_spec.rb index a4193444528..633a4b65139 100644 --- a/spec/helpers/notify_helper_spec.rb +++ b/spec/helpers/notify_helper_spec.rb @@ -70,28 +70,6 @@ RSpec.describe NotifyHelper do expect(helper.invited_join_url(token, member)) .to eq("http://test.host/-/invites/#{token}?experiment_name=invite_email_preview_text&invite_type=initial_email") end - - context 'when invite_email_from is enabled' do - before do - stub_experiments(invite_email_from: :control) - end - - it 'has correct params' do - expect(helper.invited_join_url(token, member)) - .to eq("http://test.host/-/invites/#{token}?experiment_name=invite_email_from&invite_type=initial_email") - end - end - end - - context 'when invite_email_from is enabled' do - before do - stub_experiments(invite_email_from: :control) - end - - it 'has correct params' do - expect(helper.invited_join_url(token, member)) - .to eq("http://test.host/-/invites/#{token}?experiment_name=invite_email_from&invite_type=initial_email") - end end context 'when invite_email_preview_text is disabled' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb new file mode 100644 index 00000000000..28bc685286f --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + let(:stage) { build(:ci_stage_entity, project: project, statuses: [job]) } + let(:pipeline) { create(:ci_pipeline, project: project, stages: [stage]) } + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) + end + + let(:step) { described_class.new(pipeline, command) } + + describe '#perform!' do + subject { step.perform! } + + before do + job.pipeline = pipeline + end + + context 'when a pipeline contains a deployment job' do + let!(:job) { build(:ci_build, :start_review_app, project: project) } + let!(:environment) { create(:environment, project: project, name: job.expanded_environment_name) } + + it 'creates a deployment record' do + expect { subject }.to change { Deployment.count }.by(1) + + job.reset + expect(job.deployment.project).to eq(job.project) + expect(job.deployment.ref).to eq(job.ref) + expect(job.deployment.sha).to eq(job.sha) + expect(job.deployment.deployable).to eq(job) + expect(job.deployment.deployable_type).to eq('CommitStatus') + expect(job.deployment.environment).to eq(job.persisted_environment) + end + + context 'when creation failure occures' do + before do + allow_next_instance_of(Deployment) do |deployment| + allow(deployment).to receive(:save!) { raise ActiveRecord::RecordInvalid } + end + end + + it 'trackes the exception' do + expect { subject }.to raise_error(described_class::DeploymentCreationError) + + expect(Deployment.count).to eq(0) + end + end + + context 'when the corresponding environment does not exist' do + let!(:environment) { } + + it 'does not create a deployment record' do + expect { subject }.not_to change { Deployment.count } + + expect(job.deployment).to be_nil + end + end + + context 'when create_deployment_in_separate_transaction feature flag is disabled' do + before do + stub_feature_flags(create_deployment_in_separate_transaction: false) + end + + it 'does not create a deployment record' do + expect { subject }.not_to change { Deployment.count } + + expect(job.deployment).to be_nil + end + end + end + + context 'when a pipeline contains a teardown job' do + let!(:job) { build(:ci_build, :stop_review_app, project: project) } + let!(:environment) { create(:environment, name: job.expanded_environment_name) } + + it 'does not create a deployment record' do + expect { subject }.not_to change { Deployment.count } + + expect(job.deployment).to be_nil + end + end + + context 'when a pipeline does not contain a deployment job' do + let!(:job) { build(:ci_build, project: project) } + + it 'does not create any deployments' do + expect { subject }.not_to change { Deployment.count } + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb new file mode 100644 index 00000000000..253928e1a19 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:stage) { build(:ci_stage_entity, project: project, statuses: [job]) } + let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) } + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) + end + + let(:step) { described_class.new(pipeline, command) } + + describe '#perform!' do + subject { step.perform! } + + before do + job.pipeline = pipeline + end + + context 'when a pipeline contains a deployment job' do + let!(:job) { build(:ci_build, :start_review_app, project: project) } + + it 'ensures environment existence for the job' do + expect { subject }.to change { Environment.count }.by(1) + + expect(project.environments.find_by_name('review/master')).to be_present + expect(job.persisted_environment.name).to eq('review/master') + expect(job.metadata.expanded_environment_name).to eq('review/master') + end + + context 'when an environment has already been existed' do + before do + create(:environment, project: project, name: 'review/master') + end + + it 'ensures environment existence for the job' do + expect { subject }.not_to change { Environment.count } + + expect(project.environments.find_by_name('review/master')).to be_present + expect(job.persisted_environment.name).to eq('review/master') + expect(job.metadata.expanded_environment_name).to eq('review/master') + end + end + + context 'when an environment name contains an invalid character' do + let(:pipeline) { build(:ci_pipeline, ref: '!!!', project: project, stages: [stage]) } + + it 'sets the failure status' do + expect { subject }.not_to change { Environment.count } + + expect(job).to be_failed + expect(job).to be_environment_creation_failure + expect(job.persisted_environment).to be_nil + end + end + + context 'when create_deployment_in_separate_transaction feature flag is disabled' do + before do + stub_feature_flags(create_deployment_in_separate_transaction: false) + end + + it 'does not create any environments' do + expect { subject }.not_to change { Environment.count } + + expect(job.persisted_environment).to be_nil + end + end + end + + context 'when a pipeline contains a teardown job' do + let!(:job) { build(:ci_build, :stop_review_app, project: project) } + + it 'ensures environment existence for the job' do + expect { subject }.to change { Environment.count }.by(1) + + expect(project.environments.find_by_name('review/master')).to be_present + expect(job.persisted_environment.name).to eq('review/master') + expect(job.metadata.expanded_environment_name).to eq('review/master') + end + end + + context 'when a pipeline does not contain a deployment job' do + let!(:job) { build(:ci_build, project: project) } + + it 'does not create any environments' do + expect { subject }.not_to change { Environment.count } + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb new file mode 100644 index 00000000000..87df5a3e21b --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_resource_groups_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureResourceGroups do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:stage) { build(:ci_stage_entity, project: project, statuses: [job]) } + let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage]) } + let!(:environment) { create(:environment, name: 'production', project: project) } + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) + end + + let(:step) { described_class.new(pipeline, command) } + + describe '#perform!' do + subject { step.perform! } + + before do + job.pipeline = pipeline + end + + context 'when a pipeline contains a job that requires a resource group' do + let!(:job) do + build(:ci_build, project: project, environment: 'production', options: { resource_group_key: '$CI_ENVIRONMENT_NAME' }) + end + + it 'ensures the resource group existence' do + expect { subject }.to change { Ci::ResourceGroup.count }.by(1) + + expect(project.resource_groups.find_by_key('production')).to be_present + expect(job.resource_group.key).to eq('production') + expect(job.options[:resource_group_key]).to be_nil + end + + context 'when a resource group has already been existed' do + before do + create(:ci_resource_group, project: project, key: 'production') + end + + it 'ensures the resource group existence' do + expect { subject }.not_to change { Ci::ResourceGroup.count } + + expect(project.resource_groups.find_by_key('production')).to be_present + expect(job.resource_group.key).to eq('production') + expect(job.options[:resource_group_key]).to be_nil + end + end + + context 'when a resource group key contains an invalid character' do + let!(:job) do + build(:ci_build, project: project, environment: '!!!', options: { resource_group_key: '$CI_ENVIRONMENT_NAME' }) + end + + it 'does not create any resource groups' do + expect { subject }.not_to change { Ci::ResourceGroup.count } + + expect(job.resource_group).to be_nil + end + end + + context 'when create_deployment_in_separate_transaction feature flag is disabled' do + before do + stub_feature_flags(create_deployment_in_separate_transaction: false) + end + + it 'does not create any resource groups' do + expect { subject }.not_to change { Ci::ResourceGroup.count } + + expect(job.resource_group).to be_nil + end + end + end + + context 'when a pipeline does not contain a job that requires a resource group' do + let!(:job) { build(:ci_build, project: project) } + + it 'does not create any resource groups' do + expect { subject }.not_to change { Ci::ResourceGroup.count } + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 73d827085b8..c53f1be1057 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -393,6 +393,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do describe '#to_resource' do subject { seed_build.to_resource } + before do + stub_feature_flags(create_deployment_in_separate_transaction: false) + end + context 'when job is Ci::Build' do it { is_expected.to be_a(::Ci::Build) } it { is_expected.to be_valid } @@ -443,6 +447,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it_behaves_like 'deployment job' it_behaves_like 'ensures environment existence' + context 'when create_deployment_in_separate_transaction feature flag is enabled' do + before do + stub_feature_flags(create_deployment_in_separate_transaction: true) + end + + it 'does not create any deployments nor environments' do + expect(subject.deployment).to be_nil + expect(Environment.count).to eq(0) + expect(Deployment.count).to eq(0) + end + end + context 'when the environment name is invalid' do let(:attributes) { { name: 'deploy', ref: 'master', environment: '!!!' } } @@ -496,6 +512,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'returns a job with resource group' do expect(subject.resource_group).not_to be_nil expect(subject.resource_group.key).to eq('iOS') + expect(Ci::ResourceGroup.count).to eq(1) + end + + context 'when create_deployment_in_separate_transaction feature flag is enabled' do + before do + stub_feature_flags(create_deployment_in_separate_transaction: true) + end + + it 'does not create any resource groups' do + expect(subject.resource_group).to be_nil + expect(Ci::ResourceGroup.count).to eq(0) + end end context 'when resource group has $CI_ENVIRONMENT_NAME in it' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index c3c35279fe8..66cb95a59a6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -199,7 +199,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do for_defined_days_back do user = create(:user) user2 = create(:user) - create(:event, author: user) create(:group_member, user: user) create(:authentication_event, user: user, provider: :ldapmain, result: :success) create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success) @@ -208,17 +207,24 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do create(:authentication_event, user: user, provider: :group_saml, result: :failed) end + for_defined_days_back(days: [31, 29, 3]) do + create(:event) + end + + stub_const('Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE', 1) + stub_const('Gitlab::Database::PostgresHll::BatchDistinctCounter::MIN_REQUIRED_BATCH_SIZE', 0) + expect(described_class.usage_activity_by_stage_manage({})).to include( events: -1, groups: 2, - users_created: 6, + users_created: 10, omniauth_providers: ['google_oauth2'], user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 } ) expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include( - events: be_within(error_rate).percent_of(1), + events: be_within(error_rate).percent_of(2), groups: 1, - users_created: 3, + users_created: 6, omniauth_providers: ['google_oauth2'], user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 } ) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 098ac21eb18..ad9bd0faf8a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -888,27 +888,6 @@ RSpec.describe Notify do end end - context 'with invite_email_from enabled', :experiment do - before do - stub_experiments(invite_email_from: :control) - end - - it 'has the correct invite_url with params' do - is_expected.to have_link('Join now', - href: invite_url(project_member.invite_token, - invite_type: Emails::Members::INITIAL_INVITE, - experiment_name: 'invite_email_from')) - end - - it 'tracks the sent invite' do - expect(experiment(:invite_email_from)).to track(:assignment) - .with_context(actor: project_member) - .on_next_instance - - invite_email.deliver_now - end - end - context 'when invite email sent is tracked', :snowplow do it 'tracks the sent invite' do invite_email.deliver_now diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9aa67057fca..730d7f02424 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3525,19 +3525,7 @@ RSpec.describe User do subject { user.membership_groups } - shared_examples 'returns groups where the user is a member' do - specify { is_expected.to contain_exactly(parent_group, child_group) } - end - - it_behaves_like 'returns groups where the user is a member' - - context 'when feature flag :linear_user_membership_groups is disabled' do - before do - stub_feature_flags(linear_user_membership_groups: false) - end - - it_behaves_like 'returns groups where the user is a member' - end + specify { is_expected.to contain_exactly(parent_group, child_group) } end describe '#authorizations_for_projects' do |