diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-22 21:07:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-22 21:07:44 +0300 |
commit | d0f16d56f3716d4a60027eb261f12080094f8db3 (patch) | |
tree | f9ecf9f4bda6d761f612bc4a5efb701b7c6b3d2d /spec | |
parent | 68aa32736b50c3609348f3bf740b81a2dfd1fb25 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
16 files changed, 510 insertions, 207 deletions
diff --git a/spec/frontend/approvals/mock_data.js b/spec/frontend/approvals/mock_data.js new file mode 100644 index 00000000000..e0e90c09791 --- /dev/null +++ b/spec/frontend/approvals/mock_data.js @@ -0,0 +1,10 @@ +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; + +export const createCanApproveResponse = () => { + const response = JSON.parse(JSON.stringify(approvedByCurrentUser)); + response.data.project.mergeRequest.userPermissions.canApprove = true; + response.data.project.mergeRequest.approved = false; + response.data.project.mergeRequest.approvedBy.nodes = []; + + return response; +}; diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb index 7ee89ca3694..b6f6d149756 100644 --- a/spec/frontend/fixtures/merge_requests.rb +++ b/spec/frontend/fixtures/merge_requests.rb @@ -151,7 +151,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: context 'merge request with no approvals' do base_input_path = 'vue_merge_request_widget/components/approvals/queries/' base_output_path = 'graphql/merge_requests/approvals/' - query_name = 'approved_by.query.graphql' + query_name = 'approvals.query.graphql' it "#{base_output_path}#{query_name}_no_approvals.json" do query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?) @@ -165,7 +165,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: context 'merge request approved by current user' do base_input_path = 'vue_merge_request_widget/components/approvals/queries/' base_output_path = 'graphql/merge_requests/approvals/' - query_name = 'approved_by.query.graphql' + query_name = 'approvals.query.graphql' it "#{base_output_path}#{query_name}.json" do merge_request.approved_by_users << user @@ -181,7 +181,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: context 'merge request approved by multiple users' do base_input_path = 'vue_merge_request_widget/components/approvals/queries/' base_output_path = 'graphql/merge_requests/approvals/' - query_name = 'approved_by.query.graphql' + query_name = 'approvals.query.graphql' it "#{base_output_path}#{query_name}_multiple_users.json" do merge_request.approved_by_users << user diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js index 82017754b23..e2a3fdeeb25 100644 --- a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js +++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js @@ -17,8 +17,12 @@ describe('CheckboxFilter', () => { setQuery: jest.fn(), }; + const getterSpies = { + queryLangugageFilters: jest.fn(() => []), + }; + const defaultProps = { - filterData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS), + filtersData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS), }; const createComponent = () => { @@ -27,6 +31,7 @@ describe('CheckboxFilter', () => { query: MOCK_QUERY, }, actions: actionSpies, + getters: getterSpies, }); wrapper = shallowMountExtended(CheckboxFilter, { @@ -73,7 +78,7 @@ describe('CheckboxFilter', () => { describe('actions', () => { it('triggers setQuery', () => { const filter = - defaultProps.filterData.filters[Object.keys(defaultProps.filterData.filters)[0]].value; + defaultProps.filtersData.filters[Object.keys(defaultProps.filtersData.filters)[0]].value; findFormCheckboxGroup().vm.$emit('input', filter); expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), { diff --git a/spec/frontend/search/sidebar/components/language_filters_spec.js b/spec/frontend/search/sidebar/components/language_filter_spec.js index e297d1c33b0..6870e759110 100644 --- a/spec/frontend/search/sidebar/components/language_filters_spec.js +++ b/spec/frontend/search/sidebar/components/language_filter_spec.js @@ -23,6 +23,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => { const getterSpies = { langugageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS), + queryLangugageFilters: jest.fn(() => []), }; const createComponent = (initialState) => { @@ -48,6 +49,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => { const findForm = () => wrapper.findComponent(GlForm); const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter); const findApplyButton = () => wrapper.findByTestId('apply-button'); + const findResetButton = () => wrapper.findByTestId('reset-button'); const findShowMoreButton = () => wrapper.findByTestId('show-more-button'); const findAlert = () => wrapper.findComponent(GlAlert); const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox); @@ -84,6 +86,25 @@ describe('GlobalSearchSidebarLanguageFilter', () => { }); }); + describe('resetButton', () => { + describe.each` + description | sidebarDirty | queryFilters | isDisabled + ${'sidebar dirty only'} | ${true} | ${[]} | ${undefined} + ${'query filters only'} | ${false} | ${['JSON', 'C']} | ${undefined} + ${'sidebar dirty and query filters'} | ${true} | ${['JSON', 'C']} | ${undefined} + ${'no sidebar and no query filters'} | ${false} | ${[]} | ${'true'} + `('$description', ({ sidebarDirty, queryFilters, isDisabled }) => { + beforeEach(() => { + getterSpies.queryLangugageFilters = jest.fn(() => queryFilters); + createComponent({ sidebarDirty, query: { ...MOCK_QUERY, language: queryFilters } }); + }); + + it(`button is ${isDisabled ? 'enabled' : 'disabled'}`, () => { + expect(findResetButton().attributes('disabled')).toBe(isDisabled); + }); + }); + }); + describe('ApplyButton', () => { describe('when sidebarDirty is false', () => { beforeEach(() => { diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js index 2f87802dfe6..0f270ff2491 100644 --- a/spec/frontend/search/store/actions_spec.js +++ b/spec/frontend/search/store/actions_spec.js @@ -325,4 +325,26 @@ describe('Global Search Store Actions', () => { }); }); }); + + describe('resetLanguageQueryWithRedirect', () => { + it('calls visitUrl and setParams with the state.query', () => { + return testAction(actions.resetLanguageQueryWithRedirect, null, state, [], [], () => { + expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null }); + expect(urlUtils.visitUrl).toHaveBeenCalled(); + }); + }); + }); + + describe('resetLanguageQuery', () => { + it('calls commit SET_QUERY with value []', () => { + state = { ...state, query: { ...state.query, language: ['YAML', 'Text', 'Markdown'] } }; + return testAction( + actions.resetLanguageQuery, + null, + state, + [{ type: types.SET_QUERY, payload: { key: 'language', value: [] } }], + [], + ); + }); + }); }); diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js index 818902ee720..b70a6be8d46 100644 --- a/spec/frontend/search/store/getters_spec.js +++ b/spec/frontend/search/store/getters_spec.js @@ -1,12 +1,14 @@ import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants'; import * as getters from '~/search/store/getters'; import createState from '~/search/store/state'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS, MOCK_AGGREGATIONS, MOCK_LANGUAGE_AGGREGATIONS_BUCKETS, + TEST_FILTER_DATA, } from '../mock_data'; describe('Global Search Store Getters', () => { @@ -14,37 +16,48 @@ describe('Global Search Store Getters', () => { beforeEach(() => { state = createState({ query: MOCK_QUERY }); + useMockLocationHelper(); }); describe('frequentGroups', () => { - beforeEach(() => { - state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS; - }); - it('returns the correct data', () => { + state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS; expect(getters.frequentGroups(state)).toStrictEqual(MOCK_GROUPS); }); }); describe('frequentProjects', () => { - beforeEach(() => { - state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS; - }); - it('returns the correct data', () => { + state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS; expect(getters.frequentProjects(state)).toStrictEqual(MOCK_PROJECTS); }); }); describe('langugageAggregationBuckets', () => { - beforeEach(() => { - state.aggregations.data = MOCK_AGGREGATIONS; - }); - it('returns the correct data', () => { + state.aggregations.data = MOCK_AGGREGATIONS; expect(getters.langugageAggregationBuckets(state)).toStrictEqual( MOCK_LANGUAGE_AGGREGATIONS_BUCKETS, ); }); }); + + describe('queryLangugageFilters', () => { + it('returns the correct data', () => { + state.query.language = Object.keys(TEST_FILTER_DATA.filters); + expect(getters.queryLangugageFilters(state)).toStrictEqual(state.query.language); + }); + }); + + describe('currentUrlQueryHasLanguageFilters', () => { + it.each` + description | lang | result + ${'has valid language'} | ${{ language: ['a', 'b'] }} | ${true} + ${'has empty lang'} | ${{ language: [] }} | ${false} + ${'has no lang'} | ${{}} | ${false} + `('$description', ({ lang, result }) => { + state.urlQuery = lang; + expect(getters.currentUrlQueryHasLanguageFilters(state)).toBe(result); + }); + }); }); diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js index 487ed7bfe03..8c4a17f0a5d 100644 --- a/spec/frontend/search/store/utils_spec.js +++ b/spec/frontend/search/store/utils_spec.js @@ -226,11 +226,14 @@ describe('Global Search Store Utils', () => { }); describe.each` - description | currentQuery | urlQuery | isDirty - ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${false} - ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${true} - ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${false} - ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${true} + description | currentQuery | urlQuery | isDirty + ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false} + ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'c'] }} | ${true} + ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null, [SIDEBAR_PARAMS[2]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: undefined }} | ${false} + ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: [] }} | ${true} + ${'language only no url params'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: undefined }} | ${true} + ${'language only url params symetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false} + ${'language only url params asymetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${true} `('isSidebarDirty', ({ description, currentQuery, urlQuery, isDirty }) => { describe(`with ${description} sidebar query data`, () => { let res; diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js index bf208f16d18..80a7565cbee 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js @@ -1,17 +1,24 @@ -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { GlButton, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { createAlert } from '~/flash'; import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue'; import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue'; import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue'; import { - FETCH_LOADING, - FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR, } from '~/vue_merge_request_widget/components/approvals/messages'; import eventHub from '~/vue_merge_request_widget/event_hub'; +import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql'; +import { createCanApproveResponse } from 'jest/approvals/mock_data'; + +Vue.use(VueApollo); const mockAlertDismiss = jest.fn(); jest.mock('~/flash', () => ({ @@ -20,7 +27,6 @@ jest.mock('~/flash', () => ({ })), })); -const RULE_NAME = 'first_rule'; const TEST_HELP_PATH = 'help/path'; const testApprovedBy = () => [1, 7, 10].map((id) => ({ id })); const testApprovals = () => ({ @@ -34,15 +40,18 @@ const testApprovals = () => ({ require_password_to_approve: false, invalid_approvers_rules: [], }); -const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] }); describe('MRWidget approvals', () => { let wrapper; let service; let mr; - const createComponent = (props = {}) => { + const createComponent = (props = {}, response = approvedByCurrentUser) => { + const requestHandlers = [[approvedByQuery, jest.fn().mockResolvedValue(response)]]; + const apolloProvider = createMockApollo(requestHandlers); + wrapper = shallowMount(Approvals, { + apolloProvider, propsData: { mr, service, @@ -68,15 +77,10 @@ describe('MRWidget approvals', () => { }; const findSummary = () => wrapper.findComponent(ApprovalsSummary); const findOptionalSummary = () => wrapper.findComponent(ApprovalsSummaryOptional); - const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]'); beforeEach(() => { service = { ...{ - fetchApprovals: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), - fetchApprovalSettings: jest - .fn() - .mockReturnValue(Promise.resolve(testApprovalRulesResponse())), approveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), unapproveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), approveMergeRequestWithAuth: jest.fn().mockReturnValue(Promise.resolve(testApprovals())), @@ -97,55 +101,21 @@ describe('MRWidget approvals', () => { }; jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('when created', () => { - it('shows loading message', async () => { - service = { - fetchApprovals: jest.fn().mockReturnValue(new Promise(() => {})), - }; - - createComponent(); - await nextTick(); - expect(wrapper.text()).toContain(FETCH_LOADING); - }); - - it('fetches approvals', () => { - createComponent(); - expect(service.fetchApprovals).toHaveBeenCalled(); - }); - }); - - describe('when fetch approvals error', () => { - beforeEach(() => { - jest.spyOn(service, 'fetchApprovals').mockReturnValue(Promise.reject()); - createComponent(); - return nextTick(); - }); - - it('still shows loading message', () => { - expect(wrapper.text()).toContain(FETCH_LOADING); - }); - - it('flashes error', () => { - expect(createAlert).toHaveBeenCalledWith({ message: FETCH_ERROR }); - }); + gon.current_user_id = getIdFromGraphQLId( + approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes[0].id, + ); }); describe('action button', () => { describe('when mr is closed', () => { - beforeEach(() => { + beforeEach(async () => { + const response = createCanApproveResponse(); + mr.isOpen = false; - mr.approvals.user_has_approved = false; - mr.approvals.user_can_approve = true; - createComponent(); - return nextTick(); + createComponent({}, response); + await waitForPromises(); }); it('action is not rendered', () => { @@ -154,12 +124,12 @@ describe('MRWidget approvals', () => { }); describe('when user cannot approve', () => { - beforeEach(() => { - mr.approvals.user_has_approved = false; - mr.approvals.user_can_approve = false; + beforeEach(async () => { + const response = JSON.parse(JSON.stringify(approvedByCurrentUser)); + response.data.project.mergeRequest.approvedBy.nodes = []; - createComponent(); - return nextTick(); + createComponent({}, response); + await waitForPromises(); }); it('action is not rendered', () => { @@ -168,15 +138,16 @@ describe('MRWidget approvals', () => { }); describe('when user can approve', () => { + let canApproveResponse; + beforeEach(() => { - mr.approvals.user_has_approved = false; - mr.approvals.user_can_approve = true; + canApproveResponse = createCanApproveResponse(); }); describe('and MR is unapproved', () => { - beforeEach(() => { - createComponent(); - return nextTick(); + beforeEach(async () => { + createComponent({}, canApproveResponse); + await waitForPromises(); }); it('approve action is rendered', () => { @@ -190,30 +161,33 @@ describe('MRWidget approvals', () => { describe('and MR is approved', () => { beforeEach(() => { - mr.approvals.approved = true; + canApproveResponse.data.project.mergeRequest.approved = true; }); describe('with no approvers', () => { - beforeEach(() => { - mr.approvals.approved_by = []; - createComponent(); - return nextTick(); + beforeEach(async () => { + canApproveResponse.data.project.mergeRequest.approvedBy.nodes = []; + createComponent({}, canApproveResponse); + await nextTick(); }); - it('approve action (with inverted style) is rendered', () => { - expect(findActionData()).toEqual({ + it('approve action is rendered', () => { + expect(findActionData()).toMatchObject({ variant: 'confirm', text: 'Approve', - category: 'secondary', }); }); }); describe('with approvers', () => { - beforeEach(() => { - mr.approvals.approved_by = [{ user: { id: 7 } }]; - createComponent(); - return nextTick(); + beforeEach(async () => { + canApproveResponse.data.project.mergeRequest.approvedBy.nodes = + approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes; + + canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 2; + + createComponent({}, canApproveResponse); + await waitForPromises(); }); it('approve additionally action is rendered', () => { @@ -227,9 +201,9 @@ describe('MRWidget approvals', () => { }); describe('when approve action is clicked', () => { - beforeEach(() => { - createComponent(); - return nextTick(); + beforeEach(async () => { + createComponent({}, canApproveResponse); + await waitForPromises(); }); it('shows loading icon', () => { @@ -258,10 +232,6 @@ describe('MRWidget approvals', () => { it('emits to eventHub', () => { expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); }); - - it('calls store setApprovals', () => { - expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals()); - }); }); describe('and error', () => { @@ -286,12 +256,12 @@ describe('MRWidget approvals', () => { }); describe('when user has approved', () => { - beforeEach(() => { - mr.approvals.user_has_approved = true; - mr.approvals.user_can_approve = false; + beforeEach(async () => { + const response = JSON.parse(JSON.stringify(approvedByCurrentUser)); - createComponent(); - return nextTick(); + createComponent({}, response); + + await waitForPromises(); }); it('revoke action is rendered', () => { @@ -316,10 +286,6 @@ describe('MRWidget approvals', () => { it('emits to eventHub', () => { expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); }); - - it('calls store setApprovals', () => { - expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals()); - }); }); describe('and error', () => { @@ -338,19 +304,24 @@ describe('MRWidget approvals', () => { }); describe('approvals optional summary', () => { + let optionalApprovalsResponse; + + beforeEach(() => { + optionalApprovalsResponse = JSON.parse(JSON.stringify(approvedByCurrentUser)); + }); + describe('when no approvals required and no approvers', () => { beforeEach(() => { - mr.approvals.approved_by = []; - mr.approvals.approvals_required = 0; - mr.approvals.user_has_approved = false; + optionalApprovalsResponse.data.project.mergeRequest.approvedBy.nodes = []; + optionalApprovalsResponse.data.project.mergeRequest.approvalsRequired = 0; }); describe('and can approve', () => { - beforeEach(() => { - mr.approvals.user_can_approve = true; + beforeEach(async () => { + optionalApprovalsResponse.data.project.mergeRequest.userPermissions.canApprove = true; - createComponent(); - return nextTick(); + createComponent({}, optionalApprovalsResponse); + await waitForPromises(); }); it('is shown', () => { @@ -363,11 +334,9 @@ describe('MRWidget approvals', () => { }); describe('and cannot approve', () => { - beforeEach(() => { - mr.approvals.user_can_approve = false; - - createComponent(); - return nextTick(); + beforeEach(async () => { + createComponent({}, optionalApprovalsResponse); + await nextTick(); }); it('is shown', () => { @@ -382,9 +351,9 @@ describe('MRWidget approvals', () => { }); describe('approvals summary', () => { - beforeEach(() => { + beforeEach(async () => { createComponent(); - return nextTick(); + await nextTick(); }); it('is rendered with props', () => { @@ -393,41 +362,7 @@ describe('MRWidget approvals', () => { expect(findOptionalSummary().exists()).toBe(false); expect(summary.exists()).toBe(true); expect(summary.props()).toMatchObject({ - projectPath: 'gitlab-org/gitlab', - iid: '1', - updatedCount: 0, - }); - }); - }); - - describe('invalid rules', () => { - beforeEach(() => { - mr.approvals.merge_request_approvers_available = true; - createComponent(); - }); - - it('does not render related components', () => { - expect(findInvalidRules().exists()).toBe(false); - }); - - describe('when invalid rules are present', () => { - beforeEach(() => { - mr.approvals.invalid_approvers_rules = [{ name: RULE_NAME }]; - createComponent(); - }); - - it('renders related components', () => { - const invalidRules = findInvalidRules(); - - expect(invalidRules.exists()).toBe(true); - - const invalidRulesText = invalidRules.text(); - - expect(invalidRulesText).toContain(RULE_NAME); - expect(invalidRulesText).toContain( - 'GitLab has approved this rule automatically to unblock the merge request.', - ); - expect(invalidRulesText).toContain('Learn more.'); + approvalState: approvedByCurrentUser.data.project.mergeRequest, }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js index e75ce7c60c9..62eddebd9c1 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js @@ -1,11 +1,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { mount } from '@vue/test-utils'; -import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_multiple_users.json'; -import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_no_approvals.json'; -import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql.json'; +import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_multiple_users.json'; +import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_no_approvals.json'; +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; import waitForPromises from 'helpers/wait_for_promises'; -import createMockApollo from 'helpers/mock_apollo_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue'; import { @@ -14,7 +13,6 @@ import { APPROVED_BY_YOU_AND_OTHERS, } from '~/vue_merge_request_widget/components/approvals/messages'; import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; -import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approved_by.query.graphql'; Vue.use(VueApollo); @@ -22,13 +20,11 @@ describe('MRWidget approvals summary', () => { const originalUserId = gon.current_user_id; let wrapper; - const createComponent = (response = approvedByCurrentUser) => { + const createComponent = (data = approvedByCurrentUser) => { wrapper = mount(ApprovalsSummary, { propsData: { - projectPath: 'gitlab-org/gitlab', - iid: '1', + approvalState: data.data.project.mergeRequest, }, - apolloProvider: createMockApollo([[approvedByQuery, jest.fn().mockResolvedValue(response)]]), }); }; diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js index f37276ad594..608d513e64b 100644 --- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js +++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js @@ -6,6 +6,7 @@ import VueApollo from 'vue-apollo'; import * as Sentry from '@sentry/browser'; import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json'; import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json'; +import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data'; @@ -28,6 +29,7 @@ import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_ import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql'; import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql'; +import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql'; import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql'; import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; @@ -100,6 +102,7 @@ describe('MrWidgetOptions', () => { ...options, apolloProvider: createMockApollo([ + [approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)], [ getStateQuery, jest.fn().mockResolvedValue({ diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb index cd66b93eb8b..bb38f4b1bca 100644 --- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::LegacyGithubImport::Importer do +RSpec.describe Gitlab::LegacyGithubImport::Importer, feature_category: :importers do + subject(:importer) { described_class.new(project) } + shared_examples 'Gitlab::LegacyGithubImport::Importer#execute' do let(:expected_not_called) { [] } @@ -11,8 +13,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do end it 'calls import methods' do - importer = described_class.new(project) - expected_called = [ :import_labels, :import_milestones, :import_pull_requests, :import_issues, :import_wiki, :import_releases, :handle_errors, @@ -51,11 +51,13 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) - allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) + allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request_missing_source_branch]) allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_raise(Octokit::NotFound) allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) + + allow(importer).to receive(:restore_source_branch).and_raise(StandardError, 'Some error') end let(:label1) do @@ -153,8 +155,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do } end - subject { described_class.new(project) } - it 'returns true' do expect(subject.execute).to eq true end @@ -163,18 +163,19 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do expect { subject.execute }.not_to raise_error end - it 'stores error messages' do + it 'stores error messages', :unlimited_max_formatted_output_length do error = { message: 'The remote data could not be fully imported.', errors: [ { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, + { type: :pull_request, url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", errors: 'Some error' }, { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, { type: :issues_comments, errors: 'Octokit::NotFound' }, { type: :wiki, errors: "Gitlab::Git::CommandError" } ] } - described_class.new(project).execute + importer.execute expect(project.import_state.last_error).to eq error.to_json end @@ -182,8 +183,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do shared_examples 'Gitlab::LegacyGithubImport unit-testing' do describe '#clean_up_restored_branches' do - subject { described_class.new(project) } - before do allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false } allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false } @@ -240,6 +239,16 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do } end + let(:pull_request_missing_source_branch) do + pull_request.merge( + head: { + ref: 'missing', + repo: repository, + sha: RepoHelpers.another_sample_commit + } + ) + end + let(:closed_pull_request) do { number: 1347, @@ -264,8 +273,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do let(:api_root) { 'https://try.gitea.io/api/v1' } let(:repo_root) { 'https://try.gitea.io' } - subject { described_class.new(project) } - before do project.update!(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git") end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 6c991f14156..4e3d8f633b7 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -571,17 +571,6 @@ RSpec.describe Namespace, feature_category: :subgroups do it { expect(child.traversal_ids).to eq [parent.id, child.id] } it { expect(parent.sync_events.count).to eq 1 } it { expect(child.sync_events.count).to eq 1 } - - context 'when set_traversal_ids_on_save feature flag is disabled' do - before do - stub_feature_flags(set_traversal_ids_on_save: false) - end - - it 'only sets traversal_ids on reload' do - expect { parent.reload }.to change(parent, :traversal_ids).from([]).to([parent.id]) - expect { child.reload }.to change(child, :traversal_ids).from([]).to([parent.id, child.id]) - end - end end context 'traversal_ids on update' do @@ -594,18 +583,6 @@ RSpec.describe Namespace, feature_category: :subgroups do it 'sets the traversal_ids attribute' do expect { subject }.to change { namespace1.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id]) end - - context 'when set_traversal_ids_on_save feature flag is disabled' do - before do - stub_feature_flags(set_traversal_ids_on_save: false) - end - - it 'sets traversal_ids after reload' do - subject - - expect { namespace1.reload }.to change(namespace1, :traversal_ids).from([]).to([namespace2.id, namespace1.id]) - end - end end it 'creates a Namespaces::SyncEvent using triggers' do diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb new file mode 100644 index 00000000000..bd22cc6706a --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + let(:mutation_params) do + { + description: 'create description', + maintenance_note: 'create maintenance note', + maximum_timeout: 900, + access_level: 'REF_PROTECTED', + paused: true, + run_untagged: false, + tag_list: %w[tag1 tag2] + } + end + + let(:mutation) do + variables = { + **mutation_params + } + + graphql_mutation( + :runner_create, + variables, + <<-QL + runner { + ephemeralAuthenticationToken + + runnerType + description + maintenanceNote + paused + tagList + accessLevel + locked + maximumTimeout + runUntagged + } + errors + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:runner_create) } + + context 'when user does not have permissions' do + let(:current_user) { user } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']).to contain_exactly "Insufficient permissions" + end + end + + context 'when user has permissions', :enable_admin_mode do + let(:current_user) { admin } + + context 'when :create_runner_workflow feature flag is disabled' do + before do + stub_feature_flags(create_runner_workflow: false) + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).not_to be_empty + expect(graphql_errors[0]['message']) + .to eq("`create_runner_workflow` feature flag is disabled.") + end + end + + context 'when success' do + it do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + + mutation_params.each_key do |key| + expect(mutation_response['runner'][key.to_s.camelize(:lower)]).to eq mutation_params[key] + end + + expect(mutation_response['runner']['ephemeralAuthenticationToken']).to start_with 'glrt' + + expect(mutation_response['errors']).to eq([]) + end + end + + context 'when failure' do + let(:mutation_params) do + { + description: "", + maintenanceNote: "", + paused: true, + accessLevel: "NOT_PROTECTED", + runUntagged: false, + tagList: + [], + maximumTimeout: 1 + } + end + + it do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']).to contain_exactly( + "Tags list can not be empty when runner is not allowed to pick untagged jobs", + "Maximum timeout needs to be at least 10 minutes" + ) + end + end + end +end diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb index 673bf3ef90e..52acfcbb7af 100644 --- a/spec/services/ci/runners/create_runner_service_spec.rb +++ b/spec/services/ci/runners/create_runner_service_spec.rb @@ -83,6 +83,49 @@ RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category: expect(runner.authenticated_user_registration_type?).to be_truthy expect(runner.runner_type).to eq 'instance_type' end + + context 'with a nil paused value' do + let(:args) do + { + paused: nil, + description: 'some description', + maintenance_note: 'a note', + tag_list: %w[tag1 tag2], + access_level: 'ref_protected', + locked: true, + maximum_timeout: 600, + run_untagged: false + } + end + + it { is_expected.to be_success } + + it 'creates runner with active set to true' do + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.active).to eq true + end + end + + context 'with no paused value given' do + let(:args) do + { + description: 'some description', + maintenance_note: 'a note', + tag_list: %w[tag1 tag2], + access_level: 'ref_protected', + locked: true, + maximum_timeout: 600, + run_untagged: false + } + end + + it { is_expected.to be_success } + + it 'creates runner with active set to true' do + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.active).to eq true + end + end end end diff --git a/spec/services/import/validate_remote_git_endpoint_service_spec.rb b/spec/services/import/validate_remote_git_endpoint_service_spec.rb index 221ac2cd73a..1d2b3975832 100644 --- a/spec/services/import/validate_remote_git_endpoint_service_spec.rb +++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Import::ValidateRemoteGitEndpointService do +RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :importers do include StubRequests let_it_be(:base_url) { 'http://demo.host/path' } @@ -35,6 +35,28 @@ RSpec.describe Import::ValidateRemoteGitEndpointService do end end + context 'when uri is using an invalid protocol' do + subject { described_class.new(url: 'ssh://demo.host/repo') } + + it 'reports error when invalid URL is provided' do + result = subject.execute + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + end + end + + context 'when uri is invalid' do + subject { described_class.new(url: 'http:example.com') } + + it 'reports error when invalid URL is provided' do + result = subject.execute + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + end + end + context 'when receiving HTTP response' do subject { described_class.new(url: base_url) } diff --git a/spec/tooling/danger/sidekiq_args_spec.rb b/spec/tooling/danger/sidekiq_args_spec.rb new file mode 100644 index 00000000000..bfa9ef169de --- /dev/null +++ b/spec/tooling/danger/sidekiq_args_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require 'rspec-parameterized' +require 'gitlab-dangerfiles' +require 'danger' +require 'danger/plugins/internal/helper' +require 'gitlab/dangerfiles/spec_helper' + +require_relative '../../../tooling/danger/sidekiq_args' +require_relative '../../../tooling/danger/project_helper' + +RSpec.describe Tooling::Danger::SidekiqArgs, feature_category: :tooling do + include_context "with dangerfile" + + let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } + let(:fake_project_helper) { Tooling::Danger::ProjectHelper } + + subject(:specs) { fake_danger.new(helper: fake_helper) } + + before do + allow(specs).to receive(:project_helper).and_return(fake_project_helper) + end + + describe '#args_changed?' do + using RSpec::Parameterized::TableSyntax + + where(:before, :after, :result) do + " - def perform" | " + def perform(abc)" | true + " - def perform" | " + def perform(abc)" | true + " - def perform(abc)" | " + def perform(def)" | true + " - def perform(abc, def)" | " + def perform(abc)" | true + " - def perform(abc, def)" | " + def perform(def, abc)" | true + " - def perform" | " - def perform" | false + " + def perform" | " + def perform" | false + " - def perform(abc)" | " - def perform(abc)" | false + " + def perform(abc)" | " + def perform(abc)" | false + " - def perform(abc)" | " + def perform_foo(abc)" | false + end + + with_them do + it 'returns correct result' do + expect(specs.args_changed?([before, after])).to eq(result) + end + end + end + + describe '#add_comment_for_matched_line' do + let(:filename) { 'app/workers/hello_worker.rb' } + let(:file_lines) do + [ + "Module Worker", + " def perform", + " puts hello world", + " end", + "end" + ] + end + + before do + allow(specs.project_helper).to receive(:file_lines).and_return(file_lines) + end + + context 'when args are changed' do + before do + allow(specs.helper).to receive(:changed_lines).and_return([" - def perform", " + def perform(abc)"]) + allow(specs).to receive(:args_changed?).and_return(true) + end + + it 'adds suggestion at the correct lines' do + expect(specs).to receive(:markdown).with(format(described_class::SUGGEST_MR_COMMENT), file: filename, line: 2) + + specs.add_comment_for_matched_line(filename) + end + end + + context 'when args are not changed' do + before do + allow(specs.helper).to receive(:changed_lines).and_return([" - def perform", " - def perform"]) + allow(specs).to receive(:args_changed?).and_return(false) + end + + it 'does not add suggestion' do + expect(specs).not_to receive(:markdown) + + specs.add_comment_for_matched_line(filename) + end + end + end + + describe '#changed_worker_files' do + let(:base_expected_files) { %w[app/workers/a.rb app/workers/b.rb ee/app/workers/e.rb] } + + before do + all_changed_files = %w[ + app/workers/a.rb + app/workers/b.rb + ee/app/workers/e.rb + spec/foo_spec.rb + ee/spec/foo_spec.rb + spec/bar_spec.rb + ee/spec/bar_spec.rb + spec/zab_spec.rb + ee/spec/zab_spec.rb + ] + + allow(specs.helper).to receive(:all_changed_files).and_return(all_changed_files) + end + + it 'returns added, modified, and renamed_after files by default' do + expect(specs.changed_worker_files).to match_array(base_expected_files) + end + + context 'with include_ee: :exclude' do + it 'returns spec files without EE-specific files' do + expect(specs.changed_worker_files(ee: :exclude)).not_to include(%w[ee/app/workers/e.rb]) + end + end + + context 'with include_ee: :only' do + it 'returns EE-specific spec files only' do + expect(specs.changed_worker_files(ee: :only)).to match_array(%w[ee/app/workers/e.rb]) + end + end + end +end |