diff options
Diffstat (limited to 'spec')
25 files changed, 983 insertions, 106 deletions
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 2f9303606b1..b66d0ffce87 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe GroupDescendantsFinder do - let(:user) { create(:user) } - let(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } let(:params) { {} } subject(:finder) do @@ -129,6 +129,39 @@ RSpec.describe GroupDescendantsFinder do end end + context 'with shared groups' do + let_it_be(:other_group) { create(:group) } + let_it_be(:shared_group_link) do + create(:group_group_link, + shared_group: group, + shared_with_group: other_group) + end + + context 'without common ancestor' do + it { expect(finder.execute).to be_empty } + end + + context 'with common ancestor' do + let_it_be(:common_ancestor) { create(:group) } + let_it_be(:other_group) { create(:group, parent: common_ancestor) } + let_it_be(:group) { create(:group, parent: common_ancestor) } + + context 'querying under the common ancestor' do + it { expect(finder.execute).to be_empty } + end + + context 'querying the common ancestor' do + subject(:finder) do + described_class.new(current_user: user, parent_group: common_ancestor, params: params) + end + + it 'contains shared subgroups' do + expect(finder.execute).to contain_exactly(group, other_group) + end + end + end + end + context 'with nested groups' do let!(:project) { create(:project, namespace: group) } let!(:subgroup) { create(:group, :private, parent: group) } diff --git a/spec/fixtures/api/schemas/graphql/container_repository_details.json b/spec/fixtures/api/schemas/graphql/container_repository_details.json new file mode 100644 index 00000000000..b076711dcea --- /dev/null +++ b/spec/fixtures/api/schemas/graphql/container_repository_details.json @@ -0,0 +1,78 @@ +{ + "type": "object", + "required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "tags"], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "location": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "expirationPolicyStartedAt": { + "type": ["string", "null"] + }, + "status": { + "type": ["string", "null"] + }, + "tagsCount": { + "type": "integer" + }, + "canDelete": { + "type": "boolean" + }, + "tags": { + "type": "object", + "required": ["nodes"], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "path", "location", "digest", "revision", "shortRevision", "totalSize", "createdAt", "canDelete"], + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "location": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "shortRevision": { + "type": "string" + }, + "totalSize": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "canDelete": { + "type": "boolean" + } + } + } + } + } + } + } +} diff --git a/spec/fixtures/api/schemas/internal/pages/lookup_path.json b/spec/fixtures/api/schemas/internal/pages/lookup_path.json index b2b3d3f9d0a..a8a059577a6 100644 --- a/spec/fixtures/api/schemas/internal/pages/lookup_path.json +++ b/spec/fixtures/api/schemas/internal/pages/lookup_path.json @@ -14,7 +14,7 @@ "source": { "type": "object", "required": ["type", "path"], "properties" : { - "type": { "type": "string", "enum": ["file"] }, + "type": { "type": "string", "enum": ["file", "zip", "zip_local"] }, "path": { "type": "string" } }, "additionalProperties": false diff --git a/spec/frontend/groups/members/index_spec.js b/spec/frontend/groups/members/index_spec.js index 2fb7904bcfe..aaa36665c45 100644 --- a/spec/frontend/groups/members/index_spec.js +++ b/spec/frontend/groups/members/index_spec.js @@ -9,7 +9,12 @@ describe('initGroupMembersApp', () => { let wrapper; const setup = () => { - vm = initGroupMembersApp(el, ['account'], () => ({})); + vm = initGroupMembersApp( + el, + ['account'], + { table: { 'data-qa-selector': 'members_list' } }, + () => ({}), + ); wrapper = createWrapper(vm); }; @@ -68,6 +73,12 @@ describe('initGroupMembersApp', () => { expect(vm.$store.state.tableFields).toEqual(['account']); }); + it('sets `tableAttrs` in Vuex store', () => { + setup(); + + expect(vm.$store.state.tableAttrs).toEqual({ table: { 'data-qa-selector': 'members_list' } }); + }); + it('sets `requestFormatter` in Vuex store', () => { setup(); diff --git a/spec/frontend/milestones/milestone_combobox_spec.js b/spec/frontend/milestones/milestone_combobox_spec.js index 2996c05d96e..047484f117f 100644 --- a/spec/frontend/milestones/milestone_combobox_spec.js +++ b/spec/frontend/milestones/milestone_combobox_spec.js @@ -6,7 +6,7 @@ import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; import { ENTER_KEY } from '~/lib/utils/keys'; import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue'; -import { milestones as projectMilestones } from './mock_data'; +import { projectMilestones, groupMilestones } from './mock_data'; import createStore from '~/milestones/stores/'; const extraLinks = [ @@ -19,16 +19,21 @@ localVue.use(Vuex); describe('Milestone combobox component', () => { const projectId = '8'; + const groupId = '24'; + const groupMilestonesAvailable = true; const X_TOTAL_HEADER = 'x-total'; let wrapper; let projectMilestonesApiCallSpy; + let groupMilestonesApiCallSpy; let searchApiCallSpy; const createComponent = (props = {}, attrs = {}) => { wrapper = mount(MilestoneCombobox, { propsData: { projectId, + groupId, + groupMilestonesAvailable, extraLinks, value: [], ...props, @@ -56,6 +61,10 @@ describe('Milestone combobox component', () => { .fn() .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]); + groupMilestonesApiCallSpy = jest + .fn() + .mockReturnValue([200, groupMilestones, { [X_TOTAL_HEADER]: '6' }]); + searchApiCallSpy = jest .fn() .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]); @@ -64,6 +73,10 @@ describe('Milestone combobox component', () => { .onGet(`/api/v4/projects/${projectId}/milestones`) .reply(config => projectMilestonesApiCallSpy(config)); + mock + .onGet(`/api/v4/groups/${groupId}/milestones`) + .reply(config => groupMilestonesApiCallSpy(config)); + mock.onGet(`/api/v4/projects/${projectId}/search`).reply(config => searchApiCallSpy(config)); }); @@ -89,6 +102,11 @@ describe('Milestone combobox component', () => { findProjectMilestonesSection().findAll(GlDropdownItem); const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0); + const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]'); + const findGroupMilestonesDropdownItems = () => + findGroupMilestonesSection().findAll(GlDropdownItem); + const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0); + // // Expecters // @@ -100,6 +118,14 @@ describe('Milestone combobox component', () => { .includes(s__('MilestoneCombobox|An error occurred while searching for milestones')); }; + const groupMilestoneSectionContainsErrorMessage = () => { + const groupMilestoneSection = findGroupMilestonesSection(); + + return groupMilestoneSection + .text() + .includes(s__('MilestoneCombobox|An error occurred while searching for milestones')); + }; + // // Convenience methods // @@ -111,19 +137,25 @@ describe('Milestone combobox component', () => { findFirstProjectMilestonesDropdownItem().vm.$emit('click'); }; + const selectFirstGroupMilestone = () => { + findFirstGroupMilestonesDropdownItem().vm.$emit('click'); + }; + const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) => axios.waitForAll().then(() => { if (andClearMocks) { projectMilestonesApiCallSpy.mockClear(); + groupMilestonesApiCallSpy.mockClear(); } }); describe('initialization behavior', () => { beforeEach(createComponent); - it('initializes the dropdown with project milestones when mounted', () => { + it('initializes the dropdown with milestones when mounted', () => { return waitForRequests().then(() => { expect(projectMilestonesApiCallSpy).toHaveBeenCalledTimes(1); + expect(groupMilestonesApiCallSpy).toHaveBeenCalledTimes(1); }); }); @@ -166,7 +198,7 @@ describe('Milestone combobox component', () => { return waitForRequests(); }); - it('renders the pre-selected project milestones', () => { + it('renders the pre-selected milestones', () => { expect(findButtonContent().text()).toBe('v0.1 + 5 more'); }); }); @@ -209,6 +241,8 @@ describe('Milestone combobox component', () => { .fn() .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); + groupMilestonesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); + createComponent(); return waitForRequests(); @@ -288,65 +322,195 @@ describe('Milestone combobox component', () => { expect(projectMilestoneSectionContainsErrorMessage()).toBe(true); }); }); - }); - describe('selection', () => { - beforeEach(() => { - createComponent(); + describe('selection', () => { + beforeEach(() => { + createComponent(); - return waitForRequests(); - }); + return waitForRequests(); + }); + + it('renders a checkmark by the selected item', async () => { + selectFirstProjectMilestone(); - it('renders a checkmark by the selected item', async () => { - selectFirstProjectMilestone(); + await localVue.nextTick(); - await localVue.nextTick(); + expect( + findFirstProjectMilestonesDropdownItem() + .find('span') + .classes('selected-item'), + ).toBe(false); - expect( - findFirstProjectMilestonesDropdownItem() - .find('span') - .classes('selected-item'), - ).toBe(false); + selectFirstProjectMilestone(); - selectFirstProjectMilestone(); + await localVue.nextTick(); - return localVue.nextTick().then(() => { expect( findFirstProjectMilestonesDropdownItem() .find('span') .classes('selected-item'), ).toBe(true); }); + + describe('when a project milestones is selected', () => { + beforeEach(() => { + createComponent(); + projectMilestonesApiCallSpy = jest + .fn() + .mockReturnValue([200, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]); + + return waitForRequests(); + }); + + it("displays the project milestones name in the dropdown's button", async () => { + selectFirstProjectMilestone(); + await localVue.nextTick(); + + expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone')); + + selectFirstProjectMilestone(); + + await localVue.nextTick(); + expect(findButtonContent().text()).toBe('v1.0'); + }); + + it('updates the v-model binding with the project milestone title', () => { + expect(wrapper.vm.value).toEqual([]); + + selectFirstProjectMilestone(); + + expect(wrapper.vm.value).toEqual(['v1.0']); + }); + }); }); + }); - describe('when a project milestones is selected', () => { + describe('group milestones', () => { + describe('when the group milestones search returns results', () => { beforeEach(() => { createComponent(); - projectMilestonesApiCallSpy = jest + + return waitForRequests(); + }); + + it('renders the group milestones section in the dropdown', () => { + expect(findGroupMilestonesSection().exists()).toBe(true); + }); + + it('renders the "Group milestones" heading with a total number indicator', () => { + expect( + findGroupMilestonesSection() + .find('[data-testid="milestone-results-section-header"]') + .text(), + ).toBe('Group milestones 6'); + }); + + it("does not render an error message in the group milestone section's body", () => { + expect(groupMilestoneSectionContainsErrorMessage()).toBe(false); + }); + + it('renders each group milestones as a selectable item', () => { + const dropdownItems = findGroupMilestonesDropdownItems(); + + groupMilestones.forEach((milestone, i) => { + expect(dropdownItems.at(i).text()).toBe(milestone.title); + }); + }); + }); + + describe('when the group milestones search returns no results', () => { + beforeEach(() => { + groupMilestonesApiCallSpy = jest .fn() - .mockReturnValue([200, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]); + .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]); + + createComponent(); return waitForRequests(); }); - it("displays the project milestones name in the dropdown's button", async () => { - selectFirstProjectMilestone(); + it('does not render the group milestones section in the dropdown', () => { + expect(findGroupMilestonesSection().exists()).toBe(false); + }); + }); + + describe('when the group milestones search returns an error', () => { + beforeEach(() => { + groupMilestonesApiCallSpy = jest.fn().mockReturnValue([500]); + searchApiCallSpy = jest.fn().mockReturnValue([500]); + + createComponent({ value: [] }); + + return waitForRequests(); + }); + + it('renders the group milestones section in the dropdown', () => { + expect(findGroupMilestonesSection().exists()).toBe(true); + }); + + it("renders an error message in the group milestones section's body", () => { + expect(groupMilestoneSectionContainsErrorMessage()).toBe(true); + }); + }); + + describe('selection', () => { + beforeEach(() => { + createComponent(); + + return waitForRequests(); + }); + + it('renders a checkmark by the selected item', async () => { + selectFirstGroupMilestone(); + await localVue.nextTick(); - expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone')); + expect( + findFirstGroupMilestonesDropdownItem() + .find('span') + .classes('selected-item'), + ).toBe(false); - selectFirstProjectMilestone(); + selectFirstGroupMilestone(); await localVue.nextTick(); - expect(findButtonContent().text()).toBe('v1.0'); + + expect( + findFirstGroupMilestonesDropdownItem() + .find('span') + .classes('selected-item'), + ).toBe(true); }); - it('updates the v-model binding with the project milestone title', () => { - expect(wrapper.vm.value).toEqual([]); + describe('when a group milestones is selected', () => { + beforeEach(() => { + createComponent(); + groupMilestonesApiCallSpy = jest + .fn() + .mockReturnValue([200, [{ title: 'group-v1.0' }], { [X_TOTAL_HEADER]: '1' }]); - selectFirstProjectMilestone(); + return waitForRequests(); + }); + + it("displays the group milestones name in the dropdown's button", async () => { + selectFirstGroupMilestone(); + await localVue.nextTick(); - expect(wrapper.vm.value).toEqual(['v1.0']); + expect(findButtonContent().text()).toBe(s__('MilestoneCombobox|No milestone')); + + selectFirstGroupMilestone(); + + await localVue.nextTick(); + expect(findButtonContent().text()).toBe('group-v1.0'); + }); + + it('updates the v-model binding with the group milestone title', () => { + expect(wrapper.vm.value).toEqual([]); + + selectFirstGroupMilestone(); + + expect(wrapper.vm.value).toEqual(['group-v1.0']); + }); }); }); }); diff --git a/spec/frontend/milestones/mock_data.js b/spec/frontend/milestones/mock_data.js index c64eeeba663..71fbfe54141 100644 --- a/spec/frontend/milestones/mock_data.js +++ b/spec/frontend/milestones/mock_data.js @@ -1,4 +1,4 @@ -export const milestones = [ +export const projectMilestones = [ { id: 41, iid: 6, @@ -79,4 +79,94 @@ export const milestones = [ }, ]; -export default milestones; +export const groupMilestones = [ + { + id: 141, + iid: 16, + project_id: 8, + group_id: 12, + title: 'group-v0.1', + description: '', + state: 'active', + created_at: '2020-04-04T01:30:40.051Z', + updated_at: '2020-04-04T01:30:40.051Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6', + }, + { + id: 140, + iid: 15, + project_id: 8, + group_id: 12, + title: 'group-v4.0', + description: 'Laboriosam nisi sapiente dolores et magnam nobis ad earum.', + state: 'closed', + created_at: '2020-01-13T19:39:15.191Z', + updated_at: '2020-01-13T19:39:15.191Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/5', + }, + { + id: 139, + iid: 14, + project_id: 8, + group_id: 12, + title: 'group-v3.0', + description: 'Necessitatibus illo alias et repellat dolorum assumenda ut.', + state: 'closed', + created_at: '2020-01-13T19:39:15.176Z', + updated_at: '2020-01-13T19:39:15.176Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/4', + }, + { + id: 138, + iid: 13, + project_id: 8, + group_id: 12, + title: 'group-v2.0', + description: 'Doloribus qui repudiandae iste sit.', + state: 'closed', + created_at: '2020-01-13T19:39:15.161Z', + updated_at: '2020-01-13T19:39:15.161Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/3', + }, + { + id: 137, + iid: 12, + project_id: 8, + group_id: 12, + title: 'group-v1.0', + description: 'Illo sint odio officia ea.', + state: 'closed', + created_at: '2020-01-13T19:39:15.146Z', + updated_at: '2020-01-13T19:39:15.146Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/2', + }, + { + id: 136, + iid: 11, + project_id: 8, + group_id: 12, + title: 'group-v0.0', + description: 'Sed quae facilis deleniti at delectus assumenda nobis veritatis.', + state: 'active', + created_at: '2020-01-13T19:39:15.127Z', + updated_at: '2020-01-13T19:39:15.127Z', + due_date: null, + start_date: null, + web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/1', + }, +]; + +export default { + projectMilestones, + groupMilestones, +}; diff --git a/spec/frontend/milestones/stores/actions_spec.js b/spec/frontend/milestones/stores/actions_spec.js index e14eb9280e4..a62b0c49a80 100644 --- a/spec/frontend/milestones/stores/actions_spec.js +++ b/spec/frontend/milestones/stores/actions_spec.js @@ -4,6 +4,7 @@ import * as actions from '~/milestones/stores/actions'; import * as types from '~/milestones/stores/mutation_types'; let mockProjectMilestonesReturnValue; +let mockGroupMilestonesReturnValue; let mockProjectSearchReturnValue; jest.mock('~/api', () => ({ @@ -13,6 +14,7 @@ jest.mock('~/api', () => ({ default: { projectMilestones: () => mockProjectMilestonesReturnValue, projectSearch: () => mockProjectSearchReturnValue, + groupMilestones: () => mockGroupMilestonesReturnValue, }, })); @@ -32,6 +34,24 @@ describe('Milestone combobox Vuex store actions', () => { }); }); + describe('setGroupId', () => { + it(`commits ${types.SET_GROUP_ID} with the new group ID`, () => { + const groupId = '123'; + testAction(actions.setGroupId, groupId, state, [ + { type: types.SET_GROUP_ID, payload: groupId }, + ]); + }); + }); + + describe('setGroupMilestonesAvailable', () => { + it(`commits ${types.SET_GROUP_MILESTONES_AVAILABLE} with the boolean indicating if group milestones are available (Premium)`, () => { + state.groupMilestonesAvailable = true; + testAction(actions.setGroupMilestonesAvailable, state.groupMilestonesAvailable, state, [ + { type: types.SET_GROUP_MILESTONES_AVAILABLE, payload: state.groupMilestonesAvailable }, + ]); + }); + }); + describe('setSelectedMilestones', () => { it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => { const selectedMilestones = ['v1.2.3']; @@ -66,19 +86,38 @@ describe('Milestone combobox Vuex store actions', () => { }); describe('search', () => { - it(`commits ${types.SET_SEARCH_QUERY} with the new search query`, () => { - const searchQuery = 'v1.0'; - testAction( - actions.search, - searchQuery, - state, - [{ type: types.SET_SEARCH_QUERY, payload: searchQuery }], - [{ type: 'searchMilestones' }], - ); + describe('when project has license to add group milestones', () => { + it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project and group milestones`, () => { + const getters = { + groupMilestonesEnabled: () => true, + }; + + const searchQuery = 'v1.0'; + testAction( + actions.search, + searchQuery, + { ...state, ...getters }, + [{ type: types.SET_SEARCH_QUERY, payload: searchQuery }], + [{ type: 'searchProjectMilestones' }, { type: 'searchGroupMilestones' }], + ); + }); + }); + + describe('when project does not have license to add group milestones', () => { + it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project milestones`, () => { + const searchQuery = 'v1.0'; + testAction( + actions.search, + searchQuery, + state, + [{ type: types.SET_SEARCH_QUERY, payload: searchQuery }], + [{ type: 'searchProjectMilestones' }], + ); + }); }); }); - describe('searchMilestones', () => { + describe('searchProjectMilestones', () => { describe('when the search is successful', () => { const projectSearchApiResponse = { data: [{ title: 'v1.0' }] }; @@ -87,7 +126,7 @@ describe('Milestone combobox Vuex store actions', () => { }); it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { - return testAction(actions.searchMilestones, undefined, state, [ + return testAction(actions.searchProjectMilestones, undefined, state, [ { type: types.REQUEST_START }, { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse }, { type: types.REQUEST_FINISH }, @@ -103,7 +142,7 @@ describe('Milestone combobox Vuex store actions', () => { }); it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { - return testAction(actions.searchMilestones, undefined, state, [ + return testAction(actions.searchProjectMilestones, undefined, state, [ { type: types.REQUEST_START }, { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, { type: types.REQUEST_FINISH }, @@ -112,7 +151,71 @@ describe('Milestone combobox Vuex store actions', () => { }); }); + describe('searchGroupMilestones', () => { + describe('when the search is successful', () => { + const groupSearchApiResponse = { data: [{ title: 'group-v1.0' }] }; + + beforeEach(() => { + mockGroupMilestonesReturnValue = Promise.resolve(groupSearchApiResponse); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.searchGroupMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupSearchApiResponse }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + + describe('when the search fails', () => { + const error = new Error('Something went wrong!'); + + beforeEach(() => { + mockGroupMilestonesReturnValue = Promise.reject(error); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.searchGroupMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + }); + describe('fetchMilestones', () => { + describe('when project has license to add group milestones', () => { + it(`dispatchs fetchProjectMilestones and fetchGroupMilestones`, () => { + const getters = { + groupMilestonesEnabled: () => true, + }; + + testAction( + actions.fetchMilestones, + undefined, + { ...state, ...getters }, + [], + [{ type: 'fetchProjectMilestones' }, { type: 'fetchGroupMilestones' }], + ); + }); + }); + + describe('when project does not have license to add group milestones', () => { + it(`dispatchs fetchProjectMilestones`, () => { + testAction( + actions.fetchMilestones, + undefined, + state, + [], + [{ type: 'fetchProjectMilestones' }], + ); + }); + }); + }); + + describe('fetchProjectMilestones', () => { describe('when the fetch is successful', () => { const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] }; @@ -121,7 +224,7 @@ describe('Milestone combobox Vuex store actions', () => { }); it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { - return testAction(actions.fetchMilestones, undefined, state, [ + return testAction(actions.fetchProjectMilestones, undefined, state, [ { type: types.REQUEST_START }, { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse }, { type: types.REQUEST_FINISH }, @@ -137,7 +240,7 @@ describe('Milestone combobox Vuex store actions', () => { }); it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { - return testAction(actions.fetchMilestones, undefined, state, [ + return testAction(actions.fetchProjectMilestones, undefined, state, [ { type: types.REQUEST_START }, { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, { type: types.REQUEST_FINISH }, @@ -145,4 +248,38 @@ describe('Milestone combobox Vuex store actions', () => { }); }); }); + + describe('fetchGroupMilestones', () => { + describe('when the fetch is successful', () => { + const groupMilestonesApiResponse = { data: [{ title: 'group-v1.0' }] }; + + beforeEach(() => { + mockGroupMilestonesReturnValue = Promise.resolve(groupMilestonesApiResponse); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.fetchGroupMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupMilestonesApiResponse }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + + describe('when the fetch fails', () => { + const error = new Error('Something went wrong!'); + + beforeEach(() => { + mockGroupMilestonesReturnValue = Promise.reject(error); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.fetchGroupMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + }); }); diff --git a/spec/frontend/milestones/stores/getter_spec.js b/spec/frontend/milestones/stores/getter_spec.js index df7c3d28e67..4a6116b642c 100644 --- a/spec/frontend/milestones/stores/getter_spec.js +++ b/spec/frontend/milestones/stores/getter_spec.js @@ -12,4 +12,22 @@ describe('Milestone comboxbox Vuex store getters', () => { expect(getters.isLoading({ requestCount })).toBe(isLoading); }); }); + + describe('groupMilestonesEnabled', () => { + it.each` + groupId | groupMilestonesAvailable | groupMilestonesEnabled + ${'1'} | ${true} | ${true} + ${'1'} | ${false} | ${false} + ${''} | ${true} | ${false} + ${''} | ${false} | ${false} + ${null} | ${true} | ${false} + `( + 'returns true when groupId is a truthy string and groupMilestonesAvailable is true', + ({ groupId, groupMilestonesAvailable, groupMilestonesEnabled }) => { + expect(getters.groupMilestonesEnabled({ groupId, groupMilestonesAvailable })).toBe( + groupMilestonesEnabled, + ); + }, + ); + }); }); diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js index 236e0a49ebe..0b69a9d572d 100644 --- a/spec/frontend/milestones/stores/mutations_spec.js +++ b/spec/frontend/milestones/stores/mutations_spec.js @@ -14,6 +14,7 @@ describe('Milestones combobox Vuex store mutations', () => { expect(state).toEqual({ projectId: null, groupId: null, + groupMilestonesAvailable: false, searchQuery: '', matches: { projectMilestones: { @@ -21,6 +22,11 @@ describe('Milestones combobox Vuex store mutations', () => { totalCount: 0, error: null, }, + groupMilestones: { + list: [], + totalCount: 0, + error: null, + }, }, selectedMilestones: [], requestCount: 0, @@ -37,6 +43,24 @@ describe('Milestones combobox Vuex store mutations', () => { }); }); + describe(`${types.SET_GROUP_ID}`, () => { + it('updates the group ID', () => { + const newGroupId = '8'; + mutations[types.SET_GROUP_ID](state, newGroupId); + + expect(state.groupId).toBe(newGroupId); + }); + }); + + describe(`${types.SET_GROUP_MILESTONES_AVAILABLE}`, () => { + it('sets boolean indicating if group milestones are available', () => { + const groupMilestonesAvailable = true; + mutations[types.SET_GROUP_MILESTONES_AVAILABLE](state, groupMilestonesAvailable); + + expect(state.groupMilestonesAvailable).toBe(groupMilestonesAvailable); + }); + }); + describe(`${types.SET_SELECTED_MILESTONES}`, () => { it('sets the selected milestones', () => { const selectedMilestones = ['v1.2.3']; @@ -60,7 +84,7 @@ describe('Milestones combobox Vuex store mutations', () => { }); }); - describe(`${types.ADD_SELECTED_MILESTONESs}`, () => { + describe(`${types.ADD_SELECTED_MILESTONES}`, () => { it('adds the selected milestones', () => { const selectedMilestone = 'v1.2.3'; mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone); @@ -170,4 +194,57 @@ describe('Milestones combobox Vuex store mutations', () => { }); }); }); + + describe(`${types.RECEIVE_GROUP_MILESTONES_SUCCESS}`, () => { + it('updates state.matches.groupMilestones based on the provided API response', () => { + const response = { + data: [ + { + title: 'group-0.1', + }, + { + title: 'group-0.2', + }, + ], + headers: { + 'x-total': 2, + }, + }; + + mutations[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response); + + expect(state.matches.groupMilestones).toEqual({ + list: [ + { + title: 'group-0.1', + }, + { + title: 'group-0.2', + }, + ], + error: null, + totalCount: 2, + }); + }); + + describe(`${types.RECEIVE_GROUP_MILESTONES_ERROR}`, () => { + it('updates state.matches.groupMilestones to an empty state with the error object', () => { + const error = new Error('Something went wrong!'); + + state.matches.groupMilestones = { + list: [{ title: 'group-0.1' }], + totalCount: 1, + error: null, + }; + + mutations[types.RECEIVE_GROUP_MILESTONES_ERROR](state, error); + + expect(state.matches.groupMilestones).toEqual({ + list: [], + totalCount: 0, + error, + }); + }); + }); + }); }); diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js index d92bdc3b99a..c0680acb7cd 100644 --- a/spec/frontend/releases/components/app_edit_new_spec.js +++ b/spec/frontend/releases/components/app_edit_new_spec.js @@ -27,6 +27,8 @@ describe('Release edit/new component', () => { updateReleaseApiDocsPath: 'path/to/update/release/api/docs', releasesPagePath: 'path/to/releases/page', projectId: '8', + groupId: '42', + groupMilestonesAvailable: true, }; actions = { diff --git a/spec/frontend/vue_shared/components/members/table/members_table_spec.js b/spec/frontend/vue_shared/components/members/table/members_table_spec.js index 39234e230dc..e593e88438c 100644 --- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js +++ b/spec/frontend/vue_shared/components/members/table/members_table_spec.js @@ -5,7 +5,7 @@ import { getByTestId as getByTestIdHelper, within, } from '@testing-library/dom'; -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlTable } from '@gitlab/ui'; import MembersTable from '~/vue_shared/components/members/table/members_table.vue'; import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue'; import MemberSource from '~/vue_shared/components/members/table/member_source.vue'; @@ -28,6 +28,10 @@ describe('MemberList', () => { state: { members: [], tableFields: [], + tableAttrs: { + table: { 'data-qa-selector': 'members_list' }, + tr: { 'data-qa-selector': 'member_row' }, + }, sourceId: 1, currentUserId: 1, ...state, @@ -58,6 +62,8 @@ describe('MemberList', () => { const getByTestId = (id, options) => createWrapper(getByTestIdHelper(wrapper.element, id, options)); + const findTable = () => wrapper.find(GlTable); + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -187,4 +193,20 @@ describe('MemberList', () => { expect(initUserPopoversMock).toHaveBeenCalled(); }); + + it('adds QA selector to table', () => { + createComponent(); + + expect(findTable().attributes('data-qa-selector')).toBe('members_list'); + }); + + it('adds QA selector to table row', () => { + createComponent(); + + expect( + findTable() + .find('tbody tr') + .attributes('data-qa-selector'), + ).toBe('member_row'); + }); }); diff --git a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb index 145aada019e..bf8d2139c82 100644 --- a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb +++ b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb @@ -65,7 +65,9 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do context 'blank id' do let(:args) { { id: '' } } - it_behaves_like 'it resolves to nil' + it 'responds with an error' do + expect { resolve_error(args) }.to raise_error(::GraphQL::CoercionError) + end end end diff --git a/spec/graphql/types/container_repository_details_type_spec.rb b/spec/graphql/types/container_repository_details_type_spec.rb new file mode 100644 index 00000000000..13563dbb5aa --- /dev/null +++ b/spec/graphql/types/container_repository_details_type_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do + fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete tags] + + it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') } + + it { expect(described_class.description).to eq('Details of a container repository') } + + it { expect(described_class).to require_graphql_authorizations(:read_container_image) } + + it { expect(described_class).to have_graphql_fields(fields) } + + describe 'tags field' do + subject { described_class.fields['tags'] } + + it 'returns tags connection type' do + is_expected.to have_graphql_type(Types::ContainerRepositoryTagType.connection_type) + end + end +end diff --git a/spec/graphql/types/container_repository_tag_type_spec.rb b/spec/graphql/types/container_repository_tag_type_spec.rb new file mode 100644 index 00000000000..1d1a76d6916 --- /dev/null +++ b/spec/graphql/types/container_repository_tag_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ContainerRepositoryTag'] do + fields = %i[name path location digest revision short_revision total_size created_at can_delete] + + it { expect(described_class.graphql_name).to eq('ContainerRepositoryTag') } + + it { expect(described_class.description).to eq('A tag from a container repository') } + + it { expect(described_class).to require_graphql_authorizations(:read_container_image) } + + it { expect(described_class).to have_graphql_fields(fields) } +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index eee92fbb61d..7a0b3035607 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -88,4 +88,10 @@ RSpec.describe GitlabSchema.types['Query'] do is_expected.to have_graphql_type(Types::Ci::RunnerSetupType) end end + + describe 'container_repository field' do + subject { described_class.fields['containerRepository'] } + + it { is_expected.to have_graphql_type(Types::ContainerRepositoryDetailsType) } + end end diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb index 704e8dc40cb..7dc1328f065 100644 --- a/spec/helpers/releases_helper_spec.rb +++ b/spec/helpers/releases_helper_spec.rb @@ -64,6 +64,8 @@ RSpec.describe ReleasesHelper do describe '#data_for_edit_release_page' do it 'has the needed data to display the "edit release" page' do keys = %i(project_id + group_id + group_milestones_available project_path tag_name markdown_preview_path @@ -81,6 +83,8 @@ RSpec.describe ReleasesHelper do describe '#data_for_new_release_page' do it 'has the needed data to display the "new release" page' do keys = %i(project_id + group_id + group_milestones_available project_path releases_page_path markdown_preview_path diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 88d08f1ec45..065e756ea28 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -625,7 +625,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe "coverage" do + describe '#coverage' do let(:project) { create(:project, build_coverage_regex: "/.*/") } let(:pipeline) { create(:ci_empty_pipeline, project: project) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index a18aea38eac..85f9005052e 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -1271,24 +1271,6 @@ RSpec.describe Namespace do expect(virtual_domain.lookup_paths).not_to be_empty end end - - it 'preloads project_feature and route' do - project2 = create(:project, namespace: namespace) - project3 = create(:project, namespace: namespace) - - project.mark_pages_as_deployed - project2.mark_pages_as_deployed - project3.mark_pages_as_deployed - - virtual_domain = namespace.pages_virtual_domain - - queries = ActiveRecord::QueryRecorder.new { virtual_domain.lookup_paths } - - # 1 to load projects - # 1 to preload project features - # 1 to load routes - expect(queries.count).to eq(3) - end end end diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index cb1938a0113..bd890a71dfd 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -3,15 +3,14 @@ require 'spec_helper' RSpec.describe Pages::LookupPath do - let_it_be(:project) do - create(:project, :pages_private, pages_https_only: true) - end + let(:project) { create(:project, :pages_private, pages_https_only: true) } subject(:lookup_path) { described_class.new(project) } before do stub_pages_setting(access_control: true, external_https: ["1.1.1.1:443"]) stub_artifacts_object_storage + stub_pages_object_storage(::Pages::DeploymentUploader) end describe '#project_id' do @@ -47,18 +46,63 @@ RSpec.describe Pages::LookupPath do end describe '#source' do - shared_examples 'uses disk storage' do - it 'sets the source type to "file"' do - expect(lookup_path.source[:type]).to eq('file') - end + let(:source) { lookup_path.source } - it 'sets the source path to the project full path suffixed with "public/' do - expect(lookup_path.source[:path]).to eq(project.full_path + "/public/") + shared_examples 'uses disk storage' do + it 'uses disk storage', :aggregate_failures do + expect(source[:type]).to eq('file') + expect(source[:path]).to eq(project.full_path + "/public/") end end include_examples 'uses disk storage' + context 'when there is pages deployment' do + let(:deployment) { create(:pages_deployment, project: project) } + + before do + project.mark_pages_as_deployed + project.pages_metadatum.update!(pages_deployment: deployment) + end + + it 'uses deployment from object storage', :aggregate_failures do + Timecop.freeze do + expect(source[:type]).to eq('zip') + expect(source[:path]).to eq(deployment.file.url(expire_at: 1.day.from_now)) + expect(source[:path]).to include("Expires=86400") + end + end + + context 'when deployment is in the local storage' do + before do + deployment.file.migrate!(::ObjectStorage::Store::LOCAL) + end + + it 'uses file protocol', :aggregate_failures do + Timecop.freeze do + expect(source[:type]).to eq('zip') + expect(source[:path]).to eq('file://' + deployment.file.path) + end + end + + context 'when pages_serve_with_zip_file_protocol feature flag is disabled' do + before do + stub_feature_flags(pages_serve_with_zip_file_protocol: false) + end + + include_examples 'uses disk storage' + end + end + + context 'when pages_serve_from_deployments feature flag is disabled' do + before do + stub_feature_flags(pages_serve_from_deployments: false) + end + + include_examples 'uses disk storage' + end + end + context 'when artifact_id from build job is present in pages metadata' do let(:artifacts_archive) { create(:ci_job_artifact, :zip, :remote_store, project: project) } @@ -66,26 +110,36 @@ RSpec.describe Pages::LookupPath do project.mark_pages_as_deployed(artifacts_archive: artifacts_archive) end - it 'sets the source type to "zip"' do - expect(lookup_path.source[:type]).to eq('zip') - end - - it 'sets the source path to the artifacts archive URL' do + it 'uses artifacts object storage', :aggregate_failures do Timecop.freeze do - expect(lookup_path.source[:path]).to eq(artifacts_archive.file.url(expire_at: 1.day.from_now)) - expect(lookup_path.source[:path]).to include("Expires=86400") + expect(source[:type]).to eq('zip') + expect(source[:path]).to eq(artifacts_archive.file.url(expire_at: 1.day.from_now)) + expect(source[:path]).to include("Expires=86400") end end context 'when artifact is not uploaded to object storage' do let(:artifacts_archive) { create(:ci_job_artifact, :zip) } - include_examples 'uses disk storage' + it 'uses file protocol', :aggregate_failures do + Timecop.freeze do + expect(source[:type]).to eq('zip') + expect(source[:path]).to eq('file://' + artifacts_archive.file.path) + end + end + + context 'when pages_serve_with_zip_file_protocol feature flag is disabled' do + before do + stub_feature_flags(pages_serve_with_zip_file_protocol: false) + end + + include_examples 'uses disk storage' + end end context 'when feature flag is disabled' do before do - stub_feature_flags(pages_artifacts_archive: false) + stub_feature_flags(pages_serve_from_artifacts_archive: false) end include_examples 'uses disk storage' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4c254f54590..2b7268fd380 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2906,6 +2906,34 @@ RSpec.describe User do subject { user.authorized_groups } it { is_expected.to contain_exactly private_group, project_group } + + context 'with shared memberships' do + let!(:shared_group) { create(:group) } + let!(:other_group) { create(:group) } + + before do + create(:group_group_link, shared_group: shared_group, shared_with_group: private_group) + create(:group_group_link, shared_group: private_group, shared_with_group: other_group) + end + + context 'when shared_group_membership_auth is enabled' do + before do + stub_feature_flags(shared_group_membership_auth: user) + end + + it { is_expected.to include shared_group } + it { is_expected.not_to include other_group } + end + + context 'when shared_group_membership_auth is disabled' do + before do + stub_feature_flags(shared_group_membership_auth: false) + end + + it { is_expected.not_to include shared_group } + it { is_expected.not_to include other_group } + end + end end describe '#membership_groups' do diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb new file mode 100644 index 00000000000..a63adb8efc4 --- /dev/null +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'container repository details' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:container_repository) { create(:container_repository, project: project) } + + let(:query) do + graphql_query_for( + 'containerRepository', + { id: container_repository_global_id }, + all_graphql_fields_for('ContainerRepositoryDetails') + ) + end + + let(:user) { project.owner } + let(:variables) { {} } + let(:tags) { %w(latest tag1 tag2 tag3 tag4 tag5) } + let(:container_repository_global_id) { container_repository.to_global_id.to_s } + let(:container_repository_details_response) { graphql_data.dig('containerRepository') } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: container_repository.path, tags: tags, with_manifest: true) + end + + subject { post_graphql(query, current_user: user, variables: variables) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + + it 'matches the expected schema' do + expect(container_repository_details_response).to match_schema('graphql/container_repository_details') + end + end + + context 'with different permissions' do + let_it_be(:user) { create(:user) } + + let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') } + + where(:project_visibility, :role, :access_granted, :can_delete) do + :private | :maintainer | true | true + :private | :developer | true | true + :private | :reporter | true | false + :private | :guest | false | false + :private | :anonymous | false | false + :public | :maintainer | true | true + :public | :developer | true | true + :public | :reporter | true | false + :public | :guest | true | false + :public | :anonymous | true | false + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false)) + project.add_user(user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if access_granted + expect(tags_response.size).to eq(tags.size) + expect(container_repository_details_response.dig('canDelete')).to eq(can_delete) + else + expect(container_repository_details_response).to eq(nil) + end + end + end + end + + context 'limiting the number of tags' do + let(:limit) { 2 } + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:variables) do + { id: container_repository_global_id, n: limit } + end + + let(:query) do + <<~GQL + query($id: ID!, $n: Int) { + containerRepository(id: $id) { + tags(first: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'only returns n tags' do + subject + + expect(tags_response.size).to eq(limit) + end + end +end diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb index 4fa3c030761..bcf689a5e8f 100644 --- a/spec/requests/api/graphql/group/container_repositories_spec.rb +++ b/spec/requests/api/graphql/group/container_repositories_spec.rb @@ -92,9 +92,9 @@ RSpec.describe 'getting container repositories in a group' do end context 'limiting the number of repositories' do - let(:issue_limit) { 1 } + let(:limit) { 1 } let(:variables) do - { path: group.full_path, n: issue_limit } + { path: group.full_path, n: limit } end let(:query) do @@ -107,10 +107,10 @@ RSpec.describe 'getting container repositories in a group' do GQL end - it 'only returns N issues' do + it 'only returns N repositories' do subject - expect(container_repositories_response.size).to eq(issue_limit) + expect(container_repositories_response.size).to eq(limit) end end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index 8790314fa76..428424802a2 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -87,9 +87,9 @@ RSpec.describe 'getting container repositories in a project' do end context 'limiting the number of repositories' do - let(:issue_limit) { 1 } + let(:limit) { 1 } let(:variables) do - { path: project.full_path, n: issue_limit } + { path: project.full_path, n: limit } end let(:query) do @@ -102,10 +102,10 @@ RSpec.describe 'getting container repositories in a project' do GQL end - it 'only returns N issues' do + it 'only returns N repositories' do subject - expect(container_repositories_response.size).to eq(issue_limit) + expect(container_repositories_response.size).to eq(limit) end end diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb index cd84ce9cb96..acf5201a68c 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb @@ -191,7 +191,7 @@ RSpec.describe 'sentry errors requests' do describe 'getting a stack trace' do let_it_be(:sentry_stack_trace) { build(:error_tracking_error_event) } - let(:sentry_gid) { Gitlab::ErrorTracking::DetailedError.new(id: 1).to_global_id.to_s } + let(:sentry_gid) { global_id_of(Gitlab::ErrorTracking::DetailedError.new(id: 1)) } let(:stack_trace_fields) do all_graphql_fields_for('SentryErrorStackTrace'.classify) diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index e58eba02132..7f17f22b007 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -12,6 +12,7 @@ RSpec.describe API::Internal::Pages do before do allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret) + stub_pages_object_storage(::Pages::DeploymentUploader) end describe "GET /internal/pages/status" do @@ -38,6 +39,12 @@ RSpec.describe API::Internal::Pages do get api("/internal/pages"), headers: headers, params: { host: host } end + around do |example| + freeze_time do + example.run + end + end + context 'not authenticated' do it 'responds with 401 Unauthorized' do query_host('pages.gitlab.io') @@ -55,7 +62,9 @@ RSpec.describe API::Internal::Pages do end def deploy_pages(project) + deployment = create(:pages_deployment, project: project) project.mark_pages_as_deployed + project.update_pages_deployment!(deployment) end context 'domain does not exist' do @@ -190,8 +199,8 @@ RSpec.describe API::Internal::Pages do 'https_only' => false, 'prefix' => '/', 'source' => { - 'type' => 'file', - 'path' => 'gitlab-org/gitlab-ce/public/' + 'type' => 'zip', + 'path' => project.pages_metadatum.pages_deployment.file.url(expire_at: 1.day.from_now) } } ] @@ -226,8 +235,8 @@ RSpec.describe API::Internal::Pages do 'https_only' => false, 'prefix' => '/myproject/', 'source' => { - 'type' => 'file', - 'path' => 'mygroup/myproject/public/' + 'type' => 'zip', + 'path' => project.pages_metadatum.pages_deployment.file.url(expire_at: 1.day.from_now) } } ] @@ -235,6 +244,20 @@ RSpec.describe API::Internal::Pages do end end + it 'avoids N+1 queries' do + project = create(:project, group: group) + deploy_pages(project) + + control = ActiveRecord::QueryRecorder.new { query_host('mygroup.gitlab-pages.io') } + + 3.times do + project = create(:project, group: group) + deploy_pages(project) + end + + expect { query_host('mygroup.gitlab-pages.io') }.not_to exceed_query_limit(control) + end + context 'group root project' do it 'responds with the correct domain configuration' do project = create(:project, group: group, name: 'mygroup.gitlab-pages.io') @@ -253,8 +276,8 @@ RSpec.describe API::Internal::Pages do 'https_only' => false, 'prefix' => '/', 'source' => { - 'type' => 'file', - 'path' => 'mygroup/mygroup.gitlab-pages.io/public/' + 'type' => 'zip', + 'path' => project.pages_metadatum.pages_deployment.file.url(expire_at: 1.day.from_now) } } ] |