diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-10 18:10:03 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-10 18:10:03 +0300 |
commit | c74c13e2e1f3287e98f2519b098180bb30d358af (patch) | |
tree | d410b27b793c04ed5e909edf555b99b65214c8f5 /spec | |
parent | 63c306d96043ff012510358037c19053a2102e8a (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
26 files changed, 751 insertions, 396 deletions
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap index e56c37b0dc9..3c88c05a4b4 100644 --- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap +++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap @@ -26,8 +26,21 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen </div> </form> </li> - <!----> - <!----> + <li role=\\"presentation\\" class=\\"gl-new-dropdown-divider\\"> + <hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\"> + </li> + <li role=\\"presentation\\" class=\\"gl-new-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\"> + <!----> + <!----> + <!----> + <div class=\\"gl-new-dropdown-item-text-wrapper\\"> + <p class=\\"gl-new-dropdown-item-text-primary\\"> + Upload file + </p> + <!----> + </div> + <!----> + </button></li> <input type=\\"file\\" name=\\"content_editor_attachment\\" class=\\"gl-display-none\\"> </div> <!----> </div> diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js index bf744d5d385..dab7e67d7c5 100644 --- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_image_button_spec.js @@ -1,6 +1,7 @@ import { GlButton, GlFormInputGroup } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue'; +import Attachment from '~/content_editor/extensions/attachment'; import Image from '~/content_editor/extensions/image'; import { createTestEditor, mockChainedCommands } from '../test_utils'; @@ -31,7 +32,8 @@ describe('content_editor/components/toolbar_image_button', () => { beforeEach(() => { editor = createTestEditor({ extensions: [ - Image.configure({ + Image, + Attachment.configure({ renderMarkdown: jest.fn(), uploadsPath: '/uploads/', }), @@ -64,13 +66,13 @@ describe('content_editor/components/toolbar_image_button', () => { }); it('uploads the selected image when file input changes', async () => { - const commands = mockChainedCommands(editor, ['focus', 'uploadImage', 'run']); + const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']); const file = new File(['foo'], 'foo.png', { type: 'image/png' }); await selectFile(file); expect(commands.focus).toHaveBeenCalled(); - expect(commands.uploadImage).toHaveBeenCalledWith({ file }); + expect(commands.uploadAttachment).toHaveBeenCalledWith({ file }); expect(commands.run).toHaveBeenCalled(); expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]); diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js index 405213d0487..0cf488260bd 100644 --- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui'; +import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue'; import Link from '~/content_editor/extensions/link'; @@ -19,11 +19,18 @@ describe('content_editor/components/toolbar_link_button', () => { }); }; const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider); const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]'); const findApplyLinkButton = () => wrapper.findComponent(GlButton); const findRemoveLinkButton = () => wrapper.findByText('Remove link'); + const selectFile = async (file) => { + const input = wrapper.find({ ref: 'fileSelector' }); + + // override the property definition because `input.files` isn't directly modifyable + Object.defineProperty(input.element, 'files', { value: [file], writable: true }); + await input.trigger('change'); + }; + beforeEach(() => { editor = createTestEditor(); }); @@ -51,8 +58,11 @@ describe('content_editor/components/toolbar_link_button', () => { expect(findDropdown().props('toggleClass')).toEqual({ active: true }); }); + it('does not display the upload file option', () => { + expect(wrapper.findByText('Upload file').exists()).toBe(false); + }); + it('displays a remove link dropdown option', () => { - expect(findDropdownDivider().exists()).toBe(true); expect(wrapper.findByText('Remove link').exists()).toBe(true); }); @@ -107,7 +117,7 @@ describe('content_editor/components/toolbar_link_button', () => { }); }); - describe('when there is not an active link', () => { + describe('when there is no active link', () => { beforeEach(() => { jest.spyOn(editor, 'isActive'); editor.isActive.mockReturnValueOnce(false); @@ -118,8 +128,11 @@ describe('content_editor/components/toolbar_link_button', () => { expect(findDropdown().props('toggleClass')).toEqual({ active: false }); }); + it('displays the upload file option', () => { + expect(wrapper.findByText('Upload file').exists()).toBe(true); + }); + it('does not display a remove link dropdown option', () => { - expect(findDropdownDivider().exists()).toBe(false); expect(wrapper.findByText('Remove link').exists()).toBe(false); }); @@ -138,6 +151,19 @@ describe('content_editor/components/toolbar_link_button', () => { expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]); }); + + it('uploads the selected image when file input changes', async () => { + const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']); + const file = new File(['foo'], 'foo.png', { type: 'image/png' }); + + await selectFile(file); + + expect(commands.focus).toHaveBeenCalled(); + expect(commands.uploadAttachment).toHaveBeenCalledWith({ file }); + expect(commands.run).toHaveBeenCalled(); + + expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]); + }); }); describe('when the user displays the dropdown', () => { diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js new file mode 100644 index 00000000000..d87a1459b50 --- /dev/null +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -0,0 +1,235 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { once } from 'lodash'; +import waitForPromises from 'helpers/wait_for_promises'; +import Attachment from '~/content_editor/extensions/attachment'; +import Image from '~/content_editor/extensions/image'; +import Link from '~/content_editor/extensions/link'; +import Loading from '~/content_editor/extensions/loading'; +import httpStatus from '~/lib/utils/http_status'; +import { loadMarkdownApiResult } from '../markdown_processing_examples'; +import { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/extensions/image', () => { + let tiptapEditor; + let eq; + let doc; + let p; + let image; + let loading; + let link; + let renderMarkdown; + let mock; + + const uploadsPath = '/uploads/'; + const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' }); + const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' }); + + beforeEach(() => { + renderMarkdown = jest.fn(); + + tiptapEditor = createTestEditor({ + extensions: [Loading, Link, Image, Attachment.configure({ renderMarkdown, uploadsPath })], + }); + + ({ + builders: { doc, p, image, loading, link }, + eq, + } = createDocBuilder({ + tiptapEditor, + names: { + loading: { markType: Loading.name }, + image: { nodeType: Image.name }, + link: { nodeType: Link.name }, + }, + })); + + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + it.each` + eventType | propName | eventData | output + ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [attachmentFile] } }} | ${true} + ${'paste'} | ${'handlePaste'} | ${{ clipboardData: { files: [] } }} | ${undefined} + ${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { files: [attachmentFile] } }} | ${true} + `('handles $eventType properly', ({ eventType, propName, eventData, output }) => { + const event = Object.assign(new Event(eventType), eventData); + const handled = tiptapEditor.view.someProp(propName, (eventHandler) => { + return eventHandler(tiptapEditor.view, event); + }); + + expect(handled).toBe(output); + }); + + describe('uploadAttachment command', () => { + let initialDoc; + beforeEach(() => { + initialDoc = doc(p('')); + tiptapEditor.commands.setContent(initialDoc.toJSON()); + }); + + describe('when the file has image mime type', () => { + const base64EncodedFile = 'data:image/png;base64,Zm9v'; + + beforeEach(() => { + renderMarkdown.mockResolvedValue( + loadMarkdownApiResult('project_wiki_attachment_image').body, + ); + }); + + describe('when uploading succeeds', () => { + const successResponse = { + link: { + markdown: '![test-file](test-file.png)', + }, + }; + + beforeEach(() => { + mock.onPost().reply(httpStatus.OK, successResponse); + }); + + it('inserts an image with src set to the encoded image file and uploading true', (done) => { + const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile }))); + + tiptapEditor.on( + 'update', + once(() => { + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + done(); + }), + ); + + tiptapEditor.commands.uploadAttachment({ file: imageFile }); + }); + + it('updates the inserted image with canonicalSrc when upload is successful', async () => { + const expectedDoc = doc( + p( + image({ + canonicalSrc: 'test-file.png', + src: base64EncodedFile, + alt: 'test-file', + uploading: false, + }), + ), + ); + + tiptapEditor.commands.uploadAttachment({ file: imageFile }); + + await waitForPromises(); + + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + }); + }); + + describe('when uploading request fails', () => { + beforeEach(() => { + mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR); + }); + + it('resets the doc to orginal state', async () => { + const expectedDoc = doc(p('')); + + tiptapEditor.commands.uploadAttachment({ file: imageFile }); + + await waitForPromises(); + + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + }); + + it('emits an error event that includes an error message', (done) => { + tiptapEditor.commands.uploadAttachment({ file: imageFile }); + + tiptapEditor.on('error', (message) => { + expect(message).toBe('An error occurred while uploading the image. Please try again.'); + done(); + }); + }); + }); + }); + + describe('when the file has a zip (or any other attachment) mime type', () => { + const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link').body; + + beforeEach(() => { + renderMarkdown.mockResolvedValue(markdownApiResult); + }); + + describe('when uploading succeeds', () => { + const successResponse = { + link: { + markdown: '[test-file](test-file.zip)', + }, + }; + + beforeEach(() => { + mock.onPost().reply(httpStatus.OK, successResponse); + }); + + it('inserts a loading mark', (done) => { + const expectedDoc = doc(p(loading({ label: 'test-file' }))); + + tiptapEditor.on( + 'update', + once(() => { + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + done(); + }), + ); + + tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); + }); + + it('updates the loading mark with a link with canonicalSrc and href attrs', async () => { + const [, group, project] = markdownApiResult.match(/\/(group[0-9]+)\/(project[0-9]+)\//); + const expectedDoc = doc( + p( + link( + { + canonicalSrc: 'test-file.zip', + href: `/${group}/${project}/-/wikis/test-file.zip`, + }, + 'test-file', + ), + ), + ); + + tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); + + await waitForPromises(); + + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + }); + }); + + describe('when uploading request fails', () => { + beforeEach(() => { + mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR); + }); + + it('resets the doc to orginal state', async () => { + const expectedDoc = doc(p('')); + + tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); + + await waitForPromises(); + + expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); + }); + + it('emits an error event that includes an error message', (done) => { + tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); + + tiptapEditor.on('error', (message) => { + expect(message).toBe('An error occurred while uploading the file. Please try again.'); + done(); + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js deleted file mode 100644 index 09b7274839e..00000000000 --- a/spec/frontend/content_editor/extensions/image_spec.js +++ /dev/null @@ -1,193 +0,0 @@ -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import { once } from 'lodash'; -import waitForPromises from 'helpers/wait_for_promises'; -import Image from '~/content_editor/extensions/image'; -import httpStatus from '~/lib/utils/http_status'; -import { loadMarkdownApiResult } from '../markdown_processing_examples'; -import { createTestEditor, createDocBuilder } from '../test_utils'; - -describe('content_editor/extensions/image', () => { - let tiptapEditor; - let eq; - let doc; - let p; - let image; - let renderMarkdown; - let mock; - const uploadsPath = '/uploads/'; - const validFile = new File(['foo'], 'foo.png', { type: 'image/png' }); - const invalidFile = new File(['foo'], 'bar.html', { type: 'text/html' }); - - beforeEach(() => { - renderMarkdown = jest - .fn() - .mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image').body); - - tiptapEditor = createTestEditor({ - extensions: [Image.configure({ renderMarkdown, uploadsPath })], - }); - - ({ - builders: { doc, p, image }, - eq, - } = createDocBuilder({ - tiptapEditor, - names: { image: { nodeType: Image.name } }, - })); - - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.reset(); - }); - - it.each` - file | valid | description - ${validFile} | ${true} | ${'handles paste event when mime type is valid'} - ${invalidFile} | ${false} | ${'does not handle paste event when mime type is invalid'} - `('$description', ({ file, valid }) => { - const pasteEvent = Object.assign(new Event('paste'), { - clipboardData: { - files: [file], - }, - }); - let handled; - - tiptapEditor.view.someProp('handlePaste', (eventHandler) => { - handled = eventHandler(tiptapEditor.view, pasteEvent); - }); - - expect(handled).toBe(valid); - }); - - it.each` - file | valid | description - ${validFile} | ${true} | ${'handles drop event when mime type is valid'} - ${invalidFile} | ${false} | ${'does not handle drop event when mime type is invalid'} - `('$description', ({ file, valid }) => { - const dropEvent = Object.assign(new Event('drop'), { - dataTransfer: { - files: [file], - }, - }); - let handled; - - tiptapEditor.view.someProp('handleDrop', (eventHandler) => { - handled = eventHandler(tiptapEditor.view, dropEvent); - }); - - expect(handled).toBe(valid); - }); - - it('handles paste event when mime type is correct', () => { - const pasteEvent = Object.assign(new Event('paste'), { - clipboardData: { - files: [new File(['foo'], 'foo.png', { type: 'image/png' })], - }, - }); - const handled = tiptapEditor.view.someProp('handlePaste', (eventHandler) => { - return eventHandler(tiptapEditor.view, pasteEvent); - }); - - expect(handled).toBe(true); - }); - - describe('uploadImage command', () => { - describe('when file has correct mime type', () => { - let initialDoc; - const base64EncodedFile = 'data:image/png;base64,Zm9v'; - - beforeEach(() => { - initialDoc = doc(p('')); - tiptapEditor.commands.setContent(initialDoc.toJSON()); - }); - - describe('when uploading image succeeds', () => { - const successResponse = { - link: { - markdown: '[image](/uploads/25265/image.png)', - }, - }; - - beforeEach(() => { - mock.onPost().reply(httpStatus.OK, successResponse); - }); - - it('inserts an image with src set to the encoded image file and uploading true', (done) => { - const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile }))); - - tiptapEditor.on( - 'update', - once(() => { - expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); - done(); - }), - ); - - tiptapEditor.commands.uploadImage({ file: validFile }); - }); - - it('updates the inserted image with canonicalSrc when upload is successful', async () => { - const expectedDoc = doc( - p( - image({ - canonicalSrc: 'test-file.png', - src: base64EncodedFile, - alt: 'test file', - uploading: false, - }), - ), - ); - - tiptapEditor.commands.uploadImage({ file: validFile }); - - await waitForPromises(); - - expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); - }); - }); - - describe('when uploading image request fails', () => { - beforeEach(() => { - mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR); - }); - - it('resets the doc to orginal state', async () => { - const expectedDoc = doc(p('')); - - tiptapEditor.commands.uploadImage({ file: validFile }); - - await waitForPromises(); - - expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); - }); - - it('emits an error event that includes an error message', (done) => { - tiptapEditor.commands.uploadImage({ file: validFile }); - - tiptapEditor.on('error', (message) => { - expect(message).toBe('An error occurred while uploading the image. Please try again.'); - done(); - }); - }); - }); - }); - - describe('when file does not have correct mime type', () => { - let initialDoc; - - beforeEach(() => { - initialDoc = doc(p('')); - tiptapEditor.commands.setContent(initialDoc.toJSON()); - }); - - it('does not start the upload image process', () => { - tiptapEditor.commands.uploadImage({ file: invalidFile }); - - expect(eq(tiptapEditor.state.doc, initialDoc)).toBe(true); - }); - }); - }); -}); diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js index a6d52ddabef..b78e7f0862d 100644 --- a/spec/frontend/content_editor/services/create_content_editor_spec.js +++ b/spec/frontend/content_editor/services/create_content_editor_spec.js @@ -52,9 +52,9 @@ describe('content_editor/services/create_editor', () => { expect(() => createContentEditor()).toThrow(PROVIDE_SERIALIZER_OR_RENDERER_ERROR); }); - it('provides uploadsPath and renderMarkdown function to Image extension', () => { + it('provides uploadsPath and renderMarkdown function to Attachment extension', () => { expect( - editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'image').options, + editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'attachment').options, ).toMatchObject({ uploadsPath, renderMarkdown, diff --git a/spec/frontend/content_editor/services/upload_file_spec.js b/spec/frontend/content_editor/services/upload_helpers_spec.js index 87c5298079e..ee9333232db 100644 --- a/spec/frontend/content_editor/services/upload_file_spec.js +++ b/spec/frontend/content_editor/services/upload_helpers_spec.js @@ -1,9 +1,9 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { uploadFile } from '~/content_editor/services/upload_file'; +import { uploadFile } from '~/content_editor/services/upload_helpers'; import httpStatus from '~/lib/utils/http_status'; -describe('content_editor/services/upload_file', () => { +describe('content_editor/services/upload_helpers', () => { const uploadsPath = '/uploads'; const file = new File(['content'], 'file.txt'); // TODO: Replace with automated fixture diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index f217dfd2e48..467a8bec21b 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -258,7 +258,11 @@ describe('issue_note', () => { }, }); - noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {}); + noteBodyComponent.vm.$emit('handleFormUpdate', { + noteText: noteBody, + parentElement: null, + callback: () => {}, + }); await waitForPromises(); expect(alertSpy).not.toHaveBeenCalled(); @@ -287,14 +291,18 @@ describe('issue_note', () => { const noteBody = wrapper.findComponent(NoteBody); noteBody.vm.resetAutoSave = () => {}; - noteBody.vm.$emit('handleFormUpdate', updatedText, null, () => {}); + noteBody.vm.$emit('handleFormUpdate', { + noteText: updatedText, + parentElement: null, + callback: () => {}, + }); await wrapper.vm.$nextTick(); let noteBodyProps = noteBody.props(); expect(noteBodyProps.note.note_html).toBe(`<p>${updatedText}</p>\n`); - noteBody.vm.$emit('cancelForm'); + noteBody.vm.$emit('cancelForm', {}); await wrapper.vm.$nextTick(); noteBodyProps = noteBody.props(); @@ -305,7 +313,12 @@ describe('issue_note', () => { describe('formUpdateHandler', () => { const updateNote = jest.fn(); - const params = ['', null, jest.fn(), '']; + const params = { + noteText: '', + parentElement: null, + callback: jest.fn(), + resolveDiscussion: false, + }; const updateActions = () => { store.hotUpdate({ @@ -325,14 +338,14 @@ describe('issue_note', () => { it('responds to handleFormUpdate', () => { createWrapper(); updateActions(); - wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', ...params); + wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params); expect(wrapper.emitted('handleUpdateNote')).toBeTruthy(); }); it('does not stringify empty position', () => { createWrapper(); updateActions(); - wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', ...params); + wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params); expect(updateNote.mock.calls[0][1].note.note.position).toBeUndefined(); }); @@ -341,7 +354,7 @@ describe('issue_note', () => { const expectation = JSON.stringify(position); createWrapper({ note: { ...note, position } }); updateActions(); - wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', ...params); + wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params); expect(updateNote.mock.calls[0][1].note.note.position).toBe(expectation); }); }); diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js index b8299d44f13..84863eac3d3 100644 --- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js +++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js @@ -3,6 +3,7 @@ import Vuex from 'vuex'; import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue'; import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue'; import { getStoreConfig } from '~/reports/codequality_report/store'; +import { STATUS_NOT_FOUND } from '~/reports/constants'; import { parsedReportIssues } from './mock_data'; const localVue = createLocalVue(); @@ -14,8 +15,6 @@ describe('Grouped code quality reports app', () => { const PATHS = { codequalityHelpPath: 'codequality_help.html', - basePath: 'base.json', - headPath: 'head.json', baseBlobPath: 'base/blob/path/', headBlobPath: 'head/blob/path/', }; @@ -127,21 +126,6 @@ describe('Grouped code quality reports app', () => { }); }); - describe('when there is a head report but no base report', () => { - beforeEach(() => { - mockStore.state.basePath = null; - mockStore.state.hasError = true; - }); - - it('renders error text', () => { - expect(findWidget().text()).toContain('Failed to load codeclimate report'); - }); - - it('renders a help icon with more information', () => { - expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(true); - }); - }); - describe('on error', () => { beforeEach(() => { mockStore.state.hasError = true; @@ -154,5 +138,15 @@ describe('Grouped code quality reports app', () => { it('does not render a help icon', () => { expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(false); }); + + describe('when base report was not found', () => { + beforeEach(() => { + mockStore.state.status = STATUS_NOT_FOUND; + }); + + it('renders a help icon with more information', () => { + expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(true); + }); + }); }); }); diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js index 2255b676074..1821390786b 100644 --- a/spec/frontend/reports/codequality_report/store/actions_spec.js +++ b/spec/frontend/reports/codequality_report/store/actions_spec.js @@ -5,6 +5,7 @@ import axios from '~/lib/utils/axios_utils'; import createStore from '~/reports/codequality_report/store'; import * as actions from '~/reports/codequality_report/store/actions'; import * as types from '~/reports/codequality_report/store/mutation_types'; +import { STATUS_NOT_FOUND } from '~/reports/constants'; import { reportIssues, parsedReportIssues } from '../mock_data'; const pollInterval = 123; @@ -24,8 +25,6 @@ describe('Codequality Reports actions', () => { describe('setPaths', () => { it('should commit SET_PATHS mutation', (done) => { const paths = { - basePath: 'basePath', - headPath: 'headPath', baseBlobPath: 'baseBlobPath', headBlobPath: 'headBlobPath', reportsPath: 'reportsPath', @@ -49,7 +48,6 @@ describe('Codequality Reports actions', () => { beforeEach(() => { localState.reportsPath = endpoint; - localState.basePath = '/base/path'; mock = new MockAdapter(axios); }); @@ -92,16 +90,17 @@ describe('Codequality Reports actions', () => { }); }); - describe('with no base path', () => { + describe('when base report is not found', () => { it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => { - localState.basePath = null; + const data = { status: STATUS_NOT_FOUND }; + mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, data); testAction( actions.fetchReports, null, localState, [{ type: types.REQUEST_REPORTS }], - [{ type: 'receiveReportsError' }], + [{ type: 'receiveReportsError', payload: data }], done, ); }); diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/reports/codequality_report/store/getters_spec.js index de025f814ef..0378171084d 100644 --- a/spec/frontend/reports/codequality_report/store/getters_spec.js +++ b/spec/frontend/reports/codequality_report/store/getters_spec.js @@ -1,6 +1,6 @@ import createStore from '~/reports/codequality_report/store'; import * as getters from '~/reports/codequality_report/store/getters'; -import { LOADING, ERROR, SUCCESS } from '~/reports/constants'; +import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '~/reports/constants'; describe('Codequality reports store getters', () => { let localState; @@ -76,10 +76,9 @@ describe('Codequality reports store getters', () => { }); describe('codequalityPopover', () => { - describe('when head report is available but base report is not', () => { + describe('when base report is not available', () => { it('returns a popover with a documentation link', () => { - localState.headPath = 'head.json'; - localState.basePath = undefined; + localState.status = STATUS_NOT_FOUND; localState.helpPath = 'codequality_help.html'; expect(getters.codequalityPopover(localState).title).toEqual( diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/reports/codequality_report/store/mutations_spec.js index 8bc6bb26c2a..6e14cd7438b 100644 --- a/spec/frontend/reports/codequality_report/store/mutations_spec.js +++ b/spec/frontend/reports/codequality_report/store/mutations_spec.js @@ -1,5 +1,6 @@ import createStore from '~/reports/codequality_report/store'; import mutations from '~/reports/codequality_report/store/mutations'; +import { STATUS_NOT_FOUND } from '~/reports/constants'; describe('Codequality Reports mutations', () => { let localState; @@ -12,24 +13,18 @@ describe('Codequality Reports mutations', () => { describe('SET_PATHS', () => { it('sets paths to given values', () => { - const basePath = 'base.json'; - const headPath = 'head.json'; const baseBlobPath = 'base/blob/path/'; const headBlobPath = 'head/blob/path/'; const reportsPath = 'reports.json'; const helpPath = 'help.html'; mutations.SET_PATHS(localState, { - basePath, - headPath, baseBlobPath, headBlobPath, reportsPath, helpPath, }); - expect(localState.basePath).toEqual(basePath); - expect(localState.headPath).toEqual(headPath); expect(localState.baseBlobPath).toEqual(baseBlobPath); expect(localState.headBlobPath).toEqual(headBlobPath); expect(localState.reportsPath).toEqual(reportsPath); @@ -58,9 +53,10 @@ describe('Codequality Reports mutations', () => { expect(localState.hasError).toEqual(false); }); - it('clears statusReason', () => { + it('clears status and statusReason', () => { mutations.RECEIVE_REPORTS_SUCCESS(localState, {}); + expect(localState.status).toEqual(''); expect(localState.statusReason).toEqual(''); }); @@ -86,6 +82,13 @@ describe('Codequality Reports mutations', () => { expect(localState.hasError).toEqual(true); }); + it('sets status based on error object', () => { + const error = { status: STATUS_NOT_FOUND }; + mutations.RECEIVE_REPORTS_ERROR(localState, error); + + expect(localState.status).toEqual(error.status); + }); + it('sets statusReason to string from error response data', () => { const data = { status_reason: 'This merge request does not have codequality reports' }; const error = { response: { data } }; diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js index e6f1e15d718..15dcbb99623 100644 --- a/spec/frontend/vue_mr_widget/mock_data.js +++ b/spec/frontend/vue_mr_widget/mock_data.js @@ -234,14 +234,11 @@ export default { can_revert_on_current_merge_request: true, can_cherry_pick_on_current_merge_request: true, }, - codeclimate: { - head_path: 'head.json', - base_path: 'base.json', - }, blob_path: { base_path: 'blob_path', head_path: 'blob_path', }, + codequality_reports_path: 'codequality_reports.json', codequality_help_path: 'code_quality.html', target_branch_path: '/root/acets-app/branches/main', source_branch_path: '/root/acets-app/branches/daaaa', diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 5703bfeaea7..8447b92adbf 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -263,42 +263,6 @@ RSpec.describe GroupsHelper do end end - describe '#group_container_registry_nav' do - let_it_be(:group) { create(:group, :public) } - let_it_be(:user) { create(:user) } - - before do - stub_container_registry_config(enabled: true) - allow(helper).to receive(:current_user) { user } - allow(helper).to receive(:can?).with(user, :read_container_image, group) { true } - helper.instance_variable_set(:@group, group) - end - - subject { helper.group_container_registry_nav? } - - context 'when container registry is enabled' do - it { is_expected.to be_truthy } - - it 'is disabled for guest' do - allow(helper).to receive(:can?).with(user, :read_container_image, group) { false } - expect(subject).to be false - end - end - - context 'when container registry is not enabled' do - before do - stub_container_registry_config(enabled: false) - end - - it { is_expected.to be_falsy } - - it 'is disabled for guests' do - allow(helper).to receive(:can?).with(user, :read_container_image, group) { false } - expect(subject).to be false - end - end - end - describe '#group_sidebar_links' do let_it_be(:group) { create(:group, :public) } let_it_be(:user) { create(:user) } diff --git a/spec/lib/gitlab/database/connection_spec.rb b/spec/lib/gitlab/database/connection_spec.rb index 517d40deb1c..52e43fb0f61 100644 --- a/spec/lib/gitlab/database/connection_spec.rb +++ b/spec/lib/gitlab/database/connection_spec.rb @@ -378,42 +378,6 @@ RSpec.describe Gitlab::Database::Connection do end end - describe '#create_connection_pool' do - it 'creates a new connection pool with specific pool size' do - pool = connection.create_connection_pool(5) - - begin - expect(pool) - .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) - - expect(pool.db_config.pool).to eq(5) - ensure - pool.disconnect! - end - end - - it 'allows setting of a custom hostname' do - pool = connection.create_connection_pool(5, '127.0.0.1') - - begin - expect(pool.db_config.host).to eq('127.0.0.1') - ensure - pool.disconnect! - end - end - - it 'allows setting of a custom hostname and port' do - pool = connection.create_connection_pool(5, '127.0.0.1', 5432) - - begin - expect(pool.db_config.host).to eq('127.0.0.1') - expect(pool.db_config.configuration_hash[:port]).to eq(5432) - ensure - pool.disconnect! - end - end - end - describe '#cached_column_exists?' do it 'only retrieves data once' do expect(connection.scope.connection) diff --git a/spec/lib/gitlab/database/load_balancing/host_list_spec.rb b/spec/lib/gitlab/database/load_balancing/host_list_spec.rb index 6b88505de1a..6a358b5d430 100644 --- a/spec/lib/gitlab/database/load_balancing/host_list_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/host_list_spec.rb @@ -3,25 +3,17 @@ require 'spec_helper' RSpec.describe Gitlab::Database::LoadBalancing::HostList do - def expect_metrics(hosts) - expect(Gitlab::Metrics.registry.get(:db_load_balancing_hosts).get({})).to eq(hosts) - end - - before do - allow(Gitlab::Database.main) - .to receive(:create_connection_pool) - .and_return(ActiveRecord::Base.connection_pool) - end - + let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host } let(:load_balancer) { double(:load_balancer) } let(:host_count) { 2 } + let(:hosts) { Array.new(host_count) { Gitlab::Database::LoadBalancing::Host.new(db_host, load_balancer, port: 5432) } } + let(:host_list) { described_class.new(hosts) } - let(:host_list) do - hosts = Array.new(host_count) do - Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer, port: 5432) + before do + # each call generate a new replica pool + allow(load_balancer).to receive(:create_replica_connection_pool) do + double(:replica_connection_pool) end - - described_class.new(hosts) end describe '#initialize' do @@ -42,8 +34,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do context 'with ports' do it 'returns the host names of all hosts' do hosts = [ - ['localhost', 5432], - ['localhost', 5432] + [db_host, 5432], + [db_host, 5432] ] expect(host_list.host_names_and_ports).to eq(hosts) @@ -51,18 +43,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do end context 'without ports' do - let(:host_list) do - hosts = Array.new(2) do - Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer) - end - - described_class.new(hosts) - end + let(:hosts) { Array.new(2) { Gitlab::Database::LoadBalancing::Host.new(db_host, load_balancer) } } it 'returns the host names of all hosts' do hosts = [ - ['localhost', nil], - ['localhost', nil] + [db_host, nil], + [db_host, nil] ] expect(host_list.host_names_and_ports).to eq(hosts) @@ -71,10 +57,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do end describe '#manage_pool?' do - before do - allow(Gitlab::Database.main).to receive(:create_connection_pool) { double(:connection) } - end - context 'when the testing pool belongs to one host of the host list' do it 'returns true' do pool = host_list.hosts.first.pool @@ -185,4 +167,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::HostList do end end end + + def expect_metrics(hosts) + expect(Gitlab::Metrics.registry.get(:db_load_balancing_hosts).get({})).to eq(hosts) + end end diff --git a/spec/lib/gitlab/database/load_balancing/host_spec.rb b/spec/lib/gitlab/database/load_balancing/host_spec.rb index 23467e0ae34..f42ac8be1bb 100644 --- a/spec/lib/gitlab/database/load_balancing/host_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/host_spec.rb @@ -3,15 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::Database::LoadBalancing::Host do - let(:load_balancer) do - Gitlab::Database::LoadBalancing::LoadBalancer.new(%w[localhost]) - end + let(:load_balancer) { Gitlab::Database::LoadBalancing::LoadBalancer.new } - let(:host) { load_balancer.host_list.hosts.first } + let(:host) do + Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer) + end before do - allow(Gitlab::Database.main).to receive(:create_connection_pool) - .and_return(ActiveRecord::Base.connection_pool) + allow(load_balancer).to receive(:create_replica_connection_pool) do + ActiveRecord::Base.connection_pool + end end def raise_and_wrap(wrapper, original) @@ -63,7 +64,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do expect(host.pool) .to receive(:disconnect!) - host.disconnect!(1) + host.disconnect!(timeout: 1) end end diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb index 01a17cb2805..358f382bc39 100644 --- a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb @@ -3,21 +3,22 @@ require 'spec_helper' RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do - let(:pool) { Gitlab::Database.main.create_connection_pool(2) } let(:conflict_error) { Class.new(RuntimeError) } - - let(:lb) { described_class.new(%w(localhost localhost)) } + let(:db_host) { ActiveRecord::Base.connection_pool.db_config.host } + let(:lb) { described_class.new([db_host, db_host]) } let(:request_cache) { lb.send(:request_cache) } before do - allow(Gitlab::Database.main).to receive(:create_connection_pool) - .and_return(pool) stub_const( 'Gitlab::Database::LoadBalancing::LoadBalancer::PG::TRSerializationFailure', conflict_error ) end + after do |example| + lb.disconnect!(timeout: 0) unless example.metadata[:skip_disconnect] + end + def raise_and_wrap(wrapper, original) raise original rescue original.class @@ -239,7 +240,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do context 'when the connection comes from the primary pool' do it 'returns :primary' do connection = double(:connection) - allow(connection).to receive(:pool).and_return(ActiveRecord::Base.connection_pool) + allow(connection).to receive(:pool).and_return(lb.send(:pool)) expect(lb.db_role_for_connection(connection)).to be(:primary) end @@ -271,8 +272,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do end it 'does not create conflicts with other load balancers when caching hosts' do - lb1 = described_class.new(%w(localhost localhost), ActiveRecord::Base) - lb2 = described_class.new(%w(localhost localhost), Ci::CiDatabaseRecord) + lb1 = described_class.new([db_host, db_host], ActiveRecord::Base) + lb2 = described_class.new([db_host, db_host], Ci::CiDatabaseRecord) host1 = lb1.host host2 = lb2.host @@ -456,4 +457,45 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do end end end + + describe '#create_replica_connection_pool' do + it 'creates a new connection pool with specific pool size and name' do + with_replica_pool(5, 'other_host') do |replica_pool| + expect(replica_pool) + .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) + + expect(replica_pool.db_config.host).to eq('other_host') + expect(replica_pool.db_config.pool).to eq(5) + expect(replica_pool.db_config.name).to end_with("_replica") + end + end + + it 'allows setting of a custom hostname and port' do + with_replica_pool(5, 'other_host', 5432) do |replica_pool| + expect(replica_pool.db_config.host).to eq('other_host') + expect(replica_pool.db_config.configuration_hash[:port]).to eq(5432) + end + end + + it 'does not modify connection class pool' do + expect { with_replica_pool(5) { } }.not_to change { ActiveRecord::Base.connection_pool } + end + + def with_replica_pool(*args) + pool = lb.create_replica_connection_pool(*args) + yield pool + ensure + pool&.disconnect! + end + end + + describe '#disconnect!' do + it 'calls disconnect on all hosts with a timeout', :skip_disconnect do + expect_next_instances_of(Gitlab::Database::LoadBalancing::Host, 2) do |host| + expect(host).to receive(:disconnect!).with(timeout: 30) + end + + lb.disconnect!(timeout: 30) + end + end end diff --git a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb index b65b68c463e..c853e827144 100644 --- a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb @@ -169,7 +169,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do expect(host) .to receive(:disconnect!) - .with(2) + .with(timeout: 2) service.replace_hosts([address_bar]) end diff --git a/spec/lib/gitlab/database/load_balancing_spec.rb b/spec/lib/gitlab/database/load_balancing_spec.rb index ba5aae110ca..fb482061d7c 100644 --- a/spec/lib/gitlab/database/load_balancing_spec.rb +++ b/spec/lib/gitlab/database/load_balancing_spec.rb @@ -3,10 +3,6 @@ require 'spec_helper' RSpec.describe Gitlab::Database::LoadBalancing do - before do - stub_env('ENABLE_LOAD_BALANCING_FOR_FOSS', 'true') - end - describe '.proxy' do before do @previous_proxy = ActiveRecord::Base.load_balancing_proxy diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb index f3c3e5fc550..a7cff80e43a 100644 --- a/spec/lib/gitlab/usage/metric_definition_spec.rb +++ b/spec/lib/gitlab/usage/metric_definition_spec.rb @@ -87,14 +87,14 @@ RSpec.describe Gitlab::Usage::MetricDefinition do end it 'raise exception' do - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError)) described_class.new(path, attributes).validate! end context 'with skip_validation' do it 'raise exception if skip_validation: false' do - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError)) described_class.new(path, attributes.merge( { skip_validation: false } )).validate! end @@ -113,7 +113,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do attributes[:status] = 'broken' attributes.delete(:repair_issue_url) - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError)) described_class.new(path, attributes).validate! end @@ -173,7 +173,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do write_metric(metric1, path, yaml_content) write_metric(metric2, path, yaml_content) - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError)) + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError)) subject end diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb index d4a789419a4..d83f59e4a7d 100644 --- a/spec/lib/gitlab/usage/metric_spec.rb +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -3,27 +3,46 @@ require 'spec_helper' RSpec.describe Gitlab::Usage::Metric do - describe '#definition' do - it 'returns key_path metric definiton' do - expect(described_class.new(key_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition) - end + let!(:issue) { create(:issue) } + + let(:attributes) do + { + data_category: "Operational", + key_path: "counts.issues", + description: "Count of Issues created", + product_section: "dev", + product_stage: "plan", + product_group: "group::plan", + product_category: "issue_tracking", + value_type: "number", + status: "data_available", + time_frame: "all", + data_source: "database", + instrumentation_class: "CountIssuesMetric", + distribution: %w(ce ee), + tier: %w(free premium ultimate) + } end - describe '#unflatten_default_path' do - using RSpec::Parameterized::TableSyntax + let(:issue_count_metric_definiton) do + double(:issue_count_metric_definiton, + attributes.merge({ attributes: attributes }) + ) + end - where(:key_path, :value, :expected_hash) do - 'uuid' | nil | { uuid: nil } - 'uuid' | '1111' | { uuid: '1111' } - 'counts.issues' | nil | { counts: { issues: nil } } - 'counts.issues' | 100 | { counts: { issues: 100 } } - 'usage_activity_by_stage.verify.ci_builds' | 100 | { usage_activity_by_stage: { verify: { ci_builds: 100 } } } - end + before do + allow(ApplicationRecord.connection).to receive(:transaction_open?).and_return(false) + end - with_them do - subject { described_class.new(key_path: key_path, value: value).unflatten_key_path } + describe '#with_value' do + it 'returns key_path metric with the corresponding value' do + expect(described_class.new(issue_count_metric_definiton).with_value).to eq({ counts: { issues: 1 } }) + end + end - it { is_expected.to eq(expected_hash) } + describe '#with_instrumentation' do + it 'returns key_path metric with the corresponding generated query' do + expect(described_class.new(issue_count_metric_definiton).with_instrumentation).to eq({ counts: { issues: "SELECT COUNT(\"issues\".\"id\") FROM \"issues\"" } }) end end end diff --git a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb new file mode 100644 index 00000000000..5ebd67462f8 --- /dev/null +++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do + let_it_be(:owner) { create(:user) } + let_it_be(:group) do + build(:group, :private).tap do |g| + g.add_owner(owner) + end + end + + let(:user) { owner } + let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) } + let(:menu) { described_class.new(context) } + + describe '#render?' do + context 'when menu has menu items to show' do + it 'returns true' do + expect(menu.render?).to eq true + end + end + + context 'when menu does not have any menu item to show' do + it 'returns false' do + stub_container_registry_config(enabled: false) + stub_config(packages: { enabled: false }) + stub_config(dependency_proxy: { enabled: false }) + + expect(menu.render?).to eq false + end + end + end + + describe '#link' do + let(:registry_enabled) { true } + let(:packages_enabled) { true } + + before do + stub_container_registry_config(enabled: registry_enabled) + stub_config(packages: { enabled: packages_enabled }) + stub_config(dependency_proxy: { enabled: true }) + end + + subject { menu.link } + + context 'when Packages Registry is visible' do + it 'menu link points to Packages Registry page' do + expect(subject).to eq find_menu(menu, :packages_registry).link + end + end + + context 'when Packages Registry is not visible' do + let(:packages_enabled) { false } + + it 'menu link points to Container Registry page' do + expect(subject).to eq find_menu(menu, :container_registry).link + end + + context 'when Container Registry is not visible' do + let(:registry_enabled) { false } + + it 'menu link points to Dependency Proxy page' do + expect(subject).to eq find_menu(menu, :dependency_proxy).link + end + end + end + end + + describe 'Menu items' do + subject { find_menu(menu, item_id) } + + describe 'Packages Registry' do + let(:item_id) { :packages_registry } + + context 'when user can read packages' do + before do + stub_config(packages: { enabled: packages_enabled }) + end + + context 'when config package setting is disabled' do + let(:packages_enabled) { false } + + it 'the menu item is not added to list of menu items' do + is_expected.to be_nil + end + end + + context 'when config package setting is enabled' do + let(:packages_enabled) { true } + + it 'the menu item is added to list of menu items' do + is_expected.not_to be_nil + end + end + end + end + + describe 'Container Registry' do + let(:item_id) { :container_registry } + + context 'when user can read container images' do + before do + stub_container_registry_config(enabled: container_enabled) + end + + context 'when config registry setting is disabled' do + let(:container_enabled) { false } + + it 'the menu item is not added to list of menu items' do + is_expected.to be_nil + end + end + + context 'when config registry setting is enabled' do + let(:container_enabled) { true } + + it 'the menu item is added to list of menu items' do + is_expected.not_to be_nil + end + + context 'when user cannot read container images' do + let(:user) { nil } + + it 'the menu item is not added to list of menu items' do + is_expected.to be_nil + end + end + end + end + end + + describe 'Dependency Proxy' do + let(:item_id) { :dependency_proxy } + + before do + stub_config(dependency_proxy: { enabled: dependency_enabled }) + end + + context 'when config dependency_proxy is enabled' do + let(:dependency_enabled) { true } + + it 'the menu item is added to list of menu items' do + is_expected.not_to be_nil + end + end + + context 'when config dependency_proxy is not enabled' do + let(:dependency_enabled) { false } + + it 'the menu item is not added to list of menu items' do + is_expected.to be_nil + end + end + end + end + + private + + def find_menu(menu, item) + menu.renderable_items.find { |i| i.item_id == item } + end +end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index cdd46ca4ecc..74547196445 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -52,14 +52,14 @@ RSpec.describe 'Query.runner(id)' do 'version' => runner.version, 'shortSha' => runner.short_sha, 'revision' => runner.revision, - 'locked' => runner.locked, + 'locked' => false, 'active' => runner.active, 'status' => runner.status.to_s.upcase, 'maximumTimeout' => runner.maximum_timeout, 'accessLevel' => runner.access_level.to_s.upcase, 'runUntagged' => runner.run_untagged, 'ipAddress' => runner.ip_address, - 'runnerType' => 'INSTANCE_TYPE', + 'runnerType' => runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE', 'jobCount' => 0, 'projectCount' => nil ) @@ -109,6 +109,40 @@ RSpec.describe 'Query.runner(id)' do end end + describe 'for project runner' do + using RSpec::Parameterized::TableSyntax + + where(is_locked: [true, false]) + + with_them do + let(:project_runner) do + create(:ci_runner, :project, description: 'Runner 3', contacted_at: 1.day.ago, active: false, locked: is_locked, + version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) + end + + let(:query) do + wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner'))) + end + + let(:query_path) do + [ + [:runner, { id: project_runner.to_global_id.to_s }] + ] + end + + it 'retrieves correct locked value' do + post_graphql(query, current_user: user) + + runner_data = graphql_data_at(:runner) + + expect(runner_data).to match a_hash_including( + 'id' => "gid://gitlab/Ci::Runner/#{project_runner.id}", + 'locked' => is_locked + ) + end + end + end + describe 'for inactive runner' do it_behaves_like 'runner details fetch', :inactive_instance_runner end diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb index 4d3c14eceea..349cbf1b76c 100644 --- a/spec/requests/projects/merge_requests/diffs_spec.rb +++ b/spec/requests/projects/merge_requests/diffs_spec.rb @@ -76,6 +76,78 @@ RSpec.describe 'Merge Requests Diffs' do subject end + context 'with the different user' do + let(:another_user) { create(:user) } + + before do + project.add_maintainer(another_user) + sign_in(another_user) + end + + it_behaves_like 'serializes diffs with expected arguments' do + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20) } + end + end + + context 'with a new unfoldable diff position' do + let(:unfoldable_position) do + create(:diff_position) + end + + before do + expect_next_instance_of(Gitlab::Diff::PositionCollection) do |instance| + expect(instance) + .to receive(:unfoldable) + .and_return([unfoldable_position]) + end + end + + it_behaves_like 'serializes diffs with expected arguments' do + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20) } + end + end + + context 'with a new environment' do + let(:environment) do + create(:environment, :available, project: project) + end + + let!(:deployment) do + create(:deployment, :success, environment: environment, ref: merge_request.source_branch) + end + + it_behaves_like 'serializes diffs with expected arguments' do + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20).merge(environment: environment) } + end + end + + context 'with disabled display_merge_conflicts_in_diff feature' do + before do + stub_feature_flags(display_merge_conflicts_in_diff: false) + end + + it_behaves_like 'serializes diffs with expected arguments' do + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) } + end + end + + context 'with diff_head option' do + subject { go(page: 0, per_page: 5, diff_head: true) } + + before do + merge_request.create_merge_head_diff! + end + + it_behaves_like 'serializes diffs with expected arguments' do + let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } + let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_ref_head_diff: true) } + end + end + context 'with the different pagination option' do subject { go(page: 5, per_page: 5) } diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb index f4e681b70ff..7ea9cab5453 100644 --- a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb @@ -108,4 +108,30 @@ RSpec.describe 'layouts/nav/sidebar/_group' do expect(rendered).to have_link('Kubernetes', href: group_clusters_path(group)) end end + + describe 'Packages & Registries' do + it 'has a link to the package registry page' do + stub_config(packages: { enabled: true }) + + render + + expect(rendered).to have_link('Package Registry', href: group_packages_path(group)) + end + + it 'has a link to the container registry page' do + stub_container_registry_config(enabled: true) + + render + + expect(rendered).to have_link('Container Registry', href: group_container_registries_path(group)) + end + + it 'has a link to the dependency proxy page' do + stub_config(dependency_proxy: { enabled: true }) + + render + + expect(rendered).to have_link('Dependency Proxy', href: group_dependency_proxy_path(group)) + end + end end |