diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-04 15:17:55 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-04 15:17:55 +0300 |
commit | 49d36ce6e3484aea8131dd892a02f8304ee61aa1 (patch) | |
tree | ee7d8403c0ebacf5b5953ebbf1a838dd957eac6b /spec | |
parent | b68afda3299d372ef0b3745e0603a9d51369650b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
38 files changed, 731 insertions, 343 deletions
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 26d6eef41e3..2397c3a2bdf 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -93,7 +93,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat wait_for_requests end - describe 'The view' do + describe 'the view' do it 'displays the required information description' do page.within('[data-testid="pipeline-schedule-table-row"]') do expect(page).to have_content('pipeline schedule') @@ -280,36 +280,47 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat end end - context 'logged in as non-member' do - before do - gitlab_sign_in(user) - end - + shared_examples 'when not logged in' do describe 'GET /projects/pipeline_schedules' do - before do - visit_pipelines_schedules - end - - describe 'The view' do + describe 'the view' do it 'does not show create schedule button' do + visit_pipelines_schedules + expect(page).not_to have_link('New schedule') end + + context 'when project is public' do + let_it_be(:project) { create(:project, :repository, :public, public_builds: true) } + + it 'shows Pipelines Schedules page' do + visit_pipelines_schedules + + expect(page).to have_link('New schedule') + end + + context 'when public pipelines are disabled' do + before do + project.update!(public_builds: false) + visit_pipelines_schedules + end + + it 'shows Not Found page' do + expect(page).to have_content('Page Not Found') + end + end + end end end end - context 'not logged in' do - describe 'GET /projects/pipeline_schedules' do - before do - visit_pipelines_schedules - end + it_behaves_like 'when not logged in' - describe 'The view' do - it 'does not show create schedule button' do - expect(page).not_to have_link('New schedule') - end - end + context 'logged in as non-member' do + before do + gitlab_sign_in(user) end + + it_behaves_like 'when not logged in' end def visit_new_pipeline_schedule diff --git a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js index b4d22c0d29a..5abf2a5ce53 100644 --- a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js +++ b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js @@ -31,7 +31,7 @@ describe('Job Log Collapsible Section', () => { }); it('renders clickable header line', () => { - expect(findLogLineHeader().text()).toBe('2 foo'); + expect(findLogLineHeader().text()).toBe('1 foo'); expect(findLogLineHeader().attributes('role')).toBe('button'); }); diff --git a/spec/frontend/ci/job_details/components/log/line_header_spec.js b/spec/frontend/ci/job_details/components/log/line_header_spec.js index 7d1b05346f2..45296e4b6c2 100644 --- a/spec/frontend/ci/job_details/components/log/line_header_spec.js +++ b/spec/frontend/ci/job_details/components/log/line_header_spec.js @@ -16,7 +16,7 @@ describe('Job Log Header Line', () => { style: 'term-fg-l-green', }, ], - lineNumber: 76, + lineNumber: 77, }, isClosed: true, path: '/jashkenas/underscore/-/jobs/335', diff --git a/spec/frontend/ci/job_details/components/log/line_number_spec.js b/spec/frontend/ci/job_details/components/log/line_number_spec.js index d5c1d0fd985..db964e341b7 100644 --- a/spec/frontend/ci/job_details/components/log/line_number_spec.js +++ b/spec/frontend/ci/job_details/components/log/line_number_spec.js @@ -5,7 +5,7 @@ describe('Job Log Line Number', () => { let wrapper; const data = { - lineNumber: 0, + lineNumber: 1, path: '/jashkenas/underscore/-/jobs/335', }; diff --git a/spec/frontend/ci/job_details/components/log/line_spec.js b/spec/frontend/ci/job_details/components/log/line_spec.js index b6f3a2b68df..dad41d0cd7f 100644 --- a/spec/frontend/ci/job_details/components/log/line_spec.js +++ b/spec/frontend/ci/job_details/components/log/line_spec.js @@ -224,7 +224,7 @@ describe('Job Log Line', () => { offset: 24526, content: [{ text: 'job log content' }], section: 'custom-section', - lineNumber: 76, + lineNumber: 77, }, path: '/root/ci-project/-/jobs/6353', }); diff --git a/spec/frontend/ci/job_details/components/log/log_spec.js b/spec/frontend/ci/job_details/components/log/log_spec.js index d076b9f6099..1931d5046dc 100644 --- a/spec/frontend/ci/job_details/components/log/log_spec.js +++ b/spec/frontend/ci/job_details/components/log/log_spec.js @@ -151,7 +151,7 @@ describe('Job Log', () => { ], section: 'prepare-executor', section_header: true, - lineNumber: 2, + lineNumber: 3, }, ]; diff --git a/spec/frontend/ci/job_details/components/log/mock_data.js b/spec/frontend/ci/job_details/components/log/mock_data.js index 3368f3527fe..14669872cc1 100644 --- a/spec/frontend/ci/job_details/components/log/mock_data.js +++ b/spec/frontend/ci/job_details/components/log/mock_data.js @@ -172,7 +172,7 @@ export const collapsibleSectionClosed = { offset: 80, content: [{ text: 'this is a collapsible nested section' }], section: 'prepare-script', - lineNumber: 3, + lineNumber: 2, }, ], }; @@ -193,7 +193,7 @@ export const collapsibleSectionOpened = { offset: 80, content: [{ text: 'this is a collapsible nested section' }], section: 'prepare-script', - lineNumber: 3, + lineNumber: 2, }, ], }; diff --git a/spec/frontend/ci/job_details/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js index 0ecb97cd935..78b29efed68 100644 --- a/spec/frontend/ci/job_details/store/mutations_spec.js +++ b/spec/frontend/ci/job_details/store/mutations_spec.js @@ -106,7 +106,7 @@ describe('Jobs Store Mutations', () => { { offset: 1, content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], - lineNumber: 0, + lineNumber: 1, }, ]); }); @@ -127,7 +127,7 @@ describe('Jobs Store Mutations', () => { { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }], - lineNumber: 0, + lineNumber: 1, }, ]); }); diff --git a/spec/frontend/ci/job_details/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js index f9f360ef754..394ce0ab737 100644 --- a/spec/frontend/ci/job_details/store/utils_spec.js +++ b/spec/frontend/ci/job_details/store/utils_spec.js @@ -6,7 +6,7 @@ import { addDurationToHeader, isCollapsibleSection, findOffsetAndRemove, - getIncrementalLineNumber, + getNextLineNumber, } from '~/ci/job_details/store/utils'; import { mockJobLog, @@ -192,14 +192,14 @@ describe('Jobs Store Utils', () => { describe('regular line', () => { it('adds a lineNumber property with correct index', () => { - expect(result[0].lineNumber).toEqual(0); - expect(result[1].lineNumber).toEqual(1); - expect(result[2].line.lineNumber).toEqual(2); - expect(result[2].lines[0].lineNumber).toEqual(3); - expect(result[2].lines[1].lineNumber).toEqual(4); - expect(result[3].line.lineNumber).toEqual(5); - expect(result[3].lines[0].lineNumber).toEqual(6); - expect(result[3].lines[1].lineNumber).toEqual(7); + expect(result[0].lineNumber).toEqual(1); + expect(result[1].lineNumber).toEqual(2); + expect(result[2].line.lineNumber).toEqual(3); + expect(result[2].lines[0].lineNumber).toEqual(4); + expect(result[2].lines[1].lineNumber).toEqual(5); + expect(result[3].line.lineNumber).toEqual(6); + expect(result[3].lines[0].lineNumber).toEqual(7); + expect(result[3].lines[1].lineNumber).toEqual(8); }); }); @@ -326,17 +326,24 @@ describe('Jobs Store Utils', () => { }); }); - describe('getIncrementalLineNumber', () => { - describe('when last line is 0', () => { + describe('getNextLineNumber', () => { + describe('when there is no previous log', () => { + it('returns 1', () => { + expect(getNextLineNumber([])).toEqual(1); + expect(getNextLineNumber(undefined)).toEqual(1); + }); + }); + + describe('when last line is 1', () => { it('returns 1', () => { const log = [ { content: [], - lineNumber: 0, + lineNumber: 1, }, ]; - expect(getIncrementalLineNumber(log)).toEqual(1); + expect(getNextLineNumber(log)).toEqual(2); }); }); @@ -353,7 +360,7 @@ describe('Jobs Store Utils', () => { }, ]; - expect(getIncrementalLineNumber(log)).toEqual(102); + expect(getNextLineNumber(log)).toEqual(102); }); }); @@ -374,7 +381,7 @@ describe('Jobs Store Utils', () => { }, ]; - expect(getIncrementalLineNumber(log)).toEqual(102); + expect(getNextLineNumber(log)).toEqual(102); }); }); @@ -401,7 +408,7 @@ describe('Jobs Store Utils', () => { }, ]; - expect(getIncrementalLineNumber(log)).toEqual(104); + expect(getNextLineNumber(log)).toEqual(104); }); }); }); @@ -420,7 +427,7 @@ describe('Jobs Store Utils', () => { text: 'Downloading', }, ], - lineNumber: 0, + lineNumber: 1, }, { offset: 2, @@ -429,7 +436,7 @@ describe('Jobs Store Utils', () => { text: 'log line', }, ], - lineNumber: 1, + lineNumber: 2, }, ]); }); @@ -448,7 +455,7 @@ describe('Jobs Store Utils', () => { text: 'log line', }, ], - lineNumber: 0, + lineNumber: 1, }, ]); }); @@ -472,7 +479,7 @@ describe('Jobs Store Utils', () => { }, ], section: 'section', - lineNumber: 0, + lineNumber: 1, }, lines: [], }, @@ -498,7 +505,7 @@ describe('Jobs Store Utils', () => { }, ], section: 'section', - lineNumber: 0, + lineNumber: 1, }, lines: [ { @@ -509,7 +516,7 @@ describe('Jobs Store Utils', () => { }, ], section: 'section', - lineNumber: 1, + lineNumber: 2, }, ], }, diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap index 17ebdf8725d..af7eca6a52d 100644 --- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap +++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap @@ -9,7 +9,7 @@ exports[`Repository table row component renders a symlink table row 1`] = ` > <a class="str-truncated tree-item-link" - data-qa-selector="file_name_link" + data-testid="file-name-link" href="https://test.com" title="test" > @@ -65,7 +65,7 @@ exports[`Repository table row component renders table row 1`] = ` > <a class="str-truncated tree-item-link" - data-qa-selector="file_name_link" + data-testid="file-name-link" href="https://test.com" title="test" > @@ -121,7 +121,7 @@ exports[`Repository table row component renders table row for path with special > <a class="str-truncated tree-item-link" - data-qa-selector="file_name_link" + data-testid="file-name-link" href="https://test.com" title="test" > diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js index 826fc2b2230..e08e954d79e 100644 --- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js @@ -10,9 +10,11 @@ import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comme import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue'; import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { createWorkItemNoteResponse, + groupWorkItemByIidResponseFactory, workItemByIidResponseFactory, workItemQueryResponse, } from '../../mock_data'; @@ -29,6 +31,7 @@ describe('Work item add note', () => { const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemNoteResponse); let workItemResponseHandler; + let groupWorkItemResponseHandler; const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm); const findTextarea = () => wrapper.findByTestId('note-reply-textarea'); @@ -40,27 +43,30 @@ describe('Work item add note', () => { canCreateNote = true, workItemIid = '1', workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }), + groupWorkItemResponse = groupWorkItemByIidResponseFactory({ canUpdate, canCreateNote }), signedIn = true, isEditing = true, + isGroup = false, workItemType = 'Task', isInternalThread = false, } = {}) => { workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); + groupWorkItemResponseHandler = jest.fn().mockResolvedValue(groupWorkItemResponse); if (signedIn) { window.gon.current_user_id = '1'; window.gon.current_user_avatar_url = 'avatar.png'; } - const apolloProvider = createMockApollo([ - [workItemByIidQuery, workItemResponseHandler], - [createNoteMutation, mutationHandler], - ]); - const { id } = workItemQueryResponse.data.workItem; wrapper = shallowMountExtended(WorkItemAddNote, { - apolloProvider, + apolloProvider: createMockApollo([ + [workItemByIidQuery, workItemResponseHandler], + [groupWorkItemByIidQuery, groupWorkItemResponseHandler], + [createNoteMutation, mutationHandler], + ]), provide: { fullPath: 'test-project-path', + isGroup, }, propsData: { workItemId: id, @@ -272,16 +278,44 @@ describe('Work item add note', () => { }); }); - it('calls the work item query', async () => { - await createComponent(); + describe('when project context', () => { + it('calls the project work item query', async () => { + await createComponent(); + + expect(workItemResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', async () => { + await createComponent(); - expect(workItemResponseHandler).toHaveBeenCalled(); + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('skips calling the project work item query when missing workItemIid', async () => { + await createComponent({ workItemIid: '', isEditing: false }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); }); - it('skips calling the work item query when missing workItemIid', async () => { - await createComponent({ workItemIid: '', isEditing: false }); + describe('when group context', () => { + it('skips calling the project work item query', async () => { + await createComponent({ isGroup: true }); - expect(workItemResponseHandler).not.toHaveBeenCalled(); + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', async () => { + await createComponent({ isGroup: true }); + + expect(groupWorkItemResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query when missing workItemIid', async () => { + await createComponent({ isGroup: true, workItemIid: '', isEditing: false }); + + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); }); it('wrapper adds `internal-note` class when internal thread', async () => { diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js index 9049a69656a..4bc58f19bd6 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -15,8 +15,10 @@ import NoteActions from '~/work_items/components/notes/work_item_note_actions.vu import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue'; import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { + groupWorkItemByIidResponseFactory, mockAssignees, mockWorkItemCommentNote, updateWorkItemMutationResponse, @@ -68,6 +70,9 @@ describe('Work Item Note', () => { }); const workItemResponseHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory()); + const groupWorkItemResponseHandler = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory()); const workItemByAuthoredByDifferentUser = jest .fn() .mockResolvedValue(mockWorkItemByDifferentUser); @@ -90,6 +95,7 @@ describe('Work Item Note', () => { const createComponent = ({ note = mockWorkItemCommentNote, isFirstNote = false, + isGroup = false, updateNoteMutationHandler = successHandler, workItemId = mockWorkItemId, updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler, @@ -99,6 +105,7 @@ describe('Work Item Note', () => { wrapper = shallowMount(WorkItemNote, { provide: { fullPath: 'test-project-path', + isGroup, }, propsData: { workItemId, @@ -112,6 +119,7 @@ describe('Work Item Note', () => { }, apolloProvider: mockApollo([ [workItemByIidQuery, workItemByIidResponseHandler], + [groupWorkItemByIidQuery, groupWorkItemResponseHandler], [updateWorkItemNoteMutation, updateNoteMutationHandler], [updateWorkItemMutation, updateWorkItemMutationHandler], ]), @@ -442,4 +450,32 @@ describe('Work Item Note', () => { expect(findAwardsList().props('workItemIid')).toBe('1'); }); }); + + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); + + expect(workItemResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupWorkItemResponseHandler).toHaveBeenCalled(); + }); + }); }); 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 0098a2e0864..ab8e95e5252 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -132,6 +132,7 @@ describe('WorkItemActions component', () => { }, provide: { fullPath: mockFullPath, + isGroup: false, glFeatures: { workItemsMvc2: true }, }, mocks: { diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js index f77c5481906..e4da5a68a82 100644 --- a/spec/frontend/work_items/components/work_item_created_updated_spec.js +++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js @@ -7,12 +7,18 @@ import waitForPromises from 'helpers/wait_for_promises'; import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; -import { workItemByIidResponseFactory, mockAssignees } from '../mock_data'; +import { + groupWorkItemByIidResponseFactory, + mockAssignees, + workItemByIidResponseFactory, +} from '../mock_data'; describe('WorkItemCreatedUpdated component', () => { let wrapper; let successHandler; + let groupSuccessHandler; Vue.use(VueApollo); @@ -30,19 +36,26 @@ describe('WorkItemCreatedUpdated component', () => { updatedAt, confidential = false, updateInProgress = false, + isGroup = false, } = {}) => { - const workItemQueryResponse = workItemByIidResponseFactory({ + const workItemQueryResponse = workItemByIidResponseFactory({ author, updatedAt, confidential }); + const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({ author, updatedAt, confidential, }); successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse); wrapper = shallowMount(WorkItemCreatedUpdated, { - apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]), + apolloProvider: createMockApollo([ + [workItemByIidQuery, successHandler], + [groupWorkItemByIidQuery, groupSuccessHandler], + ]), provide: { fullPath: '/some/project', + isGroup, }, propsData: { workItemIid, updateInProgress }, stubs: { @@ -54,10 +67,44 @@ describe('WorkItemCreatedUpdated component', () => { await waitForPromises(); }; - it('skips the work item query when workItemIid is not defined', async () => { - await createComponent({ workItemIid: null }); + describe('when project context', () => { + it('calls the project work item query', async () => { + await createComponent(); - expect(successHandler).not.toHaveBeenCalled(); + expect(successHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', async () => { + await createComponent(); + + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); + + it('skips calling the project work item query when workItemIid is not defined', async () => { + await createComponent({ workItemIid: null }); + + expect(successHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', async () => { + await createComponent({ isGroup: true }); + + expect(successHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', async () => { + await createComponent({ isGroup: true }); + + expect(groupSuccessHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query when workItemIid is not defined', async () => { + await createComponent({ isGroup: true, workItemIid: null }); + + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); }); it('shows work item type metadata with type and icon', async () => { diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 8b9963b2476..98df0dc1627 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -13,9 +13,11 @@ import WorkItemDescription from '~/work_items/components/work_item_description.v import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils'; import { + groupWorkItemByIidResponseFactory, updateWorkItemMutationResponse, workItemByIidResponseFactory, workItemQueryResponse, @@ -33,6 +35,7 @@ describe('WorkItemDescription', () => { const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); let workItemResponseHandler; + let groupWorkItemResponseHandler; const findForm = () => wrapper.findComponent(GlForm); const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor); @@ -51,14 +54,19 @@ describe('WorkItemDescription', () => { canUpdate = true, workItemResponse = workItemByIidResponseFactory({ canUpdate }), isEditing = false, + isGroup = false, workItemIid = '1', } = {}) => { workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); + groupWorkItemResponseHandler = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory({ canUpdate })); const { id } = workItemQueryResponse.data.workItem; wrapper = shallowMount(WorkItemDescription, { apolloProvider: createMockApollo([ [workItemByIidQuery, workItemResponseHandler], + [groupWorkItemByIidQuery, groupWorkItemResponseHandler], [updateWorkItemMutation, mutationHandler], ]), propsData: { @@ -67,6 +75,7 @@ describe('WorkItemDescription', () => { }, provide: { fullPath: 'test-project-path', + isGroup, }, }); @@ -247,9 +256,31 @@ describe('WorkItemDescription', () => { }); }); - it('calls the work item query', async () => { - await createComponent(); + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); - expect(workItemResponseHandler).toHaveBeenCalled(); + expect(workItemResponseHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupWorkItemResponseHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(workItemResponseHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupWorkItemResponseHandler).toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index fec6d0673c6..28826748cb0 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -28,12 +28,14 @@ import WorkItemStateToggleButton from '~/work_items/components/work_item_state_t import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; import { i18n } from '~/work_items/constants'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql'; import { + groupWorkItemByIidResponseFactory, mockParent, workItemByIidResponseFactory, objectiveType, @@ -49,6 +51,10 @@ describe('WorkItemDetail component', () => { Vue.use(VueApollo); const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true }); + const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({ + canUpdate: true, + canDelete: true, + }); const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({ canUpdate: false, canDelete: false, @@ -59,6 +65,7 @@ describe('WorkItemDetail component', () => { canDelete: true, }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); + const groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse); const showModalHandler = jest.fn(); const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0]; const workItemUpdatedSubscriptionHandler = jest @@ -92,6 +99,7 @@ describe('WorkItemDetail component', () => { const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon); const createComponent = ({ + isGroup = false, isModal = false, updateInProgress = false, workItemIid = '1', @@ -101,14 +109,13 @@ describe('WorkItemDetail component', () => { workItemsMvc2Enabled = false, linkedWorkItemsEnabled = false, } = {}) => { - const handlers = [ - [workItemByIidQuery, handler], - [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler], - confidentialityMock, - ]; - wrapper = shallowMountExtended(WorkItemDetail, { - apolloProvider: createMockApollo(handlers), + apolloProvider: createMockApollo([ + [workItemByIidQuery, handler], + [groupWorkItemByIidQuery, groupSuccessHandler], + [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler], + confidentialityMock, + ]), isLoggedIn: isLoggedIn(), propsData: { isModal, @@ -131,6 +138,7 @@ describe('WorkItemDetail component', () => { hasIssuableHealthStatusFeature: true, projectNamespace: 'namespace', fullPath: 'group/project', + isGroup, reportAbusePath: '/report/abuse/path', }, stubs: { @@ -484,25 +492,64 @@ describe('WorkItemDetail component', () => { expect(findAlert().text()).toBe(updateError); }); - it('calls the work item query', async () => { - createComponent(); - await waitForPromises(); + describe('when project context', () => { + it('calls the project work item query', async () => { + createComponent(); + await waitForPromises(); - expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); - }); + expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); - it('skips the work item query when there is no workItemIid', async () => { - createComponent({ workItemIid: null }); - await waitForPromises(); + it('skips calling the group work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); - expect(successHandler).not.toHaveBeenCalled(); + it('skips calling the project work item query when there is no workItemIid', async () => { + createComponent({ workItemIid: null }); + await waitForPromises(); + + expect(successHandler).not.toHaveBeenCalled(); + }); + + it('calls the project work item query when isModal=true', async () => { + createComponent({ isModal: true }); + await waitForPromises(); + + expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); }); - it('calls the work item query when isModal=true', async () => { - createComponent({ isModal: true }); - await waitForPromises(); + describe('when group context', () => { + it('skips calling the project work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(successHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); + + it('skips calling the group work item query when there is no workItemIid', async () => { + createComponent({ isGroup: true, workItemIid: null }); + await waitForPromises(); - expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + expect(groupSuccessHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query when isModal=true', async () => { + createComponent({ isGroup: true, isModal: true }); + await waitForPromises(); + + expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); + }); }); describe('hierarchy widget', () => { diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js index 4a20e654060..1e173776c68 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -7,10 +7,12 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants'; import { + groupWorkItemByIidResponseFactory, projectLabelsResponse, mockLabels, workItemByIidResponseFactory, @@ -32,6 +34,9 @@ describe('WorkItemLabels component', () => { const workItemQuerySuccess = jest .fn() .mockResolvedValue(workItemByIidResponseFactory({ labels: null })); + const groupWorkItemQuerySuccess = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory({ labels: null })); const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse); const successUpdateWorkItemMutationHandler = jest .fn() @@ -40,6 +45,7 @@ describe('WorkItemLabels component', () => { const createComponent = ({ canUpdate = true, + isGroup = false, workItemQueryHandler = workItemQuerySuccess, searchQueryHandler = successSearchQueryHandler, updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler, @@ -48,11 +54,13 @@ describe('WorkItemLabels component', () => { wrapper = mountExtended(WorkItemLabels, { apolloProvider: createMockApollo([ [workItemByIidQuery, workItemQueryHandler], + [groupWorkItemByIidQuery, groupWorkItemQuerySuccess], [labelSearchQuery, searchQueryHandler], [updateWorkItemMutation, updateWorkItemMutationHandler], ]), provide: { fullPath: 'test-project-path', + isGroup, }, propsData: { workItemId, @@ -244,17 +252,49 @@ describe('WorkItemLabels component', () => { }); }); - it('calls the work item query', async () => { - createComponent(); - await waitForPromises(); + describe('when project context', () => { + it('calls the project work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(workItemQuerySuccess).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', async () => { + createComponent(); + await waitForPromises(); + + expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled(); + }); + + it('skips calling the project work item query when missing workItemIid', async () => { + createComponent({ workItemIid: '' }); + await waitForPromises(); - expect(workItemQuerySuccess).toHaveBeenCalled(); + expect(workItemQuerySuccess).not.toHaveBeenCalled(); + }); }); - it('skips calling the work item query when missing workItemIid', async () => { - createComponent({ workItemIid: '' }); - await waitForPromises(); + describe('when group context', () => { + it('skips calling the project work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); - expect(workItemQuerySuccess).not.toHaveBeenCalled(); + expect(workItemQuerySuccess).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', async () => { + createComponent({ isGroup: true }); + await waitForPromises(); + + expect(groupWorkItemQuerySuccess).toHaveBeenCalled(); + }); + + it('skips calling the group work item query when missing workItemIid', async () => { + createComponent({ isGroup: true, workItemIid: '' }); + await waitForPromises(); + + expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled(); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js index cd077fbf705..be44ff7b7d9 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js @@ -54,6 +54,7 @@ describe('WorkItemChildrenWrapper', () => { apolloProvider: mockApollo, provide: { fullPath: 'test/project', + isGroup: false, }, propsData: { workItemType, diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index aaab22fd18d..fd068d3dd40 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -64,6 +64,7 @@ describe('WorkItemLinksForm', () => { provide: { fullPath: 'project/path', hasIterationsFeature, + isGroup: false, }, }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index e24cfe27616..111fb5d8458 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -13,9 +13,11 @@ import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/wor import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; import { FORM_TYPES } from '~/work_items/constants'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { getIssueDetailsResponse, + groupWorkItemByIidResponseFactory, workItemHierarchyResponse, workItemHierarchyEmptyResponse, workItemHierarchyNoUpdatePermissionResponse, @@ -32,6 +34,9 @@ describe('WorkItemLinks', () => { let mockApollo; const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyResponse); + const groupResponseWithAddChildPermission = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory()); const responseWithoutAddChildPermission = jest .fn() .mockResolvedValue(workItemByIidResponseFactory({ adminParentLink: false })); @@ -40,10 +45,12 @@ describe('WorkItemLinks', () => { fetchHandler = responseWithAddChildPermission, issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()), hasIterationsFeature = false, + isGroup = false, } = {}) => { mockApollo = createMockApollo( [ [workItemByIidQuery, fetchHandler], + [groupWorkItemByIidQuery, groupResponseWithAddChildPermission], [issueDetailsQuery, issueDetailsQueryHandler], ], resolvers, @@ -54,6 +61,7 @@ describe('WorkItemLinks', () => { provide: { fullPath: 'project/path', hasIterationsFeature, + isGroup, reportAbusePath: '/report/abuse/path', }, propsData: { @@ -243,4 +251,32 @@ describe('WorkItemLinks', () => { expect(findAbuseCategorySelector().exists()).toBe(false); }); }); + + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); + + expect(responseWithAddChildPermission).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupResponseWithAddChildPermission).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(responseWithAddChildPermission).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupResponseWithAddChildPermission).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js index 04b6e3d8df6..83f4cf5081e 100644 --- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js +++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js @@ -10,9 +10,11 @@ import WidgetWrapper from '~/work_items/components/widget_wrapper.vue'; import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue'; import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue'; import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue'; +import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { + groupWorkItemByIidResponseFactory, workItemByIidResponseFactory, mockLinkedItems, mockBlockingLinkedItem, @@ -25,21 +27,29 @@ describe('WorkItemRelationships', () => { const emptyLinkedWorkItemsQueryHandler = jest .fn() .mockResolvedValue(workItemByIidResponseFactory()); + const groupWorkItemsQueryHandler = jest + .fn() + .mockResolvedValue(groupWorkItemByIidResponseFactory()); const createComponent = async ({ workItemQueryHandler = emptyLinkedWorkItemsQueryHandler, workItemType = 'Task', + isGroup = false, } = {}) => { - const mockApollo = createMockApollo([[workItemByIidQuery, workItemQueryHandler]]); - wrapper = shallowMountExtended(WorkItemRelationships, { - apolloProvider: mockApollo, + apolloProvider: createMockApollo([ + [workItemByIidQuery, workItemQueryHandler], + [groupWorkItemByIidQuery, groupWorkItemsQueryHandler], + ]), propsData: { workItemId: 'gid://gitlab/WorkItem/1', workItemIid: '1', workItemFullPath: 'test-project-path', workItemType, }, + provide: { + isGroup, + }, }); await waitForPromises(); @@ -120,4 +130,32 @@ describe('WorkItemRelationships', () => { await findWorkItemRelationshipForm().vm.$emit('cancel'); expect(findWorkItemRelationshipForm().exists()).toBe(false); }); + + describe('when project context', () => { + it('calls the project work item query', () => { + createComponent(); + + expect(emptyLinkedWorkItemsQueryHandler).toHaveBeenCalled(); + }); + + it('skips calling the group work item query', () => { + createComponent(); + + expect(groupWorkItemsQueryHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when group context', () => { + it('skips calling the project work item query', () => { + createComponent({ isGroup: true }); + + expect(emptyLinkedWorkItemsQueryHandler).not.toHaveBeenCalled(); + }); + + it('calls the group work item query', () => { + createComponent({ isGroup: true }); + + expect(groupWorkItemsQueryHandler).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js index 454bd97bbee..c76cdbcee53 100644 --- a/spec/frontend/work_items/components/work_item_todos_spec.js +++ b/spec/frontend/work_items/components/work_item_todos_spec.js @@ -86,6 +86,9 @@ describe('WorkItemTodo component', () => { workItemFullpath: mockWorkItemFullpath, currentUserTodos, }, + provide: { + isGroup: false, + }, }); }; diff --git a/spec/frontend/work_items/graphql/cache_utils_spec.js b/spec/frontend/work_items/graphql/cache_utils_spec.js index 6d0083790d1..64ef1bdbb88 100644 --- a/spec/frontend/work_items/graphql/cache_utils_spec.js +++ b/spec/frontend/work_items/graphql/cache_utils_spec.js @@ -43,7 +43,7 @@ describe('work items graphql cache utils', () => { title: 'New child', }; - addHierarchyChild(mockCache, fullPath, iid, child); + addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child }); expect(mockCache.writeQuery).toHaveBeenCalledWith({ query: workItemByIidQuery, @@ -88,7 +88,7 @@ describe('work items graphql cache utils', () => { title: 'New child', }; - addHierarchyChild(mockCache, fullPath, iid, child); + addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child }); expect(mockCache.writeQuery).not.toHaveBeenCalled(); }); @@ -106,7 +106,7 @@ describe('work items graphql cache utils', () => { title: 'Child', }; - removeHierarchyChild(mockCache, fullPath, iid, childToRemove); + removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove }); expect(mockCache.writeQuery).toHaveBeenCalledWith({ query: workItemByIidQuery, @@ -145,7 +145,7 @@ describe('work items graphql cache utils', () => { title: 'Child', }; - removeHierarchyChild(mockCache, fullPath, iid, childToRemove); + removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove }); expect(mockCache.writeQuery).not.toHaveBeenCalled(); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 0250834fa85..9cd76c0651e 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -833,6 +833,21 @@ export const workItemByIidResponseFactory = (options) => { }; }; +export const groupWorkItemByIidResponseFactory = (options) => { + const response = workItemResponseFactory(options); + return { + data: { + workspace: { + __typename: 'Group', + id: 'gid://gitlab/Group/1', + workItems: { + nodes: [response.data.workItem], + }, + }, + }, + }; +}; + export const updateWorkItemMutationResponseFactory = (options) => { const response = workItemResponseFactory(options); return { diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index 87dbb18dcc4..527f5890338 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -65,6 +65,7 @@ describe('Create work item component', () => { }, provide: { fullPath: 'full-path', + isGroup: false, }, }); }; diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 79ba31e7012..d4efcf78189 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -41,6 +41,7 @@ describe('Work items router', () => { router, provide: { fullPath: 'full-path', + isGroup: false, issuesListPath: 'full-path/-/issues', hasIssueWeightsFeature: false, hasIterationsFeature: false, diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb index 497cd5d1e7f..6eaa603a43d 100644 --- a/spec/helpers/wiki_helper_spec.rb +++ b/spec/helpers/wiki_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe WikiHelper do +RSpec.describe WikiHelper, feature_category: :wiki do describe '#wiki_page_title' do let_it_be(:page) { create(:wiki_page) } @@ -75,38 +75,42 @@ RSpec.describe WikiHelper do describe '#wiki_sort_controls' do let(:wiki) { create(:project_wiki) } - let(:wiki_link) { helper.wiki_sort_controls(wiki, direction) } - let(:classes) { "gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort" } - def expected_link(direction, icon_class) + before do + allow(Pajamas::ButtonComponent).to receive(:new).and_call_original + end + + def expected_link_args(direction, icon_class) path = "/#{wiki.project.full_path}/-/wikis/pages?direction=#{direction}" title = direction == 'desc' ? _('Sort direction: Ascending') : _('Sort direction: Descending') - helper.link_to(path, type: 'button', class: classes, title: title) do - helper.sprite_icon("sort-#{icon_class}") - end + { + href: path, + icon: "sort-#{icon_class}", + button_options: hash_including(title: title) + } end - context 'initial call' do - let(:direction) { nil } + context 'when initially rendering' do + it 'uses default values' do + helper.wiki_sort_controls(wiki, nil) - it 'renders with default values' do - expect(wiki_link).to eq(expected_link('desc', 'lowest')) + expect(Pajamas::ButtonComponent).to have_received(:new).with(expected_link_args('desc', 'lowest')) end end - context 'sort by asc order' do - let(:direction) { 'asc' } - + context 'when the current sort order is ascending' do it 'renders a link with opposite direction' do - expect(wiki_link).to eq(expected_link('desc', 'lowest')) + helper.wiki_sort_controls(wiki, 'asc') + + expect(Pajamas::ButtonComponent).to have_received(:new).with(expected_link_args('desc', 'lowest')) end end - context 'sort by desc order' do - let(:direction) { 'desc' } - + context 'when the current sort order is descending' do it 'renders a link with opposite direction' do - expect(wiki_link).to eq(expected_link('asc', 'highest')) + helper.wiki_sort_controls(wiki, 'desc') + + expect(Pajamas::ButtonComponent).to have_received(:new).with(expected_link_args('asc', 'highest')) end end end diff --git a/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb deleted file mode 100644 index ba74c173794..00000000000 --- a/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BulkImports::Common::Transformers::UserReferenceTransformer do - describe '#transform' do - let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group) } - let_it_be(:bulk_import) { create(:bulk_import) } - let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } - let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } - let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } - - let(:hash) do - { - 'user' => { - 'public_email' => email - } - } - end - - before do - group.add_developer(user) - end - - shared_examples 'sets user_id and removes user key' do - it 'sets found user_id and removes user key' do - transformed_hash = subject.transform(context, hash) - - expect(transformed_hash['user']).to be_nil - expect(transformed_hash['user_id']).to eq(user.id) - end - end - - context 'when user can be found by email' do - let(:email) { user.email } - - include_examples 'sets user_id and removes user key' - end - - context 'when user cannot be found by email' do - let(:user) { bulk_import.user } - let(:email) { nil } - - include_examples 'sets user_id and removes user key' - end - - context 'when there is no data to transform' do - it 'returns' do - expect(subject.transform(nil, nil)).to be_nil - end - end - - context 'when custom reference is provided' do - shared_examples 'updates provided reference' do |reference| - let(:hash) do - { - 'author' => { - 'public_email' => user.email - } - } - end - - it 'updates provided reference' do - transformer = described_class.new(reference: reference) - result = transformer.transform(context, hash) - - expect(result['author']).to be_nil - expect(result['author_id']).to eq(user.id) - end - end - - include_examples 'updates provided reference', 'author' - include_examples 'updates provided reference', :author - end - end -end diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb deleted file mode 100644 index d0947eef2d9..00000000000 --- a/spec/lib/gitlab/prometheus/query_variables_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Prometheus::QueryVariables do - describe '.call' do - let_it_be_with_refind(:environment) { create(:environment) } - - let(:project) { environment.project } - let(:slug) { environment.slug } - let(:params) { {} } - - subject { described_class.call(environment, **params) } - - it { is_expected.to include(ci_environment_slug: slug) } - it { is_expected.to include(ci_project_name: project.name) } - it { is_expected.to include(ci_project_namespace: project.namespace.name) } - it { is_expected.to include(ci_project_path: project.full_path) } - it { is_expected.to include(ci_environment_name: environment.name) } - - it do - is_expected.to include(environment_filter: - %[container_name!="POD",environment="#{slug}"]) - end - - context 'without deployment platform' do - it { is_expected.to include(kube_namespace: '') } - end - - context 'with deployment platform' do - context 'with project cluster' do - let(:kube_namespace) { environment.deployment_namespace } - - before do - create(:cluster, :project, :provided_by_user, projects: [project]) - end - - it { is_expected.to include(kube_namespace: kube_namespace) } - end - - context 'with group cluster' do - let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) } - let(:group) { create(:group) } - let(:project2) { create(:project) } - let(:kube_namespace) { k8s_ns.namespace } - - let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project, environment: environment) } - let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2, environment: environment) } - - before do - group.projects << project - group.projects << project2 - end - - it { is_expected.to include(kube_namespace: kube_namespace) } - end - end - - context '__range' do - context 'when start_time and end_time are present' do - let(:params) do - { - start_time: Time.rfc3339('2020-05-29T07:23:05.008Z'), - end_time: Time.rfc3339('2020-05-29T15:23:05.008Z') - } - end - - it { is_expected.to include(__range: "#{8.hours.to_i}s") } - end - - context 'when start_time and end_time are not present' do - it { is_expected.to include(__range: nil) } - end - - context 'when end_time is not present' do - let(:params) do - { - start_time: Time.rfc3339('2020-05-29T07:23:05.008Z') - } - end - - it { is_expected.to include(__range: nil) } - end - - context 'when start_time is not present' do - let(:params) do - { - end_time: Time.rfc3339('2020-05-29T07:23:05.008Z') - } - end - - it { is_expected.to include(__range: nil) } - end - end - end -end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 49d6c0f9f26..7f40e762307 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -125,16 +125,6 @@ RSpec.describe Gitlab::Shell do let(:storage) { Gitlab.config.repositories.storages.each_key.first } - describe '#add_namespace' do - it 'creates a namespace' do - Gitlab::GitalyClient::NamespaceService.allow do - subject.add_namespace(storage, "mepmep") - - expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(true) - end - end - end - describe '#repository_exists?' do context 'when the repository does not exist' do it 'returns false' do @@ -150,17 +140,5 @@ RSpec.describe Gitlab::Shell do end end end - - describe '#mv_namespace' do - it 'renames the namespace' do - Gitlab::GitalyClient::NamespaceService.allow do - subject.add_namespace(storage, "mepmep") - subject.mv_namespace(storage, "mepmep", "2mep") - - expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("mepmep")).to be(false) - expect(Gitlab::GitalyClient::NamespaceService.new(storage).exists?("2mep")).to be(true) - end - end - end end end diff --git a/spec/models/integrations/telegram_spec.rb b/spec/models/integrations/telegram_spec.rb index c3a66c84f09..4c814dedd66 100644 --- a/spec/models/integrations/telegram_spec.rb +++ b/spec/models/integrations/telegram_spec.rb @@ -50,4 +50,12 @@ RSpec.describe Integrations::Telegram, feature_category: :integrations do end end end + + describe '#avatar_url' do + it 'returns the avatar image path' do + expect(subject.avatar_url).to eq( + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/telegram.svg') + ) + end + end end diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb index d760e4ddf28..fb67d7cb4fb 100644 --- a/spec/requests/api/ci/pipeline_schedules_spec.rb +++ b/spec/requests/api/ci/pipeline_schedules_spec.rb @@ -176,7 +176,7 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra end context 'with public project' do - let_it_be(:project) { create(:project, :repository, :public, public_builds: false) } + let_it_be(:project) { create(:project, :repository, :public, public_builds: true) } it_behaves_like 'request with schedule ownership' it_behaves_like 'request with project permissions' @@ -204,6 +204,30 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra expect(json_response).not_to have_key('variables') end end + + context 'when public pipelines are disabled' do + let_it_be(:project) { create(:project, :repository, :public, public_builds: false) } + + context 'authenticated user with no project permissions' do + it 'does not return pipeline_schedule' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'authenticated user with insufficient project permissions' do + before do + project.add_guest(user) + end + + it 'returns pipeline_schedule with no variables' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end end @@ -294,7 +318,7 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra end context 'with public project' do - let_it_be(:project) { create(:project, :repository, :public, public_builds: false) } + let_it_be(:project) { create(:project, :repository, :public, public_builds: true) } it_behaves_like 'request with schedule ownership' it_behaves_like 'request with project permissions' @@ -308,6 +332,18 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra expect(response).to return_pipeline_schedule_pipelines_successfully end end + + context 'when public pipelines are disabled' do + let_it_be(:project) { create(:project, :repository, :public, public_builds: false) } + + context 'authenticated user with no project permissions' do + it 'does not return the details of pipelines triggered from the pipeline schedule' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end end diff --git a/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb b/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb index 76adce6ff1b..8219cdcd673 100644 --- a/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb +++ b/spec/requests/api/graphql/ci/pipeline_schedules_spec.rb @@ -115,6 +115,31 @@ RSpec.describe 'Query.project.pipelineSchedules', feature_category: :continuous_ expect(edit_path).to be nil end + + it 'returns the pipeline schedules data' do + expect(pipeline_schedule_graphql_data['id']).to eq(pipeline_schedule.to_global_id.to_s) + end + + context 'when public pipelines are disabled' do + before do + project.update!(public_builds: false) + post_graphql(query, current_user: another_user) + end + + it 'does not return any data' do + expect(pipeline_schedule_graphql_data).to be_nil + end + + context 'when the user is authorized' do + before_all do + project.add_developer(another_user) + end + + it 'returns the pipeline schedules data' do + expect(pipeline_schedule_graphql_data['id']).to eq(pipeline_schedule.to_global_id.to_s) + end + end + end end it 'avoids N+1 queries' do diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb index 9fce5f8497f..497ae1cc13f 100644 --- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb @@ -61,6 +61,17 @@ RSpec.describe 'Setting issues crm contacts', feature_category: :service_desk do expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes)) .to match_array(expected_contacts(mutation_contacts)) end + + context 'with an empty list of contacts' do + let(:mutation_contacts) { [] } + + it 'removes all contacts' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes)) + .to be_empty + end + end end context 'append' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5973649a9d7..d2af9286a92 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -4619,6 +4619,160 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile end end + describe 'POST /user/personal_access_tokens' do + using RSpec::Parameterized::TableSyntax + + let(:name) { 'new pat' } + let(:scopes) { %w[k8s_proxy] } + let(:path) { "/user/personal_access_tokens" } + let(:params) { { name: name, scopes: scopes } } + + let(:all_scopes) do + ::Gitlab::Auth::API_SCOPES + ::Gitlab::Auth::AI_FEATURES_SCOPES + ::Gitlab::Auth::OPENID_SCOPES + + ::Gitlab::Auth::PROFILE_SCOPES + ::Gitlab::Auth::REPOSITORY_SCOPES + ::Gitlab::Auth::REGISTRY_SCOPES + + ::Gitlab::Auth::OBSERVABILITY_SCOPES + ::Gitlab::Auth::ADMIN_SCOPES + end + + context 'when user_pat_rest_api feature flag is disabled' do + before do + stub_feature_flags(user_pat_rest_api: false) + end + + it 'does not create a personal access token' do + post api(path, user), params: params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('400 Bad request - Endpoint is disabled via user_pat_rest_api feature flag. Please contact your administrator to enable it.') + end + end + + context 'when user_pat_rest_api feature flag is enabled' do + before do + stub_feature_flags(user_pat_rest_api: true) + end + + it 'returns error if required attributes are missing' do + post api(path, user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('name is missing, scopes is missing') + end + + where(:disallowed_scopes) do + all_scopes - [::Gitlab::Auth::K8S_PROXY_SCOPE] + end + + with_them do + it 'returns error if scopes has disallowed value' do + post api(path, user), params: params.merge({ scopes: [disallowed_scopes] }) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('scopes does not have a valid value') + end + end + + it 'returns error if one of the scopes is not allowed' do + post api(path, user), params: params.merge({ scopes: [::Gitlab::Auth::K8S_PROXY_SCOPE, ::Gitlab::Auth::API_SCOPE] }) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('scopes does not have a valid value') + end + + it 'returns a 401 error when not authenticated' do + post api(path), params: params + + expect(response).to have_gitlab_http_status(:unauthorized) + expect(json_response['message']).to eq('401 Unauthorized') + end + + it 'returns a 403 error when called with a read_api-scoped PAT' do + read_only_pat = create(:personal_access_token, scopes: ['read_api'], user: user) + post api(path, personal_access_token: read_only_pat), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'when scopes are empty' do + let(:scopes) { [] } + + it 'returns an error when no scopes are given' do + post api(path, user), params: params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq("Scopes can't be blank") + end + end + + it 'creates a personal access token' do + post api(path, user), params: params + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['name']).to eq(name) + expect(json_response['scopes']).to eq(scopes) + expect(json_response['expires_at']).to eq(1.day.from_now.to_date.to_s) + expect(json_response['id']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['active']).to be_truthy + expect(json_response['revoked']).to be_falsey + expect(json_response['token']).to be_present + end + + context 'when expires_at at is given' do + let(:params) { { name: name, scopes: scopes, expires_at: expires_at } } + + context 'when expires_at is in the past' do + let(:expires_at) { 1.day.ago } + + it 'creates an inactive personal access token' do + post api(path, user), params: params + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['active']).to be_falsey + end + end + + context 'when expires_at is in the future' do + let(:expires_at) { 1.month.from_now.to_date } + + it 'creates a personal access token' do + post api(path, user), params: params + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['name']).to eq(name) + expect(json_response['scopes']).to eq(scopes) + expect(json_response['expires_at']).to eq(1.month.from_now.to_date.to_s) + expect(json_response['id']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['active']).to be_truthy + expect(json_response['revoked']).to be_falsey + expect(json_response['token']).to be_present + end + end + end + end + + context 'when an error is thrown by the model' do + let!(:admin_personal_access_token) { create(:personal_access_token, :admin_mode, user: admin) } + let(:error_message) { 'error message' } + + before do + allow_next_instance_of(PersonalAccessToken) do |personal_access_token| + allow(personal_access_token).to receive_message_chain(:errors, :full_messages) + .and_return([error_message]) + + allow(personal_access_token).to receive(:save).and_return(false) + end + end + + it 'returns the error' do + post api(path, personal_access_token: admin_personal_access_token), params: params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq(error_message) + end + end + end + describe 'GET /users/:user_id/impersonation_tokens' do let_it_be(:active_personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) } @@ -4675,7 +4829,7 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile describe 'POST /users/:user_id/impersonation_tokens' do let(:name) { 'my new pat' } let(:expires_at) { '2016-12-28' } - let(:scopes) { %w(api read_user) } + let(:scopes) { %w[api read_user] } let(:impersonation) { true } let(:path) { "/users/#{user.id}/impersonation_tokens" } let(:params) { { name: name, expires_at: expires_at, scopes: scopes, impersonation: impersonation } } diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index b07296a0df2..bc1ba3357a4 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -355,19 +355,6 @@ RSpec.describe 'Git LFS API and storage', feature_category: :source_code_managem expect(json_response['objects'].first['actions']['download']['href']).to start_with("https://lfs-objects.s3.amazonaws.com/") expect(json_response['objects'].first['actions']['download']['href']).to include("X-Amz-Expires=3600&") end - - context 'when feature flag "lfs_batch_direct_downloads" is "false"' do - before do - stub_feature_flags(lfs_batch_direct_downloads: false) - end - - it_behaves_like 'LFS http 200 response' - - it 'does return proxied address URL' do - expect(json_response['objects'].first).to include(sample_object) - expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid)) - end - end end end diff --git a/spec/rubocop/check_graceful_task_spec.rb b/spec/rubocop/check_graceful_task_spec.rb index 38c2d68a593..1ab977266dc 100644 --- a/spec/rubocop/check_graceful_task_spec.rb +++ b/spec/rubocop/check_graceful_task_spec.rb @@ -119,9 +119,9 @@ RSpec.describe RuboCop::CheckGracefulTask do end context 'with args' do - let(:args) { %w[a.rb Lint/EmptyFile b.rb Lint/Syntax] } + let(:args) { %w[Lint/EmptyFile Lint/Syntax] } - it_behaves_like 'rubocop scan', rubocop_args: %w[--only Lint/EmptyFile,Lint/Syntax a.rb b.rb] + it_behaves_like 'rubocop scan', rubocop_args: %w[--only Lint/EmptyFile,Lint/Syntax] it 'does not notify slack' do expect(Gitlab::Popen).not_to receive(:popen) diff --git a/spec/services/issues/set_crm_contacts_service_spec.rb b/spec/services/issues/set_crm_contacts_service_spec.rb index aa5dec20a13..7d709bbd9c8 100644 --- a/spec/services/issues/set_crm_contacts_service_spec.rb +++ b/spec/services/issues/set_crm_contacts_service_spec.rb @@ -106,6 +106,14 @@ RSpec.describe Issues::SetCrmContactsService, feature_category: :team_planning d it_behaves_like 'setting contacts' it_behaves_like 'adds system note', 1, 1 + + context 'with empty list' do + let(:params) { { replace_ids: [] } } + let(:expected_contacts) { [] } + + it_behaves_like 'setting contacts' + it_behaves_like 'adds system note', 0, 2 + end end context 'add' do |