diff options
Diffstat (limited to 'spec')
22 files changed, 405 insertions, 110 deletions
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index e6553c027d6..7489f506674 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -56,27 +56,9 @@ RSpec.describe Oauth::AuthorizationsController do end end - shared_examples "Implicit grant can't be used in confidential application" do - context 'when application is confidential' do - before do - application.update!(confidential: true) - params[:response_type] = 'token' - end - - it 'does not allow the implicit flow' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template('doorkeeper/authorizations/error') - end - end - end - describe 'GET #new' do subject { get :new, params: params } - include_examples "Implicit grant can't be used in confidential application" - context 'when the user is confirmed' do context 'when there is already an access token for the application with a matching scope' do before do @@ -219,14 +201,12 @@ RSpec.describe Oauth::AuthorizationsController do subject { post :create, params: params } include_examples 'OAuth Authorizations require confirmed user' - include_examples "Implicit grant can't be used in confidential application" end describe 'DELETE #destroy' do subject { delete :destroy, params: params } include_examples 'OAuth Authorizations require confirmed user' - include_examples "Implicit grant can't be used in confidential application" end it 'includes Two-factor enforcement concern' do diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 99e4c680548..fca8972b56c 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -166,16 +166,6 @@ RSpec.describe 'OAuth Login', :allow_forgery_protection do expect(page).to have_current_path(Gitlab::Routing.url_helpers.root_url, ignore_query: true) end - - it 'does not include the fragment for an implicit grant' do - implicit_grant_params = params.merge(response_type: 'token') - escaped_url = Regexp.escape(Gitlab::Routing.url_helpers.root_url) - auth_params_fragment = '#[a-zA-Z0-9&=_]+' - - visit "#{Gitlab::Routing.url_helpers.oauth_authorization_url(implicit_grant_params)}#a_test-hash" - - expect(page).to have_current_path(%r{\A#{escaped_url}#{auth_params_fragment}\z}, ignore_query: true, url: true) - end end context 'when JS is disabled' do diff --git a/spec/frontend/__helpers__/flush_promises.js b/spec/frontend/__helpers__/flush_promises.js deleted file mode 100644 index eefc2ed7c17..00000000000 --- a/spec/frontend/__helpers__/flush_promises.js +++ /dev/null @@ -1,4 +0,0 @@ -export default function flushPromises() { - // eslint-disable-next-line no-restricted-syntax - return new Promise(setImmediate); -} diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 6fa42ddbd2d..25b7483f234 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -24,6 +24,7 @@ import Link from '~/content_editor/extensions/link'; import ListItem from '~/content_editor/extensions/list_item'; import OrderedList from '~/content_editor/extensions/ordered_list'; import Paragraph from '~/content_editor/extensions/paragraph'; +import Sourcemap from '~/content_editor/extensions/sourcemap'; import Strike from '~/content_editor/extensions/strike'; import Table from '~/content_editor/extensions/table'; import TableCell from '~/content_editor/extensions/table_cell'; @@ -32,6 +33,7 @@ import TableRow from '~/content_editor/extensions/table_row'; import TaskItem from '~/content_editor/extensions/task_item'; import TaskList from '~/content_editor/extensions/task_list'; import markdownSerializer from '~/content_editor/services/markdown_serializer'; +import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import { createTestEditor, createDocBuilder } from '../test_utils'; jest.mock('~/emoji'); @@ -63,6 +65,7 @@ const tiptapEditor = createTestEditor({ Link, ListItem, OrderedList, + Sourcemap, Strike, Table, TableCell, @@ -1158,4 +1161,42 @@ Oranges are orange [^1] `.trim(), ); }); + + it.each` + mark | content | modifiedContent + ${'bold'} | ${'**bold**'} | ${'**bold modified**'} + ${'bold'} | ${'__bold__'} | ${'__bold modified__'} + ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} + ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} + ${'italic'} | ${'_italic_'} | ${'_italic modified_'} + ${'italic'} | ${'*italic*'} | ${'*italic modified*'} + ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} + ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} + ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} + ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} + ${'code'} | ${'`code`'} | ${'`code modified`'} + ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} + `( + 'preserves original $mark syntax when sourceMarkdown is available', + async ({ content, modifiedContent }) => { + const { document } = await remarkMarkdownDeserializer().deserialize({ + schema: tiptapEditor.schema, + content, + }); + + tiptapEditor + .chain() + .setContent(document.toJSON()) + // changing the document ensures that block preservation doesn’t yield false positives + .insertContent(' modified') + .run(); + + const serialized = markdownSerializer({}).serialize({ + pristineDoc: document, + doc: tiptapEditor.state.doc, + }); + + expect(serialized).toEqual(modifiedContent); + }, + ); }); diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index c08453530e5..1ae04531a6b 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -37,6 +37,7 @@ const showDetailsModal = jest.fn(); const $toast = { show: jest.fn(), }; + const workItemQueryResponse = { data: { workItem: null, @@ -319,8 +320,10 @@ describe('Description component', () => { }); it('shows toast after delete success', async () => { - findWorkItemDetailModal().vm.$emit('workItemDeleted'); + const newDesc = 'description'; + findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); + expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); expect($toast.show).toHaveBeenCalledWith('Work item deleted'); }); }); @@ -381,7 +384,8 @@ describe('Description component', () => { describe('when url query `work_item_id` exists', () => { it.each` behavior | workItemId | modalOpened - ${'opens'} | ${'123'} | ${1} + ${'opens'} | ${'2'} | ${1} + ${'does not open'} | ${'123'} | ${0} ${'does not open'} | ${'123e'} | ${0} ${'does not open'} | ${'12e3'} | ${0} ${'does not open'} | ${'1e23'} | ${0} diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js index 6496850b028..c987accbb0d 100644 --- a/spec/frontend/pipeline_wizard/components/commit_spec.js +++ b/spec/frontend/pipeline_wizard/components/commit_spec.js @@ -8,7 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import createCommitMutation from '~/pipeline_wizard/queries/create_commit.graphql'; import getFileMetadataQuery from '~/pipeline_wizard/queries/get_file_meta.graphql'; import RefSelector from '~/ref/components/ref_selector.vue'; -import flushPromises from 'helpers/flush_promises'; +import waitForPromises from 'helpers/wait_for_promises'; import { createCommitMutationErrorResult, createCommitMutationResult, @@ -107,7 +107,7 @@ describe('Pipeline Wizard - Commit Page', () => { it('does not show a load error if call is successful', async () => { createComponent({ projectPath, filename }); - await flushPromises(); + await waitForPromises(); expect(wrapper.findByTestId('load-error').exists()).not.toBe(true); }); @@ -117,7 +117,7 @@ describe('Pipeline Wizard - Commit Page', () => { { defaultBranch: branch, projectPath, filename }, createMockApollo([[getFileMetadataQuery, () => fileQueryErrorResult]]), ); - await flushPromises(); + await waitForPromises(); expect(wrapper.findByTestId('load-error').exists()).toBe(true); expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError); }); @@ -131,9 +131,9 @@ describe('Pipeline Wizard - Commit Page', () => { describe('successful commit', () => { beforeEach(async () => { createComponent(); - await flushPromises(); + await waitForPromises(); await getButtonWithLabel(__('Commit')).trigger('click'); - await flushPromises(); + await waitForPromises(); }); it('will not show an error', async () => { @@ -159,9 +159,9 @@ describe('Pipeline Wizard - Commit Page', () => { describe('failed commit', () => { beforeEach(async () => { createComponent({}, getMockApollo({ commitHasError: true })); - await flushPromises(); + await waitForPromises(); await getButtonWithLabel(__('Commit')).trigger('click'); - await flushPromises(); + await waitForPromises(); }); it('will show an error', async () => { @@ -229,7 +229,7 @@ describe('Pipeline Wizard - Commit Page', () => { }), ); - await flushPromises(); + await waitForPromises(); consoleSpy = jest.spyOn(console, 'error'); @@ -243,7 +243,7 @@ describe('Pipeline Wizard - Commit Page', () => { } await Vue.nextTick(); - await flushPromises(); + await waitForPromises(); }); afterAll(() => { diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js index b673e5407d4..b180e8c12dd 100644 --- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js +++ b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js @@ -1,7 +1,7 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; -import flushPromises from 'helpers/flush_promises'; +import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue'; @@ -43,7 +43,7 @@ describe('GitlabVersionCheck', () => { describe(`is ${description}`, () => { beforeEach(async () => { createComponent(mockResponse); - await flushPromises(); // Ensure we wrap up the axios call + await waitForPromises(); // Ensure we wrap up the axios call }); it(`does${renders ? '' : ' not'} render GlBadge`, () => { @@ -61,7 +61,7 @@ describe('GitlabVersionCheck', () => { describe(`when response is ${mockResponse.res.severity}`, () => { beforeEach(async () => { createComponent(mockResponse); - await flushPromises(); // Ensure we wrap up the axios call + await waitForPromises(); // Ensure we wrap up the axios call }); it(`title is ${expectedUI.title}`, () => { diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js index 286c8180e16..137a0a7326d 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -1,29 +1,17 @@ import { GlDropdownItem, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; -import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql'; -import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data'; describe('WorkItemActions component', () => { let wrapper; let glModalDirective; - Vue.use(VueApollo); - const findModal = () => wrapper.findComponent(GlModal); const findDeleteButton = () => wrapper.findComponent(GlDropdownItem); - const createComponent = ({ - canDelete = true, - deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse), - } = {}) => { + const createComponent = ({ canDelete = true } = {}) => { glModalDirective = jest.fn(); wrapper = shallowMount(WorkItemActions, { - apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]), propsData: { workItemId: '123', canDelete }, directives: { glModal: { @@ -54,43 +42,12 @@ describe('WorkItemActions component', () => { expect(glModalDirective).toHaveBeenCalled(); }); - it('calls delete mutation when clicking OK button', () => { - const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse); - - createComponent({ - deleteWorkItemHandler, - }); - - findModal().vm.$emit('ok'); - - expect(deleteWorkItemHandler).toHaveBeenCalled(); - expect(wrapper.emitted('error')).toBeUndefined(); - }); - - it('emits event after delete success', async () => { + it('emits event when clicking OK button', () => { createComponent(); findModal().vm.$emit('ok'); - await waitForPromises(); - - expect(wrapper.emitted('workItemDeleted')).not.toBeUndefined(); - expect(wrapper.emitted('error')).toBeUndefined(); - }); - - it('emits error event after delete failure', async () => { - createComponent({ - deleteWorkItemHandler: jest.fn().mockResolvedValue(deleteWorkItemFailureResponse), - }); - - findModal().vm.$emit('ok'); - - await waitForPromises(); - - expect(wrapper.emitted('error')[0]).toEqual([ - "The resource that you are attempting to access does not exist or you don't have permission to perform this action", - ]); - expect(wrapper.emitted('workItemDeleted')).toBeUndefined(); + expect(wrapper.emitted('deleteWorkItem')).toEqual([[]]); }); it('does not render when canDelete is false', () => { diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js index 67d794519b6..aaabdbc82d9 100644 --- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js @@ -1,21 +1,51 @@ -import { GlModal, GlAlert } from '@gitlab/ui'; +import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; +import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql'; describe('WorkItemDetailModal component', () => { let wrapper; Vue.use(VueApollo); + const hideModal = jest.fn(); + const GlModal = { + template: ` + <div> + <slot></slot> + </div> + `, + methods: { + hide: hideModal, + }, + }; + const findModal = () => wrapper.findComponent(GlModal); const findAlert = () => wrapper.findComponent(GlAlert); const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); const createComponent = ({ workItemId = '1', error = false } = {}) => { + const apolloProvider = createMockApollo([ + [ + deleteWorkItemFromTaskMutation, + jest.fn().mockResolvedValue({ + data: { + workItemDeleteTask: { + workItem: { id: 123, descriptionHtml: 'updated work item desc' }, + errors: [], + }, + }, + }), + ], + ]); + wrapper = shallowMount(WorkItemDetailModal, { + apolloProvider, propsData: { workItemId }, data() { return { @@ -35,7 +65,9 @@ describe('WorkItemDetailModal component', () => { it('renders WorkItemDetail', () => { createComponent(); - expect(findWorkItemDetail().props()).toEqual({ workItemId: '1' }); + expect(findWorkItemDetail().props()).toEqual({ + workItemId: '1', + }); }); it('renders alert if there is an error', () => { @@ -65,10 +97,24 @@ describe('WorkItemDetailModal component', () => { expect(wrapper.emitted('close')).toBeTruthy(); }); - it('emits `workItemDeleted` event on deleting work item', () => { + it('emits `workItemUpdated` event on updating work item', () => { createComponent(); - findWorkItemDetail().vm.$emit('workItemDeleted'); + findWorkItemDetail().vm.$emit('workItemUpdated'); + + expect(wrapper.emitted('workItemUpdated')).toBeTruthy(); + }); + + describe('delete work item', () => { + it('emits workItemDeleted and closes modal', async () => { + createComponent(); + const newDesc = 'updated work item desc'; + + findWorkItemDetail().vm.$emit('deleteWorkItem'); - expect(wrapper.emitted('workItemDeleted')).toBeTruthy(); + await waitForPromises(); + + expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]); + expect(hideModal).toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js index 6584d197206..9e48f56d9e9 100644 --- a/spec/frontend/work_items/components/work_item_state_spec.js +++ b/spec/frontend/work_items/components/work_item_state_spec.js @@ -81,6 +81,15 @@ describe('WorkItemState component', () => { }); }); + it('emits updated event', async () => { + createComponent(); + + findItemState().vm.$emit('changed', STATE_CLOSED); + await waitForPromises(); + + expect(wrapper.emitted('updated')).toEqual([[]]); + }); + it('emits an error message when the mutation was unsuccessful', async () => { createComponent({ mutationHandler: jest.fn().mockRejectedValue('Error!') }); diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js index afde0d9ec45..19b56362ac0 100644 --- a/spec/frontend/work_items/components/work_item_title_spec.js +++ b/spec/frontend/work_items/components/work_item_title_spec.js @@ -57,6 +57,15 @@ describe('WorkItemTitle component', () => { }); }); + it('emits updated event', async () => { + createComponent(); + + findItemTitle().vm.$emit('title-changed', 'new title'); + await waitForPromises(); + + expect(wrapper.emitted('updated')).toEqual([[]]); + }); + it('does not call a mutation when the title has not changed', () => { createComponent(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 0c50a3aa50a..f3483550013 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -5,6 +5,7 @@ export const workItemQueryResponse = { id: 'gid://gitlab/WorkItem/1', title: 'Test', state: 'OPEN', + description: 'description', workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -27,6 +28,7 @@ export const updateWorkItemMutationResponse = { id: 'gid://gitlab/WorkItem/1', title: 'Updated title', state: 'OPEN', + description: 'description', workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', @@ -65,6 +67,7 @@ export const createWorkItemMutationResponse = { id: 'gid://gitlab/WorkItem/1', title: 'Updated title', state: 'OPEN', + description: 'description', workItemType: { __typename: 'WorkItemType', id: 'gid://gitlab/WorkItems::Type/5', diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js index 39fe7aed0ea..9f87655175c 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/pages/work_item_detail_spec.js @@ -104,4 +104,18 @@ describe('WorkItemDetail component', () => { issuableId: workItemQueryResponse.data.workItem.id, }); }); + + it('emits workItemUpdated event when fields updated', async () => { + createComponent(); + + await waitForPromises(); + + findWorkItemState().vm.$emit('updated'); + + expect(wrapper.emitted('workItemUpdated')).toEqual([[]]); + + findWorkItemTitle().vm.$emit('updated'); + + expect(wrapper.emitted('workItemUpdated')).toEqual([[], []]); + }); }); diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js index 81d01a0cb45..85096392e84 100644 --- a/spec/frontend/work_items/pages/work_item_root_spec.js +++ b/spec/frontend/work_items/pages/work_item_root_spec.js @@ -1,21 +1,45 @@ +import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { visitUrl } from '~/lib/utils/url_utility'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; +import deleteWorkItem from '~/work_items/graphql/delete_work_item.mutation.graphql'; +import { deleteWorkItemResponse, deleteWorkItemFailureResponse } from '../mock_data'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); Vue.use(VueApollo); describe('Work items root component', () => { let wrapper; + const issuesListPath = '/-/issues'; + const mockToastShow = jest.fn(); const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail); + const findAlert = () => wrapper.findComponent(GlAlert); - const createComponent = () => { + const createComponent = ({ + deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse), + } = {}) => { wrapper = shallowMount(WorkItemsRoot, { + apolloProvider: createMockApollo([[deleteWorkItem, deleteWorkItemHandler]]), + provide: { + issuesListPath, + }, propsData: { id: '1', }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, }); }; @@ -30,4 +54,34 @@ describe('Work items root component', () => { workItemId: 'gid://gitlab/WorkItem/1', }); }); + + it('deletes work item when deleteWorkItem event emitted', async () => { + const deleteWorkItemHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse); + + createComponent({ + deleteWorkItemHandler, + }); + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + + await waitForPromises(); + + expect(deleteWorkItemHandler).toHaveBeenCalled(); + expect(mockToastShow).toHaveBeenCalled(); + expect(visitUrl).toHaveBeenCalledWith(issuesListPath); + }); + + it('shows alert if delete fails', async () => { + const deleteWorkItemHandler = jest.fn().mockRejectedValue(deleteWorkItemFailureResponse); + + createComponent({ + deleteWorkItemHandler, + }); + + findWorkItemDetail().vm.$emit('deleteWorkItem'); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + }); }); diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 7e68c5e4f0e..99dcd886f7b 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -17,6 +17,7 @@ describe('Work items router', () => { router, provide: { fullPath: 'full-path', + issuesListPath: 'full-path/-/issues', }, mocks: { $apollo: { diff --git a/spec/graphql/types/timelog_type_spec.rb b/spec/graphql/types/timelog_type_spec.rb index 56303e8c1ab..c897a25d10d 100644 --- a/spec/graphql/types/timelog_type_spec.rb +++ b/spec/graphql/types/timelog_type_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['Timelog'] do - let(:fields) { %i[id spent_at time_spent user issue merge_request note summary] } + let_it_be(:fields) { %i[id spent_at time_spent user issue merge_request note summary userPermissions] } it { expect(described_class.graphql_name).to eq('Timelog') } it { expect(described_class).to have_graphql_fields(fields) } it { expect(described_class).to require_graphql_authorizations(:read_issue) } + it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Timelog) } describe 'user field' do subject { described_class.fields['user'] } diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 24c318d0218..a5e24e8e288 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -270,15 +270,6 @@ RSpec.describe Ci::JobArtifact do end end - describe '.order_expired_desc' do - let_it_be(:first_artifact) { create(:ci_job_artifact, expire_at: 2.days.ago) } - let_it_be(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } - - it 'returns ordered artifacts' do - expect(described_class.order_expired_desc).to eq([second_artifact, first_artifact]) - end - end - describe '.order_expired_asc' do let_it_be(:first_artifact) { create(:ci_job_artifact, expire_at: 2.days.ago) } let_it_be(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } diff --git a/spec/tooling/fixtures/find_codeowners/dir0/dir1/dir2/file2 b/spec/tooling/fixtures/find_codeowners/dir0/dir1/dir2/file2 new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/tooling/fixtures/find_codeowners/dir0/dir1/dir2/file2 diff --git a/spec/tooling/fixtures/find_codeowners/dir0/dir1/file1 b/spec/tooling/fixtures/find_codeowners/dir0/dir1/file1 new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/tooling/fixtures/find_codeowners/dir0/dir1/file1 diff --git a/spec/tooling/fixtures/find_codeowners/dir0/file0 b/spec/tooling/fixtures/find_codeowners/dir0/file0 new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/tooling/fixtures/find_codeowners/dir0/file0 diff --git a/spec/tooling/fixtures/find_codeowners/file b/spec/tooling/fixtures/find_codeowners/file new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/spec/tooling/fixtures/find_codeowners/file diff --git a/spec/tooling/lib/tooling/find_codeowners_spec.rb b/spec/tooling/lib/tooling/find_codeowners_spec.rb new file mode 100644 index 00000000000..b29c5f35ec9 --- /dev/null +++ b/spec/tooling/lib/tooling/find_codeowners_spec.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require_relative '../../../../tooling/lib/tooling/find_codeowners' + +RSpec.describe Tooling::FindCodeowners do + let(:subject) { described_class.new } + let(:root) { File.expand_path('../../fixtures/find_codeowners', __dir__) } + + describe '#execute' do + before do + allow(subject).to receive(:load_config).and_return( + '[Section name]': { + '@group': { + allow: { + keywords: %w[dir0 file], + patterns: ['/%{keyword}/**/*', '/%{keyword}'] + }, + deny: { + keywords: %w[file0], + patterns: ['**/%{keyword}'] + } + } + } + ) + end + + it 'prints CODEOWNERS as configured' do + expect do + Dir.chdir(root) do + subject.execute + end + end.to output(<<~CODEOWNERS).to_stdout + [Section name] + /dir0/dir1 @group + /file @group + CODEOWNERS + end + end + + describe '#load_definitions' do + it 'expands the allow and deny list with keywords and patterns' do + subject.load_definitions.each do |section, group_defintions| + group_defintions.each do |group, definitions| + expect(definitions[:allow]).to be_an(Array) + expect(definitions[:deny]).to be_an(Array) + end + end + end + + it 'expands the auth group' do + auth = subject.load_definitions.dig( + :'[Authentication and Authorization]', + :'@gitlab-org/manage/authentication-and-authorization') + + expect(auth).to eq( + allow: %w[ + /{,ee/}app/**/*password*{/**/*,} + /{,ee/}config/**/*password*{/**/*,} + /{,ee/}lib/**/*password*{/**/*,} + /{,ee/}app/**/*auth*{/**/*,} + /{,ee/}config/**/*auth*{/**/*,} + /{,ee/}lib/**/*auth*{/**/*,} + /{,ee/}app/**/*token*{/**/*,} + /{,ee/}config/**/*token*{/**/*,} + /{,ee/}lib/**/*token*{/**/*,} + ], + deny: %w[ + **/*author.*{/**/*,} + **/*author_*{/**/*,} + **/*authored*{/**/*,} + **/*authoring*{/**/*,} + **/*.png*{/**/*,} + **/*.svg*{/**/*,} + **/*deploy_token*{/**/*,} + **/*runner{,s}_token*{/**/*,} + **/*job_token*{/**/*,} + **/*autocomplete_tokens*{/**/*,} + **/*dast_site_token*{/**/*,} + **/*reset_prometheus_token*{/**/*,} + **/*reset_registration_token*{/**/*,} + **/*runners_registration_token*{/**/*,} + **/*terraform_registry_token*{/**/*,} + **/*tokenizer*{/**/*,} + **/*filtered_search*{/**/*,} + **/*/alert_management/*{/**/*,} + **/*/analytics/*{/**/*,} + **/*/bitbucket/*{/**/*,} + **/*/clusters/*{/**/*,} + **/*/clusters_list/*{/**/*,} + **/*/dast/*{/**/*,} + **/*/dast_profiles/*{/**/*,} + **/*/dast_site_tokens/*{/**/*,} + **/*/dast_site_validation/*{/**/*,} + **/*/dependency_proxy/*{/**/*,} + **/*/error_tracking/*{/**/*,} + **/*/google_api/*{/**/*,} + **/*/google_cloud/*{/**/*,} + **/*/jira_connect/*{/**/*,} + **/*/kubernetes/*{/**/*,} + **/*/protected_environments/*{/**/*,} + **/*/config/feature_flags/development/jira_connect_*{/**/*,} + **/*/config/metrics/*{/**/*,} + **/*/app/controllers/groups/dependency_proxy_auth_controller.rb*{/**/*,} + **/*/app/finders/ci/auth_job_finder.rb*{/**/*,} + **/*/ee/config/metrics/*{/**/*,} + **/*/lib/gitlab/conan_token.rb*{/**/*,} + ] + ) + end + end + + describe '#load_config' do + it 'loads the config with symbolized keys' do + config = subject.load_config + + expect_hash_keys_to_be_symbols(config) + end + + context 'when YAML has safe_load_file' do + before do + allow(YAML).to receive(:respond_to?).with(:safe_load_file).and_return(true) + end + + it 'calls safe_load_file' do + expect(YAML).to receive(:safe_load_file) + + subject.load_config + end + end + + context 'when YAML does not have safe_load_file' do + before do + allow(YAML).to receive(:respond_to?).with(:safe_load_file).and_return(false) + end + + it 'calls load_file' do + expect(YAML).to receive(:safe_load) + + subject.load_config + end + end + + def expect_hash_keys_to_be_symbols(object) + if object.is_a?(Hash) + object.each do |key, value| + expect(key).to be_a(Symbol) + + expect_hash_keys_to_be_symbols(value) + end + end + end + end + + describe '#path_matches?' do + let(:pattern) { 'pattern' } + let(:path) { 'path' } + + it 'passes flags we are expecting to File.fnmatch?' do + expected_flags = + ::File::FNM_DOTMATCH | ::File::FNM_PATHNAME | ::File::FNM_EXTGLOB + + expect(File).to receive(:fnmatch?).with(pattern, path, expected_flags) + + subject.path_matches?(pattern, path) + end + end + + describe '#consolidate_paths' do + before do + allow(subject).to receive(:find_dir_maxdepth_1).and_return(<<~LINES) + dir + dir/0 + dir/2 + dir/3 + dir/1 + LINES + end + + context 'when the directory has the same number of entries' do + let(:input_paths) { %W[dir/0\n dir/1\n dir/2\n dir/3\n] } + + it 'consolidates into the directory' do + paths = subject.consolidate_paths(input_paths) + + expect(paths).to eq(["dir\n"]) + end + end + + context 'when the directory has different number of entries' do + let(:input_paths) { %W[dir/0\n dir/1\n dir/2\n] } + + it 'returns the original paths' do + paths = subject.consolidate_paths(input_paths) + + expect(paths).to eq(input_paths) + end + end + end +end |