Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/finders/group_descendants_finder_spec.rb37
-rw-r--r--spec/fixtures/api/schemas/graphql/container_repository_details.json78
-rw-r--r--spec/fixtures/api/schemas/internal/pages/lookup_path.json2
-rw-r--r--spec/frontend/groups/members/index_spec.js13
-rw-r--r--spec/frontend/milestones/milestone_combobox_spec.js226
-rw-r--r--spec/frontend/milestones/mock_data.js94
-rw-r--r--spec/frontend/milestones/stores/actions_spec.js165
-rw-r--r--spec/frontend/milestones/stores/getter_spec.js18
-rw-r--r--spec/frontend/milestones/stores/mutations_spec.js79
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/members/table/members_table_spec.js24
-rw-r--r--spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb4
-rw-r--r--spec/graphql/types/container_repository_details_type_spec.rb23
-rw-r--r--spec/graphql/types/container_repository_tag_type_spec.rb15
-rw-r--r--spec/graphql/types/query_type_spec.rb6
-rw-r--r--spec/helpers/releases_helper_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb18
-rw-r--r--spec/models/pages/lookup_path_spec.rb90
-rw-r--r--spec/models/user_spec.rb28
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb108
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb2
-rw-r--r--spec/requests/api/internal/pages_spec.rb35
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)
}
}
]