summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb20
-rw-r--r--spec/features/oauth_login_spec.rb10
-rw-r--r--spec/frontend/__helpers__/flush_promises.js4
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js41
-rw-r--r--spec/frontend/issues/show/components/description_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js49
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js56
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js9
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js9
-rw-r--r--spec/frontend/work_items/mock_data.js3
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js14
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js56
-rw-r--r--spec/frontend/work_items/router_spec.js1
-rw-r--r--spec/graphql/types/timelog_type_spec.rb3
-rw-r--r--spec/models/ci/job_artifact_spec.rb9
-rw-r--r--spec/tooling/fixtures/find_codeowners/dir0/dir1/dir2/file20
-rw-r--r--spec/tooling/fixtures/find_codeowners/dir0/dir1/file10
-rw-r--r--spec/tooling/fixtures/find_codeowners/dir0/file00
-rw-r--r--spec/tooling/fixtures/find_codeowners/file0
-rw-r--r--spec/tooling/lib/tooling/find_codeowners_spec.rb199
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