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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-11 03:08:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-11 03:08:58 +0300
commit5427433c6d79f9131f4025cabb7e3208380bce9a (patch)
treeea0a22450f467f1ef1e3449255017dbe0f178882 /spec
parent13bcb8221306526671a61df589f7c05505c9934c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/concerns/lfs_request_spec.rb75
-rw-r--r--spec/factories/design_management/designs.rb2
-rw-r--r--spec/factories/wikis.rb4
-rw-r--r--spec/features/runners_spec.rb13
-rw-r--r--spec/fixtures/lib/gitlab/import_export/designs/project.json9
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js78
-rw-r--r--spec/frontend/search/dropdown_filter/components/dropdown_filter_spec.js198
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js99
-rw-r--r--spec/frontend/search/store/actions_spec.js42
-rw-r--r--spec/graphql/mutations/releases/create_spec.rb133
-rw-r--r--spec/graphql/resolvers/metadata_resolver_spec.rb2
-rw-r--r--spec/graphql/types/release_asset_link_input_type_spec.rb15
-rw-r--r--spec/graphql/types/release_assets_input_type_spec.rb15
-rw-r--r--spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb69
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb2
-rw-r--r--spec/lib/gitlab/experimentation/controller_concern_spec.rb438
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb414
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/concerns/atomic_internal_id_spec.rb16
-rw-r--r--spec/models/design_management/design_spec.rb8
-rw-r--r--spec/models/instance_metadata_spec.rb12
-rw-r--r--spec/models/internal_id_spec.rb23
-rw-r--r--spec/policies/instance_metadata_policy_spec.rb19
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb375
-rw-r--r--spec/requests/lfs_http_spec.rb428
-rw-r--r--spec/requests/lfs_locks_api_spec.rb61
-rw-r--r--spec/services/design_management/copy_design_collection/copy_service_spec.rb37
-rw-r--r--spec/support/helpers/lfs_http_helpers.rb12
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/requests/lfs_http_shared_examples.rb224
-rw-r--r--spec/views/search/_results.html.haml_spec.rb24
32 files changed, 1894 insertions, 976 deletions
diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb
deleted file mode 100644
index 3bafd761a3e..00000000000
--- a/spec/controllers/concerns/lfs_request_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe LfsRequest do
- include ProjectForksHelper
-
- controller(Repositories::GitHttpClientController) do
- # `described_class` is not available in this context
- include LfsRequest
-
- def show
- head :ok
- end
-
- def project
- @project ||= Project.find_by(id: params[:id])
- end
-
- def download_request?
- true
- end
-
- def upload_request?
- false
- end
-
- def ci?
- false
- end
- end
-
- let(:project) { create(:project, :public) }
-
- before do
- stub_lfs_setting(enabled: true)
- end
-
- context 'user is authenticated without access to lfs' do
- before do
- allow(controller).to receive(:authenticate_user)
- allow(controller).to receive(:authentication_result) do
- Gitlab::Auth::Result.new
- end
- end
-
- context 'with access to the project' do
- it 'returns 403' do
- get :show, params: { id: project.id }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'without access to the project' do
- context 'project does not exist' do
- it 'returns 404' do
- get :show, params: { id: 'does not exist' }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'project is private' do
- let(:project) { create(:project, :private) }
-
- it 'returns 404' do
- get :show, params: { id: project.id }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
- end
-end
diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb
index c58763791cc..c4fb330a0da 100644
--- a/spec/factories/design_management/designs.rb
+++ b/spec/factories/design_management/designs.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :design, class: 'DesignManagement::Design' do
+ factory :design, traits: [:has_internal_id], class: 'DesignManagement::Design' do
issue { association(:issue) }
project { issue&.project || association(:project) }
sequence(:filename) { |n| "homescreen-#{n}.jpg" }
diff --git a/spec/factories/wikis.rb b/spec/factories/wikis.rb
index 86d98bfd756..05f6fb0de58 100644
--- a/spec/factories/wikis.rb
+++ b/spec/factories/wikis.rb
@@ -17,5 +17,9 @@ FactoryBot.define do
container { project }
end
+
+ trait :empty_repo do
+ after(:create, &:create_wiki_repository)
+ end
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 6e18de3be7b..9697e10c3d1 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -122,6 +122,19 @@ RSpec.describe 'Runners' do
end
end
+ context 'when multiple runners are configured' do
+ let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let!(:specific_runner_2) { create(:ci_runner, :project, projects: [project]) }
+
+ it 'adds pagination to the runner list' do
+ stub_const('Projects::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE', 1)
+
+ visit project_runners_path(project)
+
+ expect(find('.pagination')).not_to be_nil
+ end
+ end
+
context 'when a specific runner exists in another project' do
let(:another_project) { create(:project) }
let!(:specific_runner) { create(:ci_runner, :project, projects: [another_project]) }
diff --git a/spec/fixtures/lib/gitlab/import_export/designs/project.json b/spec/fixtures/lib/gitlab/import_export/designs/project.json
index ebc08868d9e..e11b10a1d4c 100644
--- a/spec/fixtures/lib/gitlab/import_export/designs/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/designs/project.json
@@ -98,6 +98,7 @@
"designs":[
{
"id":38,
+ "iid": 1,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg",
@@ -107,6 +108,7 @@
},
{
"id":39,
+ "iid": 2,
"project_id":30,
"issue_id":469,
"filename":"jonathan_richman.jpg",
@@ -116,6 +118,7 @@
},
{
"id":40,
+ "iid": 3,
"project_id":30,
"issue_id":469,
"filename":"mariavontrap.jpeg",
@@ -137,6 +140,7 @@
"event":0,
"design":{
"id":38,
+ "iid": 1,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg"
@@ -156,6 +160,7 @@
"event":1,
"design":{
"id":38,
+ "iid": 1,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg"
@@ -167,6 +172,7 @@
"event":0,
"design":{
"id":39,
+ "iid": 2,
"project_id":30,
"issue_id":469,
"filename":"jonathan_richman.jpg"
@@ -186,6 +192,7 @@
"event":1,
"design":{
"id":38,
+ "iid": 1,
"project_id":30,
"issue_id":469,
"filename":"chirrido3.jpg"
@@ -197,6 +204,7 @@
"event":2,
"design":{
"id":39,
+ "iid": 2,
"project_id":30,
"issue_id":469,
"filename":"jonathan_richman.jpg"
@@ -208,6 +216,7 @@
"event":0,
"design":{
"id":40,
+ "iid": 3,
"project_id":30,
"issue_id":469,
"filename":"mariavontrap.jpeg"
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index bd21252eb5a..71e0ffd176f 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -2,6 +2,7 @@ import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import createDiffsStore from '~/diffs/store/modules';
+import createNotesStore from '~/notes/stores/modules';
import diffFileMockDataReadable from '../mock_data/diff_file';
import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
@@ -10,9 +11,13 @@ import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
import eventHub from '~/diffs/event_hub';
+import {
+ EVT_EXPAND_ALL_FILES,
+ EVT_PERF_MARK_DIFF_FILES_END,
+ EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
+} from '~/diffs/constants';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
-import { EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
const file = store.state.diffs.diffFiles[index];
@@ -58,12 +63,13 @@ function markFileToBeRendered(store, index = 0) {
});
}
-function createComponent({ file }) {
+function createComponent({ file, first = false, last = false }) {
const localVue = createLocalVue();
localVue.use(Vuex);
const store = new Vuex.Store({
+ ...createNotesStore(),
modules: {
diffs: createDiffsStore(),
},
@@ -78,6 +84,8 @@ function createComponent({ file }) {
file,
canCurrentUserFork: false,
viewDiffsFileByFile: false,
+ isFirstFile: first,
+ isLastFile: last,
},
});
@@ -117,6 +125,72 @@ describe('DiffFile', () => {
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('bus events', () => {
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ });
+
+ describe('during mount', () => {
+ it.each`
+ first | last | events | file
+ ${false} | ${false} | ${[]} | ${{ inlineLines: [], parallelLines: [], readableText: true }}
+ ${true} | ${true} | ${[]} | ${{ inlineLines: [], parallelLines: [], readableText: true }}
+ ${true} | ${false} | ${[EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN]} | ${false}
+ ${false} | ${true} | ${[EVT_PERF_MARK_DIFF_FILES_END]} | ${false}
+ ${true} | ${true} | ${[EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN, EVT_PERF_MARK_DIFF_FILES_END]} | ${false}
+ `(
+ 'emits the events $events based on the file and its position ({ first: $first, last: $last }) among all files',
+ async ({ file, first, last, events }) => {
+ if (file) {
+ forceHasDiff({ store, ...file });
+ }
+
+ ({ wrapper, store } = createComponent({
+ file: store.state.diffs.diffFiles[0],
+ first,
+ last,
+ }));
+
+ await wrapper.vm.$nextTick();
+
+ expect(eventHub.$emit).toHaveBeenCalledTimes(events.length);
+ events.forEach(event => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(event);
+ });
+ },
+ );
+ });
+
+ describe('after loading the diff', () => {
+ it('indicates that it loaded the file', async () => {
+ forceHasDiff({ store, inlineLines: [], parallelLines: [], readableText: true });
+ ({ wrapper, store } = createComponent({
+ file: store.state.diffs.diffFiles[0],
+ first: true,
+ last: true,
+ }));
+
+ jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue(getReadableFile());
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn());
+
+ makeFileAutomaticallyCollapsed(store);
+
+ await wrapper.vm.$nextTick(); // Wait for store updates to flow into the component
+
+ toggleFile(wrapper);
+
+ await wrapper.vm.$nextTick(); // Wait for the load to resolve
+ await wrapper.vm.$nextTick(); // Wait for the idleCallback
+ await wrapper.vm.$nextTick(); // Wait for nextTick inside postRender
+
+ expect(eventHub.$emit).toHaveBeenCalledTimes(2);
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN);
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_DIFF_FILES_END);
+ });
+ });
});
describe('template', () => {
diff --git a/spec/frontend/search/dropdown_filter/components/dropdown_filter_spec.js b/spec/frontend/search/dropdown_filter/components/dropdown_filter_spec.js
new file mode 100644
index 00000000000..f795a23404e
--- /dev/null
+++ b/spec/frontend/search/dropdown_filter/components/dropdown_filter_spec.js
@@ -0,0 +1,198 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { MOCK_QUERY } from 'jest/search/mock_data';
+import * as urlUtils from '~/lib/utils/url_utility';
+import initStore from '~/search/store';
+import DropdownFilter from '~/search/dropdown_filter/components/dropdown_filter.vue';
+import stateFilterData from '~/search/dropdown_filter/constants/state_filter_data';
+import confidentialFilterData from '~/search/dropdown_filter/constants/confidential_filter_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ setUrlParams: jest.fn(),
+}));
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('DropdownFilter', () => {
+ let wrapper;
+ let store;
+
+ const createStore = options => {
+ store = initStore({ query: MOCK_QUERY, ...options });
+ };
+
+ const createComponent = (props = { filterData: stateFilterData }) => {
+ wrapper = shallowMount(DropdownFilter, {
+ localVue,
+ store,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ store = null;
+ });
+
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
+ const findDropdownItemsText = () => findGlDropdownItems().wrappers.map(w => w.text());
+ const firstDropDownItem = () => findGlDropdownItems().at(0);
+
+ describe('StatusFilter', () => {
+ describe('template', () => {
+ describe.each`
+ scope | showDropdown
+ ${'issues'} | ${true}
+ ${'merge_requests'} | ${true}
+ ${'projects'} | ${false}
+ ${'milestones'} | ${false}
+ ${'users'} | ${false}
+ ${'notes'} | ${false}
+ ${'wiki_blobs'} | ${false}
+ ${'blobs'} | ${false}
+ `(`dropdown`, ({ scope, showDropdown }) => {
+ beforeEach(() => {
+ createStore({ query: { ...MOCK_QUERY, scope } });
+ createComponent();
+ });
+
+ it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
+ expect(findGlDropdown().exists()).toBe(showDropdown);
+ });
+ });
+
+ describe.each`
+ initialFilter | label
+ ${stateFilterData.filters.ANY.value} | ${`Any ${stateFilterData.header}`}
+ ${stateFilterData.filters.OPEN.value} | ${stateFilterData.filters.OPEN.label}
+ ${stateFilterData.filters.CLOSED.value} | ${stateFilterData.filters.CLOSED.label}
+ `(`filter text`, ({ initialFilter, label }) => {
+ describe(`when initialFilter is ${initialFilter}`, () => {
+ beforeEach(() => {
+ createStore({ query: { ...MOCK_QUERY, [stateFilterData.filterParam]: initialFilter } });
+ createComponent();
+ });
+
+ it(`sets dropdown label to ${label}`, () => {
+ expect(findGlDropdown().attributes('text')).toBe(label);
+ });
+ });
+ });
+ });
+
+ describe('Filter options', () => {
+ beforeEach(() => {
+ createStore();
+ createComponent();
+ });
+
+ it('renders a dropdown item for each filterOption', () => {
+ expect(findDropdownItemsText()).toStrictEqual(
+ stateFilterData.filterByScope[stateFilterData.scopes.ISSUES].map(v => {
+ return v.label;
+ }),
+ );
+ });
+
+ it('clicking a dropdown item calls setUrlParams', () => {
+ const filter = stateFilterData.filters[Object.keys(stateFilterData.filters)[0]].value;
+ firstDropDownItem().vm.$emit('click');
+
+ expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
+ page: null,
+ [stateFilterData.filterParam]: filter,
+ });
+ });
+
+ it('clicking a dropdown item calls visitUrl', () => {
+ firstDropDownItem().vm.$emit('click');
+
+ expect(urlUtils.visitUrl).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('ConfidentialFilter', () => {
+ describe('template', () => {
+ describe.each`
+ scope | showDropdown
+ ${'issues'} | ${true}
+ ${'merge_requests'} | ${false}
+ ${'projects'} | ${false}
+ ${'milestones'} | ${false}
+ ${'users'} | ${false}
+ ${'notes'} | ${false}
+ ${'wiki_blobs'} | ${false}
+ ${'blobs'} | ${false}
+ `(`dropdown`, ({ scope, showDropdown }) => {
+ beforeEach(() => {
+ createStore({ query: { ...MOCK_QUERY, scope } });
+ createComponent({ filterData: confidentialFilterData });
+ });
+
+ it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
+ expect(findGlDropdown().exists()).toBe(showDropdown);
+ });
+ });
+
+ describe.each`
+ initialFilter | label
+ ${confidentialFilterData.filters.ANY.value} | ${`Any ${confidentialFilterData.header}`}
+ ${confidentialFilterData.filters.CONFIDENTIAL.value} | ${confidentialFilterData.filters.CONFIDENTIAL.label}
+ ${confidentialFilterData.filters.NOT_CONFIDENTIAL.value} | ${confidentialFilterData.filters.NOT_CONFIDENTIAL.label}
+ `(`filter text`, ({ initialFilter, label }) => {
+ describe(`when initialFilter is ${initialFilter}`, () => {
+ beforeEach(() => {
+ createStore({
+ query: { ...MOCK_QUERY, [confidentialFilterData.filterParam]: initialFilter },
+ });
+ createComponent({ filterData: confidentialFilterData });
+ });
+
+ it(`sets dropdown label to ${label}`, () => {
+ expect(findGlDropdown().attributes('text')).toBe(label);
+ });
+ });
+ });
+ });
+ });
+
+ describe('Filter options', () => {
+ beforeEach(() => {
+ createStore();
+ createComponent({ filterData: confidentialFilterData });
+ });
+
+ it('renders a dropdown item for each filterOption', () => {
+ expect(findDropdownItemsText()).toStrictEqual(
+ confidentialFilterData.filterByScope[confidentialFilterData.scopes.ISSUES].map(v => {
+ return v.label;
+ }),
+ );
+ });
+
+ it('clicking a dropdown item calls setUrlParams', () => {
+ const filter =
+ confidentialFilterData.filters[Object.keys(confidentialFilterData.filters)[0]].value;
+ firstDropDownItem().vm.$emit('click');
+
+ expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
+ page: null,
+ [confidentialFilterData.filterParam]: filter,
+ });
+ });
+
+ it('clicking a dropdown item calls visitUrl', () => {
+ firstDropDownItem().vm.$emit('click');
+
+ expect(urlUtils.visitUrl).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
deleted file mode 100644
index c68be10f664..00000000000
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlButton, GlLink } from '@gitlab/ui';
-import { MOCK_QUERY } from 'jest/search/mock_data';
-import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
-import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
-import StatusFilter from '~/search/sidebar/components/status_filter.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('GlobalSearchSidebar', () => {
- let wrapper;
-
- const actionSpies = {
- applyQuery: jest.fn(),
- resetQuery: jest.fn(),
- };
-
- const createComponent = initialState => {
- const store = new Vuex.Store({
- state: {
- query: MOCK_QUERY,
- ...initialState,
- },
- actions: actionSpies,
- });
-
- wrapper = shallowMount(GlobalSearchSidebar, {
- localVue,
- store,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findSidebarForm = () => wrapper.find('form');
- const findStatusFilter = () => wrapper.find(StatusFilter);
- const findConfidentialityFilter = () => wrapper.find(ConfidentialityFilter);
- const findApplyButton = () => wrapper.find(GlButton);
- const findResetLinkButton = () => wrapper.find(GlLink);
-
- describe('template', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders StatusFilter always', () => {
- expect(findStatusFilter().exists()).toBe(true);
- });
-
- it('renders ConfidentialityFilter always', () => {
- expect(findConfidentialityFilter().exists()).toBe(true);
- });
-
- it('renders ApplyButton always', () => {
- expect(findApplyButton().exists()).toBe(true);
- });
-
- describe('ResetLinkButton', () => {
- describe('with no filter selected', () => {
- beforeEach(() => {
- createComponent({ query: {} });
- });
-
- it('does not render', () => {
- expect(findResetLinkButton().exists()).toBe(false);
- });
- });
-
- describe('with filter selected', () => {
- it('does render when a filter selected', () => {
- expect(findResetLinkButton().exists()).toBe(true);
- });
- });
- });
- });
-
- describe('actions', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('clicking ApplyButton calls applyQuery', () => {
- findSidebarForm().trigger('submit');
-
- expect(actionSpies.applyQuery).toHaveBeenCalled();
- });
-
- it('clicking ResetLinkButton calls resetQuery', () => {
- findResetLinkButton().vm.$emit('click');
-
- expect(actionSpies.resetQuery).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 35d97c7dcb1..0bab4ce17a6 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/search/store/actions';
import * as types from '~/search/store/mutation_types';
-import * as urlUtils from '~/lib/utils/url_utility';
import state from '~/search/store/state';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
@@ -43,47 +42,6 @@ describe('Global Search Store Actions', () => {
});
});
});
-
- describe('setQuery', () => {
- const payload = { key: 'key1', value: 'value1' };
-
- it('calls the SET_QUERY mutation', done => {
- testAction(actions.setQuery, payload, state, [{ type: types.SET_QUERY, payload }], [], done);
- });
- });
-
- describe('applyQuery', () => {
- beforeEach(() => {
- urlUtils.setUrlParams = jest.fn();
- urlUtils.visitUrl = jest.fn();
- });
-
- it('calls visitUrl and setParams with the state.query', () => {
- testAction(actions.applyQuery, null, state, [], [], () => {
- expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
- expect(urlUtils.visitUrl).toHaveBeenCalled();
- });
- });
- });
-
- describe('resetQuery', () => {
- beforeEach(() => {
- urlUtils.setUrlParams = jest.fn();
- urlUtils.visitUrl = jest.fn();
- });
-
- it('calls visitUrl and setParams with empty values', () => {
- testAction(actions.resetQuery, null, state, [], [], () => {
- expect(urlUtils.setUrlParams).toHaveBeenCalledWith({
- ...state.query,
- page: null,
- state: null,
- confidential: null,
- });
- expect(urlUtils.visitUrl).toHaveBeenCalled();
- });
- });
- });
});
describe('setQuery', () => {
diff --git a/spec/graphql/mutations/releases/create_spec.rb b/spec/graphql/mutations/releases/create_spec.rb
new file mode 100644
index 00000000000..d6305691dac
--- /dev/null
+++ b/spec/graphql/mutations/releases/create_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Releases::Create do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
+ let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+
+ let(:tag) { 'v1.1.0'}
+ let(:ref) { 'master'}
+ let(:name) { 'Version 1.0'}
+ let(:description) { 'The first release :rocket:' }
+ let(:released_at) { Time.parse('2018-12-10') }
+ let(:milestones) { [milestone_12_3.title, milestone_12_4.title] }
+ let(:assets) do
+ {
+ links: [
+ {
+ name: 'An asset link',
+ url: 'https://gitlab.example.com/link',
+ filepath: '/permanent/link',
+ link_type: 'other'
+ }
+ ]
+ }
+ end
+
+ let(:mutation_arguments) do
+ {
+ project_path: project.full_path,
+ tag: tag,
+ ref: ref,
+ name: name,
+ description: description,
+ released_at: released_at,
+ milestones: milestones,
+ assets: assets
+ }
+ end
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ subject(:resolve) do
+ mutation.resolve(**mutation_arguments)
+ end
+
+ let(:new_release) { subject[:release] }
+
+ context 'when the current user has access to create releases' do
+ let(:current_user) { developer }
+
+ it 'returns no errors' do
+ expect(resolve).to include(errors: [])
+ end
+
+ it 'creates the release with the correct tag' do
+ expect(new_release.tag).to eq(tag)
+ end
+
+ it 'creates the release with the correct name' do
+ expect(new_release.name).to eq(name)
+ end
+
+ it 'creates the release with the correct description' do
+ expect(new_release.description).to eq(description)
+ end
+
+ it 'creates the release with the correct released_at' do
+ expect(new_release.released_at).to eq(released_at)
+ end
+
+ it 'creates the release with the correct created_at' do
+ expect(new_release.created_at).to eq(Time.current)
+ end
+
+ it 'creates the release with the correct milestone associations' do
+ expected_milestone_titles = [milestone_12_3.title, milestone_12_4.title]
+ actual_milestone_titles = new_release.milestones.map { |m| m.title }
+
+ # Right now the milestones are returned in a non-deterministic order.
+ # `match_array` should be updated to `eq` once
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
+ expect(actual_milestone_titles).to match_array(expected_milestone_titles)
+ end
+
+ describe 'asset links' do
+ let(:expected_link) { assets[:links].first }
+ let(:new_link) { new_release.links.first }
+
+ it 'creates a single asset link' do
+ expect(new_release.links.size).to eq(1)
+ end
+
+ it 'creates the link with the correct name' do
+ expect(new_link.name).to eq(expected_link[:name])
+ end
+
+ it 'creates the link with the correct url' do
+ expect(new_link.url).to eq(expected_link[:url])
+ end
+
+ it 'creates the link with the correct link type' do
+ expect(new_link.link_type).to eq(expected_link[:link_type])
+ end
+
+ it 'creates the link with the correct direct filepath' do
+ expect(new_link.filepath).to eq(expected_link[:filepath])
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to create releases" do
+ let(:current_user) { reporter }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/metadata_resolver_spec.rb b/spec/graphql/resolvers/metadata_resolver_spec.rb
index 20556941de4..f8c01f9d531 100644
--- a/spec/graphql/resolvers/metadata_resolver_spec.rb
+++ b/spec/graphql/resolvers/metadata_resolver_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Resolvers::MetadataResolver do
describe '#resolve' do
it 'returns version and revision' do
- expect(resolve(described_class)).to eq(version: Gitlab::VERSION, revision: Gitlab.revision)
+ expect(resolve(described_class)).to have_attributes(version: Gitlab::VERSION, revision: Gitlab.revision)
end
end
end
diff --git a/spec/graphql/types/release_asset_link_input_type_spec.rb b/spec/graphql/types/release_asset_link_input_type_spec.rb
new file mode 100644
index 00000000000..d97a91b609a
--- /dev/null
+++ b/spec/graphql/types/release_asset_link_input_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::ReleaseAssetLinkInputType do
+ specify { expect(described_class.graphql_name).to eq('ReleaseAssetLinkInput') }
+
+ it 'has the correct arguments' do
+ expect(described_class.arguments.keys).to match_array(%w[name url directAssetPath linkType])
+ end
+
+ it 'sets the type of link_type argument to ReleaseAssetLinkTypeEnum' do
+ expect(described_class.arguments['linkType'].type).to eq(Types::ReleaseAssetLinkTypeEnum)
+ end
+end
diff --git a/spec/graphql/types/release_assets_input_type_spec.rb b/spec/graphql/types/release_assets_input_type_spec.rb
new file mode 100644
index 00000000000..c44abe1e171
--- /dev/null
+++ b/spec/graphql/types/release_assets_input_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::ReleaseAssetsInputType do
+ specify { expect(described_class.graphql_name).to eq('ReleaseAssetsInput') }
+
+ it 'has the correct arguments' do
+ expect(described_class.arguments.keys).to match_array(%w[links])
+ end
+
+ it 'sets the type of links argument to ReleaseAssetLinkInputType' do
+ expect(described_class.arguments['links'].type.of_type.of_type).to eq(Types::ReleaseAssetLinkInputType)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb b/spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb
new file mode 100644
index 00000000000..4bf59a02a31
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_design_internal_ids_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillDesignInternalIds, :migration, schema: 20201030203854 do
+ subject { described_class.new(designs) }
+
+ let_it_be(:namespaces) { table(:namespaces) }
+ let_it_be(:projects) { table(:projects) }
+ let_it_be(:designs) { table(:design_management_designs) }
+
+ let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
+ let(:project_2) { projects.create!(namespace_id: namespace.id) }
+
+ def create_design!(proj = project)
+ designs.create!(project_id: proj.id, filename: generate(:filename))
+ end
+
+ def migrate!
+ relation = designs.where(project_id: [project.id, project_2.id]).select(:project_id).distinct
+
+ subject.perform(relation)
+ end
+
+ it 'backfills the iid for designs' do
+ 3.times { create_design! }
+
+ expect do
+ migrate!
+ end.to change { designs.pluck(:iid) }.from(contain_exactly(nil, nil, nil)).to(contain_exactly(1, 2, 3))
+ end
+
+ it 'scopes IIDs and handles range and starting-point correctly' do
+ create_design!.update!(iid: 10)
+ create_design!.update!(iid: 12)
+ create_design!(project_2).update!(iid: 7)
+ project_3 = projects.create!(namespace_id: namespace.id)
+
+ 2.times { create_design! }
+ 2.times { create_design!(project_2) }
+ 2.times { create_design!(project_3) }
+
+ migrate!
+
+ expect(designs.where(project_id: project.id).pluck(:iid)).to contain_exactly(10, 12, 13, 14)
+ expect(designs.where(project_id: project_2.id).pluck(:iid)).to contain_exactly(7, 8, 9)
+ expect(designs.where(project_id: project_3.id).pluck(:iid)).to contain_exactly(nil, nil)
+ end
+
+ it 'updates the internal ID records' do
+ design = create_design!
+ 2.times { create_design! }
+ design.update!(iid: 10)
+ scope = { project_id: project.id }
+ usage = :design_management_designs
+ init = ->(_d, _s) { 0 }
+
+ ::InternalId.track_greatest(design, scope, usage, 10, init)
+
+ migrate!
+
+ next_iid = ::InternalId.generate_next(design, scope, usage, init)
+
+ expect(designs.pluck(:iid)).to contain_exactly(10, 11, 12)
+ expect(design.reload.iid).to eq(10)
+ expect(next_iid).to eq(13)
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index a8edcc5f7e5..ff6e5437559 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1680,7 +1680,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
has_internal_id :iid,
scope: :project,
- init: ->(s) { s&.project&.issues&.maximum(:iid) },
+ init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) },
backfill: true,
presence: false
end
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 86b3c029944..359e0597f4e 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Database::Reindexing do
it 'retrieves regular indexes that are no left-overs from previous runs' do
result = double
- expect(Gitlab::Database::PostgresIndex).to receive_message_chain('regular.not_match.not_match').with(no_args).with('^tmp_reindex_').with('^old_reindex_').and_return(result)
+ expect(Gitlab::Database::PostgresIndex).to receive_message_chain('regular.where.not_match.not_match').with(no_args).with('NOT expression').with('^tmp_reindex_').with('^old_reindex_').and_return(result)
expect(subject).to eq(result)
end
diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
new file mode 100644
index 00000000000..2fe3d36daf7
--- /dev/null
+++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
@@ -0,0 +1,438 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
+ before do
+ stub_const('Gitlab::Experimentation::EXPERIMENTS', {
+ backwards_compatible_test_experiment: {
+ environment: environment,
+ tracking_category: 'Team',
+ use_backwards_compatible_subject_index: true
+ },
+ test_experiment: {
+ environment: environment,
+ tracking_category: 'Team'
+ }
+ }
+ )
+
+ Feature.enable_percentage_of_time(:backwards_compatible_test_experiment_experiment_percentage, enabled_percentage)
+ Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage)
+ end
+
+ let(:environment) { Rails.env.test? }
+ let(:enabled_percentage) { 10 }
+
+ controller(ApplicationController) do
+ include Gitlab::Experimentation::ControllerConcern
+
+ def index
+ head :ok
+ end
+ end
+
+ describe '#set_experimentation_subject_id_cookie' do
+ let(:do_not_track) { nil }
+ let(:cookie) { cookies.permanent.signed[:experimentation_subject_id] }
+
+ before do
+ request.headers['DNT'] = do_not_track if do_not_track.present?
+
+ get :index
+ end
+
+ context 'cookie is present' do
+ before do
+ cookies[:experimentation_subject_id] = 'test'
+ end
+
+ it 'does not change the cookie' do
+ expect(cookies[:experimentation_subject_id]).to eq 'test'
+ end
+ end
+
+ context 'cookie is not present' do
+ it 'sets a permanent signed cookie' do
+ expect(cookie).to be_present
+ end
+
+ context 'DNT: 0' do
+ let(:do_not_track) { '0' }
+
+ it 'sets a permanent signed cookie' do
+ expect(cookie).to be_present
+ end
+ end
+
+ context 'DNT: 1' do
+ let(:do_not_track) { '1' }
+
+ it 'does nothing' do
+ expect(cookie).not_to be_present
+ end
+ end
+ end
+ end
+
+ describe '#push_frontend_experiment' do
+ it 'pushes an experiment to the frontend' do
+ gon = instance_double('gon')
+ experiments = { experiments: { 'myExperiment' => true } }
+
+ stub_experiment_for_user(my_experiment: true)
+ allow(controller).to receive(:gon).and_return(gon)
+
+ expect(gon).to receive(:push).with(experiments, true)
+
+ controller.push_frontend_experiment(:my_experiment)
+ end
+ end
+
+ describe '#experiment_enabled?' do
+ def check_experiment(exp_key = :test_experiment)
+ controller.experiment_enabled?(exp_key)
+ end
+
+ subject { check_experiment }
+
+ context 'cookie is not present' do
+ it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and an experimentation_subject_index of nil' do
+ expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(:test_experiment, nil)
+ check_experiment
+ end
+ end
+
+ context 'cookie is present' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ cookies.permanent.signed[:experimentation_subject_id] = 'abcd-1234'
+ get :index
+ end
+
+ where(:experiment_key, :index_value) do
+ :test_experiment | 40 # Zlib.crc32('test_experimentabcd-1234') % 100 = 40
+ :backwards_compatible_test_experiment | 76 # 'abcd1234'.hex % 100 = 76
+ end
+
+ with_them do
+ it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and the calculated experimentation_subject_index based on the uuid' do
+ expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(experiment_key, index_value)
+ check_experiment(experiment_key)
+ end
+ end
+ end
+
+ it 'returns true when DNT: 0 is set in the request' do
+ allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
+ controller.request.headers['DNT'] = '0'
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when DNT: 1 is set in the request' do
+ allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
+ controller.request.headers['DNT'] = '1'
+
+ is_expected.to be_falsy
+ end
+
+ describe 'URL parameter to force enable experiment' do
+ it 'returns true unconditionally' do
+ get :index, params: { force_experiment: :test_experiment }
+
+ is_expected.to be_truthy
+ end
+ end
+ end
+
+ describe '#track_experiment_event', :snowplow do
+ context 'when the experiment is enabled' do
+ before do
+ stub_experiment(test_experiment: true)
+ end
+
+ context 'the user is part of the experimental group' do
+ before do
+ stub_experiment_for_user(test_experiment: true)
+ end
+
+ it 'tracks the event with the right parameters' do
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
+ property: 'experimental_group',
+ value: 1
+ )
+ end
+ end
+
+ context 'the user is part of the control group' do
+ before do
+ stub_experiment_for_user(test_experiment: false)
+ end
+
+ it 'tracks the event with the right parameters' do
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
+ property: 'control_group',
+ value: 1
+ )
+ end
+ end
+
+ context 'do not track is disabled' do
+ before do
+ request.headers['DNT'] = '0'
+ end
+
+ it 'does track the event' do
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_snowplow_event(
+ category: 'Team',
+ action: 'start',
+ property: 'control_group',
+ value: 1
+ )
+ end
+ end
+
+ context 'do not track enabled' do
+ before do
+ request.headers['DNT'] = '1'
+ end
+
+ it 'does not track the event' do
+ controller.track_experiment_event(:test_experiment, 'start', 1)
+
+ expect_no_snowplow_event
+ end
+ end
+ end
+
+ context 'when the experiment is disabled' do
+ before do
+ stub_experiment(test_experiment: false)
+ end
+
+ it 'does not track the event' do
+ controller.track_experiment_event(:test_experiment, 'start')
+
+ expect_no_snowplow_event
+ end
+ end
+ end
+
+ describe '#frontend_experimentation_tracking_data' do
+ context 'when the experiment is enabled' do
+ before do
+ stub_experiment(test_experiment: true)
+ end
+
+ context 'the user is part of the experimental group' do
+ before do
+ stub_experiment_for_user(test_experiment: true)
+ end
+
+ it 'pushes the right parameters to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Team',
+ action: 'start',
+ property: 'experimental_group',
+ value: 'team_id'
+ }
+ )
+ end
+ end
+
+ context 'the user is part of the control group' do
+ before do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
+ end
+ end
+
+ it 'pushes the right parameters to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Team',
+ action: 'start',
+ property: 'control_group',
+ value: 'team_id'
+ }
+ )
+ end
+
+ it 'does not send nil value to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Team',
+ action: 'start',
+ property: 'control_group'
+ }
+ )
+ end
+ end
+
+ context 'do not track disabled' do
+ before do
+ request.headers['DNT'] = '0'
+ end
+
+ it 'pushes the right parameters to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Team',
+ action: 'start',
+ property: 'control_group'
+ }
+ )
+ end
+ end
+
+ context 'do not track enabled' do
+ before do
+ request.headers['DNT'] = '1'
+ end
+
+ it 'does not push data to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+
+ expect(Gon.method_defined?(:tracking_data)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the experiment is disabled' do
+ before do
+ stub_experiment(test_experiment: false)
+ end
+
+ it 'does not push data to gon' do
+ expect(Gon.method_defined?(:tracking_data)).to be_falsey
+ controller.track_experiment_event(:test_experiment, 'start')
+ end
+ end
+ end
+
+ describe '#record_experiment_user' do
+ let(:user) { build(:user) }
+
+ context 'when the experiment is enabled' do
+ before do
+ stub_experiment(test_experiment: true)
+ allow(controller).to receive(:current_user).and_return(user)
+ end
+
+ context 'the user is part of the experimental group' do
+ before do
+ stub_experiment_for_user(test_experiment: true)
+ end
+
+ it 'calls add_user on the Experiment model' do
+ expect(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+
+ context 'the user is part of the control group' do
+ before do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
+ end
+ end
+
+ it 'calls add_user on the Experiment model' do
+ expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+ end
+
+ context 'when the experiment is disabled' do
+ before do
+ stub_experiment(test_experiment: false)
+ allow(controller).to receive(:current_user).and_return(user)
+ end
+
+ it 'does not call add_user on the Experiment model' do
+ expect(::Experiment).not_to receive(:add_user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+
+ context 'when there is no current_user' do
+ before do
+ stub_experiment(test_experiment: true)
+ end
+
+ it 'does not call add_user on the Experiment model' do
+ expect(::Experiment).not_to receive(:add_user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+
+ context 'do not track' do
+ before do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
+ end
+ end
+
+ context 'is disabled' do
+ before do
+ request.headers['DNT'] = '0'
+ end
+
+ it 'calls add_user on the Experiment model' do
+ expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+
+ context 'is enabled' do
+ before do
+ request.headers['DNT'] = '1'
+ end
+
+ it 'does not call add_user on the Experiment model' do
+ expect(::Experiment).not_to receive(:add_user)
+
+ controller.record_experiment_user(:test_experiment)
+ end
+ end
+ end
+ end
+
+ describe '#experiment_tracking_category_and_group' do
+ let_it_be(:experiment_key) { :test_something }
+
+ subject { controller.experiment_tracking_category_and_group(experiment_key) }
+
+ it 'returns a string with the experiment tracking category & group joined with a ":"' do
+ expect(controller).to receive(:tracking_category).with(experiment_key).and_return('Experiment::Category')
+ expect(controller).to receive(:tracking_group).with(experiment_key, '_group').and_return('experimental_group')
+
+ expect(subject).to eq('Experiment::Category:experimental_group')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index 238392e2067..ebf98a0151f 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -50,420 +50,6 @@ RSpec.describe Gitlab::Experimentation, :snowplow do
let(:environment) { Rails.env.test? }
let(:enabled_percentage) { 10 }
- describe Gitlab::Experimentation::ControllerConcern, type: :controller do
- controller(ApplicationController) do
- include Gitlab::Experimentation::ControllerConcern
-
- def index
- head :ok
- end
- end
-
- describe '#set_experimentation_subject_id_cookie' do
- let(:do_not_track) { nil }
- let(:cookie) { cookies.permanent.signed[:experimentation_subject_id] }
-
- before do
- request.headers['DNT'] = do_not_track if do_not_track.present?
-
- get :index
- end
-
- context 'cookie is present' do
- before do
- cookies[:experimentation_subject_id] = 'test'
- end
-
- it 'does not change the cookie' do
- expect(cookies[:experimentation_subject_id]).to eq 'test'
- end
- end
-
- context 'cookie is not present' do
- it 'sets a permanent signed cookie' do
- expect(cookie).to be_present
- end
-
- context 'DNT: 0' do
- let(:do_not_Track) { '0' }
-
- it 'sets a permanent signed cookie' do
- expect(cookie).to be_present
- end
- end
-
- context 'DNT: 1' do
- let(:do_not_track) { '1' }
-
- it 'does nothing' do
- expect(cookie).not_to be_present
- end
- end
- end
- end
-
- describe '#push_frontend_experiment' do
- it 'pushes an experiment to the frontend' do
- gon = instance_double('gon')
- experiments = { experiments: { 'myExperiment' => true } }
-
- stub_experiment_for_user(my_experiment: true)
- allow(controller).to receive(:gon).and_return(gon)
-
- expect(gon).to receive(:push).with(experiments, true)
-
- controller.push_frontend_experiment(:my_experiment)
- end
- end
-
- describe '#experiment_enabled?' do
- def check_experiment(exp_key = :test_experiment)
- controller.experiment_enabled?(exp_key)
- end
-
- subject { check_experiment }
-
- context 'cookie is not present' do
- it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and an experimentation_subject_index of nil' do
- expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(:test_experiment, nil)
- check_experiment
- end
- end
-
- context 'cookie is present' do
- using RSpec::Parameterized::TableSyntax
-
- before do
- cookies.permanent.signed[:experimentation_subject_id] = 'abcd-1234'
- get :index
- end
-
- where(:experiment_key, :index_value) do
- :test_experiment | 40 # Zlib.crc32('test_experimentabcd-1234') % 100 = 40
- :backwards_compatible_test_experiment | 76 # 'abcd1234'.hex % 100 = 76
- end
-
- with_them do
- it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and the calculated experimentation_subject_index based on the uuid' do
- expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(experiment_key, index_value)
- check_experiment(experiment_key)
- end
- end
- end
-
- it 'returns true when DNT: 0 is set in the request' do
- allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
- controller.request.headers['DNT'] = '0'
-
- is_expected.to be_truthy
- end
-
- it 'returns false when DNT: 1 is set in the request' do
- allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true }
- controller.request.headers['DNT'] = '1'
-
- is_expected.to be_falsy
- end
-
- describe 'URL parameter to force enable experiment' do
- it 'returns true unconditionally' do
- get :index, params: { force_experiment: :test_experiment }
-
- is_expected.to be_truthy
- end
- end
- end
-
- describe '#track_experiment_event' do
- context 'when the experiment is enabled' do
- before do
- stub_experiment(test_experiment: true)
- end
-
- context 'the user is part of the experimental group' do
- before do
- stub_experiment_for_user(test_experiment: true)
- end
-
- it 'tracks the event with the right parameters' do
- controller.track_experiment_event(:test_experiment, 'start', 1)
-
- expect_snowplow_event(
- category: 'Team',
- action: 'start',
- property: 'experimental_group',
- value: 1
- )
- end
- end
-
- context 'the user is part of the control group' do
- before do
- stub_experiment_for_user(test_experiment: false)
- end
-
- it 'tracks the event with the right parameters' do
- controller.track_experiment_event(:test_experiment, 'start', 1)
-
- expect_snowplow_event(
- category: 'Team',
- action: 'start',
- property: 'control_group',
- value: 1
- )
- end
- end
-
- context 'do not track is disabled' do
- before do
- request.headers['DNT'] = '0'
- end
-
- it 'does track the event' do
- controller.track_experiment_event(:test_experiment, 'start', 1)
-
- expect_snowplow_event(
- category: 'Team',
- action: 'start',
- property: 'control_group',
- value: 1
- )
- end
- end
-
- context 'do not track enabled' do
- before do
- request.headers['DNT'] = '1'
- end
-
- it 'does not track the event' do
- controller.track_experiment_event(:test_experiment, 'start', 1)
-
- expect_no_snowplow_event
- end
- end
- end
-
- context 'when the experiment is disabled' do
- before do
- stub_experiment(test_experiment: false)
- end
-
- it 'does not track the event' do
- controller.track_experiment_event(:test_experiment, 'start')
-
- expect_no_snowplow_event
- end
- end
- end
-
- describe '#frontend_experimentation_tracking_data' do
- context 'when the experiment is enabled' do
- before do
- stub_experiment(test_experiment: true)
- end
-
- context 'the user is part of the experimental group' do
- before do
- stub_experiment_for_user(test_experiment: true)
- end
-
- it 'pushes the right parameters to gon' do
- controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
- expect(Gon.tracking_data).to eq(
- {
- category: 'Team',
- action: 'start',
- property: 'experimental_group',
- value: 'team_id'
- }
- )
- end
- end
-
- context 'the user is part of the control group' do
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
- end
- end
-
- it 'pushes the right parameters to gon' do
- controller.frontend_experimentation_tracking_data(:test_experiment, 'start', 'team_id')
- expect(Gon.tracking_data).to eq(
- {
- category: 'Team',
- action: 'start',
- property: 'control_group',
- value: 'team_id'
- }
- )
- end
-
- it 'does not send nil value to gon' do
- controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
- expect(Gon.tracking_data).to eq(
- {
- category: 'Team',
- action: 'start',
- property: 'control_group'
- }
- )
- end
- end
-
- context 'do not track disabled' do
- before do
- request.headers['DNT'] = '0'
- end
-
- it 'pushes the right parameters to gon' do
- controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
-
- expect(Gon.tracking_data).to eq(
- {
- category: 'Team',
- action: 'start',
- property: 'control_group'
- }
- )
- end
- end
-
- context 'do not track enabled' do
- before do
- request.headers['DNT'] = '1'
- end
-
- it 'does not push data to gon' do
- controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
-
- expect(Gon.method_defined?(:tracking_data)).to be_falsey
- end
- end
- end
-
- context 'when the experiment is disabled' do
- before do
- stub_experiment(test_experiment: false)
- end
-
- it 'does not push data to gon' do
- expect(Gon.method_defined?(:tracking_data)).to be_falsey
- controller.track_experiment_event(:test_experiment, 'start')
- end
- end
- end
-
- describe '#record_experiment_user' do
- let(:user) { build(:user) }
-
- context 'when the experiment is enabled' do
- before do
- stub_experiment(test_experiment: true)
- allow(controller).to receive(:current_user).and_return(user)
- end
-
- context 'the user is part of the experimental group' do
- before do
- stub_experiment_for_user(test_experiment: true)
- end
-
- it 'calls add_user on the Experiment model' do
- expect(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user)
-
- controller.record_experiment_user(:test_experiment)
- end
- end
-
- context 'the user is part of the control group' do
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
- end
- end
-
- it 'calls add_user on the Experiment model' do
- expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user)
-
- controller.record_experiment_user(:test_experiment)
- end
- end
- end
-
- context 'when the experiment is disabled' do
- before do
- stub_experiment(test_experiment: false)
- allow(controller).to receive(:current_user).and_return(user)
- end
-
- it 'does not call add_user on the Experiment model' do
- expect(::Experiment).not_to receive(:add_user)
-
- controller.record_experiment_user(:test_experiment)
- end
- end
-
- context 'when there is no current_user' do
- before do
- stub_experiment(test_experiment: true)
- end
-
- it 'does not call add_user on the Experiment model' do
- expect(::Experiment).not_to receive(:add_user)
-
- controller.record_experiment_user(:test_experiment)
- end
- end
-
- context 'do not track' do
- before do
- allow(controller).to receive(:current_user).and_return(user)
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
- end
- end
-
- context 'is disabled' do
- before do
- request.headers['DNT'] = '0'
- end
-
- it 'calls add_user on the Experiment model' do
- expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user)
-
- controller.record_experiment_user(:test_experiment)
- end
- end
-
- context 'is enabled' do
- before do
- request.headers['DNT'] = '1'
- end
-
- it 'does not call add_user on the Experiment model' do
- expect(::Experiment).not_to receive(:add_user)
-
- controller.record_experiment_user(:test_experiment)
- end
- end
- end
- end
-
- describe '#experiment_tracking_category_and_group' do
- let_it_be(:experiment_key) { :test_something }
-
- subject { controller.experiment_tracking_category_and_group(experiment_key) }
-
- it 'returns a string with the experiment tracking category & group joined with a ":"' do
- expect(controller).to receive(:tracking_category).with(experiment_key).and_return('Experiment::Category')
- expect(controller).to receive(:tracking_group).with(experiment_key, '_group').and_return('experimental_group')
-
- expect(subject).to eq('Experiment::Category:experimental_group')
- end
- end
- end
-
describe '.enabled?' do
subject { described_class.enabled?(:test_experiment) }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 8b254e82a92..da5e1f14ee0 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -774,6 +774,7 @@ ExternalPullRequest:
- target_sha
DesignManagement::Design:
- id
+- iid
- project_id
- filename
- relative_position
diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb
index 8c3537f1dcc..5ee3c012dc9 100644
--- a/spec/models/concerns/atomic_internal_id_spec.rb
+++ b/spec/models/concerns/atomic_internal_id_spec.rb
@@ -86,4 +86,20 @@ RSpec.describe AtomicInternalId do
expect { subject }.to change { milestone.iid }.from(nil).to(iid.to_i)
end
end
+
+ describe '.with_project_iid_supply' do
+ let(:iid) { 100 }
+
+ it 'wraps generate and track_greatest in a concurrency-safe lock' do
+ expect_next_instance_of(InternalId::InternalIdGenerator) do |g|
+ expect(g).to receive(:with_lock).and_call_original
+ expect(g.record).to receive(:last_value).and_return(iid)
+ expect(g).to receive(:track_greatest).with(iid + 4)
+ end
+
+ ::Milestone.with_project_iid_supply(milestone.project) do |supply|
+ 4.times { supply.next_value }
+ end
+ end
+ end
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 833f32abfcc..946541a0602 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -11,6 +11,14 @@ RSpec.describe DesignManagement::Design do
let_it_be(:design3) { create(:design, :with_versions, issue: issue, versions_count: 1) }
let_it_be(:deleted_design) { create(:design, :with_versions, deleted: true) }
+ it_behaves_like 'AtomicInternalId', validate_presence: true do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:design, issue: issue) }
+ let(:scope) { :project }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :design_management_designs }
+ end
+
it_behaves_like 'a class that supports relative positioning' do
let_it_be(:relative_parent) { create(:issue) }
diff --git a/spec/models/instance_metadata_spec.rb b/spec/models/instance_metadata_spec.rb
new file mode 100644
index 00000000000..1835dc8a9af
--- /dev/null
+++ b/spec/models/instance_metadata_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe InstanceMetadata do
+ it 'has the correct properties' do
+ expect(subject).to have_attributes(
+ version: Gitlab::VERSION,
+ revision: Gitlab.revision
+ )
+ end
+end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 751e8724872..07f62b9de55 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe InternalId do
let(:project) { create(:project) }
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
+ let(:id_subject) { issue }
let(:scope) { { project: project } }
- let(:init) { ->(s) { s.project.issues.size } }
+ let(:init) { ->(issue, scope) { issue&.project&.issues&.size || Issue.where(**scope).count } }
it_behaves_like 'having unique enum values'
@@ -39,7 +40,7 @@ RSpec.describe InternalId do
end
describe '.generate_next' do
- subject { described_class.generate_next(issue, scope, usage, init) }
+ subject { described_class.generate_next(id_subject, scope, usage, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
@@ -88,6 +89,14 @@ RSpec.describe InternalId do
expect(normalized).to eq((0..seq.size - 1).to_a)
end
+
+ context 'there are no instances to pass in' do
+ let(:id_subject) { Issue }
+
+ it 'accepts classes instead' do
+ expect(subject).to eq(1)
+ end
+ end
end
describe '.reset' do
@@ -130,7 +139,7 @@ RSpec.describe InternalId do
describe '.track_greatest' do
let(:value) { 9001 }
- subject { described_class.track_greatest(issue, scope, usage, value, init) }
+ subject { described_class.track_greatest(id_subject, scope, usage, value, init) }
context 'in the absence of a record' do
it 'creates a record if not yet present' do
@@ -166,6 +175,14 @@ RSpec.describe InternalId do
expect(subject).to eq 10_001
end
end
+
+ context 'there are no instances to pass in' do
+ let(:id_subject) { Issue }
+
+ it 'accepts classes instead' do
+ expect(subject).to eq(value)
+ end
+ end
end
describe '#increment_and_save!' do
diff --git a/spec/policies/instance_metadata_policy_spec.rb b/spec/policies/instance_metadata_policy_spec.rb
new file mode 100644
index 00000000000..2c8e18483e6
--- /dev/null
+++ b/spec/policies/instance_metadata_policy_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe InstanceMetadataPolicy do
+ subject { described_class.new(user, InstanceMetadata.new) }
+
+ context 'for any logged-in user' do
+ let(:user) { create(:user) }
+
+ specify { expect_allowed(:read_instance_metadata) }
+ end
+
+ context 'for anonymous users' do
+ let(:user) { nil }
+
+ specify { expect_disallowed(:read_instance_metadata) }
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
new file mode 100644
index 00000000000..2402cf62a49
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -0,0 +1,375 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creation of a new release' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
+ let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
+ let_it_be(:public_user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ let(:mutation_name) { :release_create }
+
+ let(:tag_name) { 'v7.12.5'}
+ let(:ref) { 'master'}
+ let(:name) { 'Version 7.12.5'}
+ let(:description) { 'Release 7.12.5 :rocket:' }
+ let(:released_at) { '2018-12-10' }
+ let(:milestones) { [milestone_12_3.title, milestone_12_4.title] }
+ let(:asset_link) { { name: 'An asset link', url: 'https://gitlab.example.com/link', directAssetPath: '/permanent/link', linkType: 'OTHER' } }
+ let(:assets) { { links: [asset_link] } }
+
+ let(:mutation_arguments) do
+ {
+ projectPath: project.full_path,
+ tagName: tag_name,
+ ref: ref,
+ name: name,
+ description: description,
+ releasedAt: released_at,
+ milestones: milestones,
+ assets: assets
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ release {
+ tagName
+ name
+ description
+ releasedAt
+ createdAt
+ milestones {
+ nodes {
+ title
+ }
+ }
+ assets {
+ links {
+ nodes {
+ name
+ url
+ linkType
+ external
+ directAssetUrl
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ let(:create_release) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+
+ stub_default_url_options(host: 'www.example.com')
+ end
+
+ shared_examples 'no errors' do
+ it 'returns no errors' do
+ create_release
+
+ expect(graphql_errors).not_to be_present
+ end
+ end
+
+ shared_examples 'top-level error with message' do |error_message|
+ it 'returns a top-level error with message' do
+ create_release
+
+ expect(mutation_response).to be_nil
+ expect(graphql_errors.count).to eq(1)
+ expect(graphql_errors.first['message']).to eq(error_message)
+ end
+ end
+
+ shared_examples 'errors-as-data with message' do |error_message|
+ it 'returns an error-as-data with message' do
+ create_release
+
+ expect(mutation_response[:release]).to be_nil
+ expect(mutation_response[:errors].count).to eq(1)
+ expect(mutation_response[:errors].first).to match(error_message)
+ end
+ end
+
+ context 'when the current user has access to create releases' do
+ let(:current_user) { developer }
+
+ context 'when all available mutation arguments are provided' do
+ it_behaves_like 'no errors'
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ it 'returns the new release data' do
+ create_release
+
+ release = mutation_response[:release]
+ expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << asset_link[:directAssetPath]
+
+ expected_attributes = {
+ tagName: tag_name,
+ name: name,
+ description: description,
+ releasedAt: Time.parse(released_at).utc.iso8601,
+ createdAt: Time.current.utc.iso8601,
+ assets: {
+ links: {
+ nodes: [{
+ name: asset_link[:name],
+ url: asset_link[:url],
+ linkType: asset_link[:linkType],
+ external: true,
+ directAssetUrl: expected_direct_asset_url
+ }]
+ }
+ }
+ }
+
+ expect(release).to include(expected_attributes)
+
+ # Right now the milestones are returned in a non-deterministic order.
+ # This `milestones` test should be moved up into the expect(release)
+ # above (and `.to include` updated to `.to eq`) once
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
+ expect(release['milestones']['nodes']).to match_array([
+ { 'title' => '12.4' },
+ { 'title' => '12.3' }
+ ])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ context 'when only the required mutation arguments are provided' do
+ let(:mutation_arguments) { super().slice(:projectPath, :tagName, :ref) }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the new release data' do
+ create_release
+
+ expected_response = {
+ tagName: tag_name,
+ name: tag_name,
+ description: nil,
+ releasedAt: Time.current.utc.iso8601,
+ createdAt: Time.current.utc.iso8601,
+ milestones: {
+ nodes: []
+ },
+ assets: {
+ links: {
+ nodes: []
+ }
+ }
+ }.with_indifferent_access
+
+ expect(mutation_response[:release]).to eq(expected_response)
+ end
+ end
+
+ context 'when the provided tag already exists' do
+ let(:tag_name) { 'v1.1.0' }
+
+ it_behaves_like 'no errors'
+
+ it 'does not create a new tag' do
+ expect { create_release }.not_to change { Project.find_by_id(project.id).repository.tag_count }
+ end
+ end
+
+ context 'when the provided tag does not already exist' do
+ let(:tag_name) { 'v7.12.5-alpha' }
+
+ it_behaves_like 'no errors'
+
+ it 'creates a new tag' do
+ expect { create_release }.to change { Project.find_by_id(project.id).repository.tag_count }.by(1)
+ end
+ end
+
+ context 'when a local timezone is provided for releasedAt' do
+ let(:released_at) { Time.parse(super()).in_time_zone('Hawaii').iso8601 }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the correct releasedAt date in UTC' do
+ create_release
+
+ expect(mutation_response[:release]).to include({ releasedAt: Time.parse(released_at).utc.iso8601 })
+ end
+ end
+
+ context 'when no releasedAt is provided' do
+ let(:mutation_arguments) { super().except(:releasedAt) }
+
+ it_behaves_like 'no errors'
+
+ it 'sets releasedAt to the current time' do
+ create_release
+
+ expect(mutation_response[:release]).to include({ releasedAt: Time.current.utc.iso8601 })
+ end
+ end
+
+ context "when a release asset doesn't include an explicit linkType" do
+ let(:asset_link) { super().except(:linkType) }
+
+ it_behaves_like 'no errors'
+
+ it 'defaults the linkType to OTHER' do
+ create_release
+
+ returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :linkType)
+
+ expect(returned_asset_link_type).to eq('OTHER')
+ end
+ end
+
+ context "when a release asset doesn't include a directAssetPath" do
+ let(:asset_link) { super().except(:directAssetPath) }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the provided url as the directAssetUrl' do
+ create_release
+
+ returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :directAssetUrl)
+
+ expect(returned_asset_link_type).to eq(asset_link[:url])
+ end
+ end
+
+ context 'empty milestones' do
+ shared_examples 'no associated milestones' do
+ it_behaves_like 'no errors'
+
+ it 'creates a release with no associated milestones' do
+ create_release
+
+ returned_milestones = mutation_response.dig(:release, :milestones, :nodes)
+
+ expect(returned_milestones.count).to eq(0)
+ end
+ end
+
+ context 'when the milestones parameter is not provided' do
+ let(:mutation_arguments) { super().except(:milestones) }
+
+ it_behaves_like 'no associated milestones'
+ end
+
+ context 'when the milestones parameter is null' do
+ let(:milestones) { nil }
+
+ it_behaves_like 'no associated milestones'
+ end
+
+ context 'when the milestones parameter is an empty array' do
+ let(:milestones) { [] }
+
+ it_behaves_like 'no associated milestones'
+ end
+ end
+
+ context 'validation' do
+ context 'when a release is already associated to the specified tag' do
+ before do
+ create(:release, project: project, tag: tag_name)
+ end
+
+ it_behaves_like 'errors-as-data with message', 'Release already exists'
+ end
+
+ context "when a provided milestone doesn\'t exist" do
+ let(:milestones) { ['a fake milestone'] }
+
+ it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: a fake milestone'
+ end
+
+ context "when a provided milestone belongs to a different project than the release" do
+ let(:milestone_in_different_project) { create(:milestone, title: 'different milestone') }
+ let(:milestones) { [milestone_in_different_project.title] }
+
+ it_behaves_like 'errors-as-data with message', "Milestone(s) not found: different milestone"
+ end
+
+ context 'when two release assets share the same name' do
+ let(:asset_link_1) { { name: 'My link', url: 'https://example.com/1' } }
+ let(:asset_link_2) { { name: 'My link', url: 'https://example.com/2' } }
+ let(:assets) { { links: [asset_link_1, asset_link_2] } }
+
+ # Right now the raw Postgres error message is sent to the user as the validation message.
+ # We should catch this validation error and return a nicer message:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/277087
+ it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ end
+
+ context 'when two release assets share the same URL' do
+ let(:asset_link_1) { { name: 'My first link', url: 'https://example.com' } }
+ let(:asset_link_2) { { name: 'My second link', url: 'https://example.com' } }
+ let(:assets) { { links: [asset_link_1, asset_link_2] } }
+
+ # Same note as above about the ugly error message
+ it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ end
+
+ context 'when the provided tag name is HEAD' do
+ let(:tag_name) { 'HEAD' }
+
+ it_behaves_like 'errors-as-data with message', 'Tag name invalid'
+ end
+
+ context 'when the provided tag name is empty' do
+ let(:tag_name) { '' }
+
+ it_behaves_like 'errors-as-data with message', 'Tag name invalid'
+ end
+
+ context "when the provided tag doesn't already exist, and no ref parameter was provided" do
+ let(:ref) { nil }
+ let(:tag_name) { 'v7.12.5-beta' }
+
+ it_behaves_like 'errors-as-data with message', 'Ref is not specified'
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to create releases" do
+ expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+
+ context 'when the current user is a Reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a Guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a public user' do
+ let(:current_user) { public_user }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+ end
+end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 31bb0586e9f..48d125a37c3 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -9,18 +9,17 @@ RSpec.describe 'Git LFS API and storage' do
let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:other_project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
- let!(:lfs_object) { create(:lfs_object, :with_file) }
+ let(:lfs_object) { create(:lfs_object, :with_file) }
let(:headers) do
{
'Authorization' => authorization,
- 'X-Sendfile-Type' => sendfile
+ 'X-Sendfile-Type' => 'X-Sendfile'
}.compact
end
let(:include_workhorse_jwt_header) { true }
let(:authorization) { }
- let(:sendfile) { }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:sample_oid) { lfs_object.oid }
@@ -37,18 +36,6 @@ RSpec.describe 'Git LFS API and storage' do
stub_lfs_setting(enabled: lfs_enabled)
end
- describe 'when LFS is disabled' do
- let(:lfs_enabled) { false }
- let(:body) { upload_body(multiple_objects) }
- let(:authorization) { authorize_user }
-
- before do
- post_lfs_json batch_url(project), body, headers
- end
-
- it_behaves_like 'LFS http 501 response'
- end
-
context 'project specific LFS settings' do
let(:body) { upload_body(sample_object) }
let(:authorization) { authorize_user }
@@ -60,105 +47,36 @@ RSpec.describe 'Git LFS API and storage' do
subject
end
- context 'with LFS disabled globally' do
- let(:lfs_enabled) { false }
-
- describe 'LFS disabled in project' do
- let(:project_lfs_enabled) { false }
-
- context 'when uploading' do
- subject { post_lfs_json(batch_url(project), body, headers) }
-
- it_behaves_like 'LFS http 501 response'
- end
+ describe 'LFS disabled in project' do
+ let(:project_lfs_enabled) { false }
- context 'when downloading' do
- subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
+ context 'when uploading' do
+ subject { post_lfs_json(batch_url(project), body, headers) }
- it_behaves_like 'LFS http 501 response'
- end
+ it_behaves_like 'LFS http 404 response'
end
- describe 'LFS enabled in project' do
- let(:project_lfs_enabled) { true }
-
- context 'when uploading' do
- subject { post_lfs_json(batch_url(project), body, headers) }
-
- it_behaves_like 'LFS http 501 response'
- end
+ context 'when downloading' do
+ subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- context 'when downloading' do
- subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
-
- it_behaves_like 'LFS http 501 response'
- end
+ it_behaves_like 'LFS http 404 response'
end
end
- context 'with LFS enabled globally' do
- describe 'LFS disabled in project' do
- let(:project_lfs_enabled) { false }
-
- context 'when uploading' do
- subject { post_lfs_json(batch_url(project), body, headers) }
-
- it_behaves_like 'LFS http 403 response'
- end
-
- context 'when downloading' do
- subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
-
- it_behaves_like 'LFS http 403 response'
- end
- end
-
- describe 'LFS enabled in project' do
- let(:project_lfs_enabled) { true }
-
- context 'when uploading' do
- subject { post_lfs_json(batch_url(project), body, headers) }
-
- it_behaves_like 'LFS http 200 response'
- end
+ describe 'LFS enabled in project' do
+ let(:project_lfs_enabled) { true }
- context 'when downloading' do
- subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
+ context 'when uploading' do
+ subject { post_lfs_json(batch_url(project), body, headers) }
- it_behaves_like 'LFS http 200 response'
- end
+ it_behaves_like 'LFS http 200 response'
end
- end
- end
- describe 'deprecated API' do
- let(:authorization) { authorize_user }
+ context 'when downloading' do
+ subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
- shared_examples 'deprecated request' do
- before do
- subject
+ it_behaves_like 'LFS http 200 blob response'
end
-
- it_behaves_like 'LFS http expected response code and message' do
- let(:response_code) { 501 }
- let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' }
- end
- end
-
- context 'when fetching LFS object using deprecated API' do
- subject { get(deprecated_objects_url(project, sample_oid), params: {}, headers: headers) }
-
- it_behaves_like 'deprecated request'
- end
-
- context 'when handling LFS request using deprecated API' do
- subject { post_lfs_json(deprecated_objects_url(project), nil, headers) }
-
- it_behaves_like 'deprecated request'
- end
-
- def deprecated_objects_url(project, oid = nil)
- File.join(["#{project.http_url_to_repo}/info/lfs/objects/", oid].compact)
end
end
@@ -167,196 +85,133 @@ RSpec.describe 'Git LFS API and storage' do
let(:before_get) { }
before do
+ project.lfs_objects << lfs_object
update_permissions
before_get
+
get objects_url(project, sample_oid), params: {}, headers: headers
end
- context 'and request comes from gitlab-workhorse' do
- context 'without user being authorized' do
- it_behaves_like 'LFS http 401 response'
- end
+ context 'when LFS uses object storage' do
+ let(:authorization) { authorize_user }
- context 'with required headers' do
- shared_examples 'responds with a file' do
- let(:sendfile) { 'X-Sendfile' }
+ let(:update_permissions) do
+ project.add_maintainer(user)
+ end
- it_behaves_like 'LFS http 200 response'
+ context 'when proxy download is enabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: true)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
- it 'responds with the file location' do
- expect(response.headers['Content-Type']).to eq('application/octet-stream')
- expect(response.headers['X-Sendfile']).to eq(lfs_object.file.path)
- end
+ it 'responds with the workhorse send-url' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
end
+ end
- context 'with user is authorized' do
- let(:authorization) { authorize_user }
+ context 'when proxy download is disabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: false)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
- context 'and does not have project access' do
- let(:update_permissions) do
- project.lfs_objects << lfs_object
- end
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(:found)
+ end
- it_behaves_like 'LFS http 404 response'
- end
+ it 'responds with the file location' do
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
+ end
+ end
- context 'and does have project access' do
- let(:update_permissions) do
- project.add_maintainer(user)
- project.lfs_objects << lfs_object
- end
+ context 'when deploy key is authorized' do
+ let(:key) { create(:deploy_key) }
+ let(:authorization) { authorize_deploy_key }
- it_behaves_like 'responds with a file'
+ let(:update_permissions) do
+ project.deploy_keys << key
+ end
- context 'when LFS uses object storage' do
- context 'when proxy download is enabled' do
- let(:before_get) do
- stub_lfs_object_storage(proxy_download: true)
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- end
+ it_behaves_like 'LFS http 200 blob response'
+ end
- it_behaves_like 'LFS http 200 response'
+ context 'when using a user key (LFSToken)' do
+ let(:authorization) { authorize_user_key }
- it 'responds with the workhorse send-url' do
- expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
- end
- end
+ context 'when user allowed' do
+ let(:update_permissions) do
+ project.add_maintainer(user)
+ end
- context 'when proxy download is disabled' do
- let(:before_get) do
- stub_lfs_object_storage(proxy_download: false)
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- end
+ it_behaves_like 'LFS http 200 blob response'
- it 'responds with redirect' do
- expect(response).to have_gitlab_http_status(:found)
- end
+ context 'when user password is expired' do
+ let(:user) { create(:user, password_expires_at: 1.minute.ago)}
- it 'responds with the file location' do
- expect(response.location).to include(lfs_object.reload.file.path)
- end
- end
- end
- end
+ it_behaves_like 'LFS http 401 response'
end
- context 'when deploy key is authorized' do
- let(:key) { create(:deploy_key) }
- let(:authorization) { authorize_deploy_key }
-
- let(:update_permissions) do
- project.deploy_keys << key
- project.lfs_objects << lfs_object
- end
+ context 'when user is blocked' do
+ let(:user) { create(:user, :blocked)}
- it_behaves_like 'responds with a file'
+ it_behaves_like 'LFS http 401 response'
end
+ end
- describe 'when using a user key (LFSToken)' do
- let(:authorization) { authorize_user_key }
-
- context 'when user allowed' do
- let(:update_permissions) do
- project.add_maintainer(user)
- project.lfs_objects << lfs_object
- end
+ context 'when user not allowed' do
+ it_behaves_like 'LFS http 404 response'
+ end
+ end
- it_behaves_like 'responds with a file'
+ context 'when build is authorized as' do
+ let(:authorization) { authorize_ci_project }
- context 'when user password is expired' do
- let(:user) { create(:user, password_expires_at: 1.minute.ago)}
+ shared_examples 'can download LFS only from own projects' do
+ context 'for owned project' do
+ let(:project) { create(:project, namespace: user.namespace) }
- it_behaves_like 'LFS http 401 response'
- end
+ it_behaves_like 'LFS http 200 blob response'
+ end
- context 'when user is blocked' do
- let(:user) { create(:user, :blocked)}
+ context 'for member of project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- it_behaves_like 'LFS http 401 response'
- end
+ let(:update_permissions) do
+ project.add_reporter(user)
end
- context 'when user not allowed' do
- let(:update_permissions) do
- project.lfs_objects << lfs_object
- end
-
- it_behaves_like 'LFS http 404 response'
- end
+ it_behaves_like 'LFS http 200 blob response'
end
- context 'when build is authorized as' do
- let(:authorization) { authorize_ci_project }
-
- shared_examples 'can download LFS only from own projects' do
- context 'for owned project' do
- let(:project) { create(:project, namespace: user.namespace) }
-
- let(:update_permissions) do
- project.lfs_objects << lfs_object
- end
-
- it_behaves_like 'responds with a file'
- end
-
- context 'for member of project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
-
- let(:update_permissions) do
- project.add_reporter(user)
- project.lfs_objects << lfs_object
- end
-
- it_behaves_like 'responds with a file'
- end
+ context 'for other project' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
- context 'for other project' do
- let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
-
- let(:update_permissions) do
- project.lfs_objects << lfs_object
- end
-
- it 'rejects downloading code' do
- expect(response).to have_gitlab_http_status(other_project_status)
- end
- end
- end
-
- context 'administrator' do
- let(:user) { create(:admin) }
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
-
- it_behaves_like 'can download LFS only from own projects' do
- # We render 403, because administrator does have normally access
- let(:other_project_status) { 403 }
- end
+ it 'rejects downloading code' do
+ expect(response).to have_gitlab_http_status(:not_found)
end
+ end
+ end
- context 'regular user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+ context 'administrator' do
+ let(:user) { create(:admin) }
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it_behaves_like 'can download LFS only from own projects' do
- # We render 404, to prevent data leakage about existence of the project
- let(:other_project_status) { 404 }
- end
- end
+ it_behaves_like 'can download LFS only from own projects'
+ end
- context 'does not have user' do
- let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ context 'regular user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it_behaves_like 'can download LFS only from own projects' do
- # We render 404, to prevent data leakage about existence of the project
- let(:other_project_status) { 404 }
- end
- end
- end
+ it_behaves_like 'can download LFS only from own projects'
end
- context 'without required headers' do
- let(:authorization) { authorize_user }
+ context 'does not have user' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it_behaves_like 'LFS http 404 response'
+ it_behaves_like 'can download LFS only from own projects'
end
end
end
@@ -511,7 +366,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:role) { :reporter }
end
- context 'when user does is not member of the project' do
+ context 'when user is not a member of the project' do
let(:update_user_permissions) { nil }
it_behaves_like 'LFS http 404 response'
@@ -520,7 +375,7 @@ RSpec.describe 'Git LFS API and storage' do
context 'when user does not have download access' do
let(:role) { :guest }
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 404 response'
end
context 'when user password is expired' do
@@ -591,7 +446,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it 'rejects downloading code' do
- expect(response).to have_gitlab_http_status(other_project_status)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -600,28 +455,19 @@ RSpec.describe 'Git LFS API and storage' do
let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it_behaves_like 'can download LFS only from own projects', renew_authorization: true do
- # We render 403, because administrator does have normally access
- let(:other_project_status) { 403 }
- end
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: true
end
context 'regular user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
- it_behaves_like 'can download LFS only from own projects', renew_authorization: true do
- # We render 404, to prevent data leakage about existence of the project
- let(:other_project_status) { 404 }
- end
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: true
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it_behaves_like 'can download LFS only from own projects', renew_authorization: false do
- # We render 404, to prevent data leakage about existence of the project
- let(:other_project_status) { 404 }
- end
+ it_behaves_like 'can download LFS only from own projects', renew_authorization: false
end
end
@@ -919,11 +765,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize
end
- it_behaves_like 'LFS http 200 response'
-
- it 'uses the gitlab-workhorse content type' do
- expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- end
+ it_behaves_like 'LFS http 200 workhorse response'
end
shared_examples 'a local file' do
@@ -1142,7 +984,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize
end
- it_behaves_like 'LFS http 404 response'
+ it_behaves_like 'LFS http 403 response'
end
end
@@ -1155,7 +997,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize
end
- it_behaves_like 'LFS http 200 response'
+ it_behaves_like 'LFS http 200 workhorse response'
context 'when user password is expired' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)}
@@ -1202,7 +1044,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize
end
- it_behaves_like 'LFS http 200 response'
+ it_behaves_like 'LFS http 200 workhorse response'
it 'with location of LFS store and object details' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
@@ -1330,4 +1172,50 @@ RSpec.describe 'Git LFS API and storage' do
"#{sample_oid}012345678"
end
end
+
+ context 'with projects' do
+ it_behaves_like 'LFS http requests' do
+ let(:container) { project }
+ let(:authorize_guest) { project.add_guest(user) }
+ let(:authorize_download) { project.add_reporter(user) }
+ let(:authorize_upload) { project.add_developer(user) }
+ end
+ end
+
+ context 'with project wikis' do
+ it_behaves_like 'LFS http requests' do
+ let(:container) { create(:project_wiki, :empty_repo, project: project) }
+ let(:authorize_guest) { project.add_guest(user) }
+ let(:authorize_download) { project.add_reporter(user) }
+ let(:authorize_upload) { project.add_developer(user) }
+ end
+ end
+
+ context 'with snippets' do
+ # LFS is not supported on snippets, so we override the shared examples
+ # to expect 404 responses instead.
+ [
+ 'LFS http 200 response',
+ 'LFS http 200 blob response',
+ 'LFS http 403 response'
+ ].each do |examples|
+ shared_examples_for(examples) { it_behaves_like 'LFS http 404 response' }
+ end
+
+ context 'with project snippets' do
+ it_behaves_like 'LFS http requests' do
+ let(:container) { create(:project_snippet, :empty_repo, project: project) }
+ let(:authorize_guest) { project.add_guest(user) }
+ let(:authorize_download) { project.add_reporter(user) }
+ let(:authorize_upload) { project.add_developer(user) }
+ end
+ end
+
+ context 'with personal snippets' do
+ it_behaves_like 'LFS http requests' do
+ let(:container) { create(:personal_snippet, :empty_repo) }
+ let(:authorize_upload) { container.update!(author: user) }
+ end
+ end
+ end
end
diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb
index 34e345cb1cf..0eb3cb4ca07 100644
--- a/spec/requests/lfs_locks_api_spec.rb
+++ b/spec/requests/lfs_locks_api_spec.rb
@@ -3,24 +3,38 @@
require 'spec_helper'
RSpec.describe 'Git LFS File Locking API' do
+ include LfsHttpHelpers
include WorkhorseHelpers
- let(:project) { create(:project) }
- let(:maintainer) { create(:user) }
- let(:developer) { create(:user) }
- let(:guest) { create(:user) }
- let(:path) { 'README.md' }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:path) { 'README.md' }
+
+ let(:user) { developer }
let(:headers) do
{
- 'Authorization' => authorization
+ 'Authorization' => authorize_user
}.compact
end
shared_examples 'unauthorized request' do
- context 'when user is not authorized' do
- let(:authorization) { authorize_user(guest) }
+ context 'when user does not have download permission' do
+ let(:user) { guest }
- it 'returns a forbidden 403 response' do
+ it 'returns a 404 response' do
+ post_lfs_json url, body, headers
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user does not have upload permission' do
+ let(:user) { reporter }
+
+ it 'returns a 403 response' do
post_lfs_json url, body, headers
expect(response).to have_gitlab_http_status(:forbidden)
@@ -31,15 +45,15 @@ RSpec.describe 'Git LFS File Locking API' do
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- project.add_developer(maintainer)
+ project.add_maintainer(maintainer)
project.add_developer(developer)
+ project.add_reporter(reporter)
project.add_guest(guest)
end
describe 'Create File Lock endpoint' do
- let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" }
- let(:authorization) { authorize_user(developer) }
- let(:body) { { path: path } }
+ let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" }
+ let(:body) { { path: path } }
include_examples 'unauthorized request'
@@ -76,8 +90,7 @@ RSpec.describe 'Git LFS File Locking API' do
end
describe 'Listing File Locks endpoint' do
- let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" }
- let(:authorization) { authorize_user(developer) }
+ let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" }
include_examples 'unauthorized request'
@@ -95,8 +108,7 @@ RSpec.describe 'Git LFS File Locking API' do
end
describe 'List File Locks for verification endpoint' do
- let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/verify" }
- let(:authorization) { authorize_user(developer) }
+ let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/verify" }
include_examples 'unauthorized request'
@@ -116,9 +128,8 @@ RSpec.describe 'Git LFS File Locking API' do
end
describe 'Delete File Lock endpoint' do
- let!(:lock) { lock_file('README.md', developer) }
- let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/#{lock[:id]}/unlock" }
- let(:authorization) { authorize_user(developer) }
+ let!(:lock) { lock_file('README.md', developer) }
+ let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/#{lock[:id]}/unlock" }
include_examples 'unauthorized request'
@@ -136,7 +147,7 @@ RSpec.describe 'Git LFS File Locking API' do
end
context 'when a maintainer uses force' do
- let(:authorization) { authorize_user(maintainer) }
+ let(:user) { maintainer }
it 'deletes the lock' do
project.add_maintainer(maintainer)
@@ -154,14 +165,6 @@ RSpec.describe 'Git LFS File Locking API' do
result[:lock]
end
- def authorize_user(user)
- ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- def post_lfs_json(url, body = nil, headers = nil)
- post(url, params: body.try(:to_json), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE))
- end
-
def do_get(url, params = nil, headers = nil)
get(url, params: (params || {}), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE))
end
diff --git a/spec/services/design_management/copy_design_collection/copy_service_spec.rb b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
index e93e5f13fea..ddbed91815f 100644
--- a/spec/services/design_management/copy_design_collection/copy_service_spec.rb
+++ b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
@@ -68,6 +68,31 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
include_examples 'service error', message: 'Target design collection already has designs'
end
+ context 'when target project already has designs' do
+ let!(:issue_x) { create(:issue, project: target_issue.project) }
+ let!(:existing) { create(:design, issue: issue_x, project: target_issue.project) }
+
+ let(:new_designs) do
+ target_issue.reset
+ target_issue.designs.where.not(id: existing.id)
+ end
+
+ it 'sets IIDs for new designs above existing ones' do
+ subject
+
+ expect(new_designs).to all(have_attributes(iid: (be > existing.iid)))
+ end
+
+ it 'does not allow for IID collisions' do
+ subject
+ create(:design, issue: issue_x, project: target_issue.project)
+
+ design_iids = target_issue.project.designs.map(&:id)
+
+ expect(design_iids).to match_array(design_iids.uniq)
+ end
+ end
+
include_examples 'service success'
it 'creates a design repository for the target project' do
@@ -162,9 +187,7 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
it 'copies the Git repository data', :aggregate_failures do
subject
- commit_shas = target_repository.commits('master', limit: 99).map(&:id)
-
- expect(commit_shas).to include(*target_issue.design_versions.ordered.pluck(:sha))
+ expect(commits_on_master(limit: 99)).to include(*target_issue.design_versions.ordered.pluck(:sha))
end
it 'creates a master branch if none previously existed' do
@@ -212,9 +235,7 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
issue_2 = create(:issue, project: target_issue.project)
create(:design, :with_file, issue: issue_2, project: target_issue.project)
- expect { subject }.not_to change {
- expect(target_repository.commits('master', limit: 10).size).to eq(1)
- }
+ expect { subject }.not_to change { commits_on_master }
end
it 'sets the design collection copy state' do
@@ -223,6 +244,10 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
expect(target_issue.design_collection).to be_copy_error
end
end
+
+ def commits_on_master(limit: 10)
+ target_repository.commits('master', limit: limit).map(&:id)
+ end
end
end
end
diff --git a/spec/support/helpers/lfs_http_helpers.rb b/spec/support/helpers/lfs_http_helpers.rb
index 0537b122040..199d5e70e32 100644
--- a/spec/support/helpers/lfs_http_helpers.rb
+++ b/spec/support/helpers/lfs_http_helpers.rb
@@ -31,16 +31,16 @@ module LfsHttpHelpers
post(url, params: params, headers: headers)
end
- def batch_url(project)
- "#{project.http_url_to_repo}/info/lfs/objects/batch"
+ def batch_url(container)
+ "#{container.http_url_to_repo}/info/lfs/objects/batch"
end
- def objects_url(project, oid = nil, size = nil)
- File.join(["#{project.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s))
+ def objects_url(container, oid = nil, size = nil)
+ File.join(["#{container.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s))
end
- def authorize_url(project, oid, size)
- File.join(objects_url(project, oid, size), 'authorize')
+ def authorize_url(container, oid, size)
+ File.join(objects_url(container, oid, size), 'authorize')
end
def download_body(objects)
diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
index 62d56f2e86e..fe99b1cacd9 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
@@ -76,6 +76,26 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
end
end
+ describe 'supply of internal ids' do
+ let(:scope_value) { scope_attrs.each_value.first }
+ let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" }
+
+ it 'provides a persistent supply of IID values, sensitive to the current state' do
+ iid = rand(1..1000)
+ write_internal_id(iid)
+ instance.public_send(:"track_#{scope}_#{internal_id_attribute}!")
+
+ # Allocate 3 IID values
+ described_class.public_send(method_name, scope_value) do |supply|
+ 3.times { supply.next_value }
+ end
+
+ current_value = described_class.public_send(method_name, scope_value, &:current_value)
+
+ expect(current_value).to eq(iid + 3)
+ end
+ end
+
describe "#reset_scope_internal_id_attribute" do
it 'rewinds the allocated IID' do
expect { ensure_scope_attribute! }.not_to raise_error
diff --git a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
index 48c5a5933e6..4ae77179527 100644
--- a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
+++ b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
@@ -2,42 +2,252 @@
RSpec.shared_examples 'LFS http 200 response' do
it_behaves_like 'LFS http expected response code and message' do
- let(:response_code) { 200 }
+ let(:response_code) { :ok }
+ end
+end
+
+RSpec.shared_examples 'LFS http 200 blob response' do
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { :ok }
+ let(:content_type) { Repositories::LfsApiController::LFS_TRANSFER_CONTENT_TYPE }
+ let(:response_headers) { { 'X-Sendfile' => lfs_object.file.path } }
+ end
+end
+
+RSpec.shared_examples 'LFS http 200 workhorse response' do
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { :ok }
+ let(:content_type) { Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE }
end
end
RSpec.shared_examples 'LFS http 401 response' do
it_behaves_like 'LFS http expected response code and message' do
- let(:response_code) { 401 }
+ let(:response_code) { :unauthorized }
+ let(:content_type) { 'text/plain' }
end
end
RSpec.shared_examples 'LFS http 403 response' do
it_behaves_like 'LFS http expected response code and message' do
- let(:response_code) { 403 }
+ let(:response_code) { :forbidden }
let(:message) { 'Access forbidden. Check your access level.' }
end
end
RSpec.shared_examples 'LFS http 501 response' do
it_behaves_like 'LFS http expected response code and message' do
- let(:response_code) { 501 }
+ let(:response_code) { :not_implemented }
let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' }
end
end
RSpec.shared_examples 'LFS http 404 response' do
it_behaves_like 'LFS http expected response code and message' do
- let(:response_code) { 404 }
+ let(:response_code) { :not_found }
end
end
RSpec.shared_examples 'LFS http expected response code and message' do
let(:response_code) { }
- let(:message) { }
+ let(:response_headers) { {} }
+ let(:content_type) { LfsRequest::CONTENT_TYPE }
+ let(:message) {}
- it 'responds with the expected response code and message' do
+ specify do
expect(response).to have_gitlab_http_status(response_code)
+ expect(response.headers.to_hash).to include(response_headers)
+ expect(response.media_type).to match(content_type)
expect(json_response['message']).to eq(message) if message
end
end
+
+RSpec.shared_examples 'LFS http requests' do
+ include LfsHttpHelpers
+
+ let(:authorize_guest) {}
+ let(:authorize_download) {}
+ let(:authorize_upload) {}
+
+ let(:lfs_object) { create(:lfs_object, :with_file) }
+ let(:sample_oid) { lfs_object.oid }
+
+ let(:authorization) { authorize_user }
+ let(:headers) do
+ {
+ 'Authorization' => authorization,
+ 'X-Sendfile-Type' => 'X-Sendfile'
+ }
+ end
+
+ let(:request_download) do
+ get objects_url(container, sample_oid), params: {}, headers: headers
+ end
+
+ let(:request_upload) do
+ post_lfs_json batch_url(container), upload_body(multiple_objects), headers
+ end
+
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ context 'when LFS is disabled globally' do
+ before do
+ stub_lfs_setting(enabled: false)
+ end
+
+ describe 'download request' do
+ before do
+ request_download
+ end
+
+ it_behaves_like 'LFS http 501 response'
+ end
+
+ describe 'upload request' do
+ before do
+ request_upload
+ end
+
+ it_behaves_like 'LFS http 501 response'
+ end
+ end
+
+ context 'unauthenticated' do
+ let(:headers) { {} }
+
+ describe 'download request' do
+ before do
+ request_download
+ end
+
+ it_behaves_like 'LFS http 401 response'
+ end
+
+ describe 'upload request' do
+ before do
+ request_upload
+ end
+
+ it_behaves_like 'LFS http 401 response'
+ end
+ end
+
+ context 'without access' do
+ describe 'download request' do
+ before do
+ request_download
+ end
+
+ it_behaves_like 'LFS http 404 response'
+ end
+
+ describe 'upload request' do
+ before do
+ request_upload
+ end
+
+ it_behaves_like 'LFS http 404 response'
+ end
+ end
+
+ context 'with guest access' do
+ before do
+ authorize_guest
+ end
+
+ describe 'download request' do
+ before do
+ request_download
+ end
+
+ it_behaves_like 'LFS http 404 response'
+ end
+
+ describe 'upload request' do
+ before do
+ request_upload
+ end
+
+ it_behaves_like 'LFS http 404 response'
+ end
+ end
+
+ context 'with download permission' do
+ before do
+ authorize_download
+ end
+
+ describe 'download request' do
+ before do
+ request_download
+ end
+
+ it_behaves_like 'LFS http 200 blob response'
+
+ context 'when container does not exist' do
+ def objects_url(*args)
+ super.sub(container.full_path, 'missing/path')
+ end
+
+ it_behaves_like 'LFS http 404 response'
+ end
+ end
+
+ describe 'upload request' do
+ before do
+ request_upload
+ end
+
+ it_behaves_like 'LFS http 403 response'
+ end
+ end
+
+ context 'with upload permission' do
+ before do
+ authorize_upload
+ end
+
+ describe 'upload request' do
+ before do
+ request_upload
+ end
+
+ it_behaves_like 'LFS http 200 response'
+ end
+ end
+
+ describe 'deprecated API' do
+ shared_examples 'deprecated request' do
+ before do
+ request
+ end
+
+ it_behaves_like 'LFS http expected response code and message' do
+ let(:response_code) { 501 }
+ let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' }
+ end
+ end
+
+ context 'when fetching LFS object using deprecated API' do
+ subject(:request) do
+ get deprecated_objects_url(container, sample_oid), params: {}, headers: headers
+ end
+
+ it_behaves_like 'deprecated request'
+ end
+
+ context 'when handling LFS request using deprecated API' do
+ subject(:request) do
+ post_lfs_json deprecated_objects_url(container), nil, headers
+ end
+
+ it_behaves_like 'deprecated request'
+ end
+
+ def deprecated_objects_url(container, oid = nil)
+ File.join(["#{container.http_url_to_repo}/info/lfs/objects/", oid].compact)
+ end
+ end
+end
diff --git a/spec/views/search/_results.html.haml_spec.rb b/spec/views/search/_results.html.haml_spec.rb
index 6299fd0cf36..58912eab51e 100644
--- a/spec/views/search/_results.html.haml_spec.rb
+++ b/spec/views/search/_results.html.haml_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'search/_results' do
let_it_be(:wiki_blob) { create(:wiki_page, project: project, content: '*') }
let_it_be(:user) { create(:admin) }
- %w[issues merge_requests].each do |search_scope|
+ %w[issues blobs notes wiki_blobs merge_requests milestones].each do |search_scope|
context "when scope is #{search_scope}" do
let(:scope) { search_scope }
let(:search_objects) { Gitlab::ProjectSearchResults.new(user, '*', project: project).objects(scope) }
@@ -55,30 +55,16 @@ RSpec.describe 'search/_results' do
expect(rendered).to have_selector('[data-track-property=search_result]')
end
- it 'does render the sidebar' do
+ it 'renders the state filter drop down' do
render
- expect(rendered).to have_selector('#js-search-sidebar')
- end
- end
- end
-
- %w[blobs notes wiki_blobs milestones].each do |search_scope|
- context "when scope is #{search_scope}" do
- let(:scope) { search_scope }
- let(:search_objects) { Gitlab::ProjectSearchResults.new(user, '*', project: project).objects(scope) }
-
- it 'renders the click text event tracking attributes' do
- render
-
- expect(rendered).to have_selector('[data-track-event=click_text]')
- expect(rendered).to have_selector('[data-track-property=search_result]')
+ expect(rendered).to have_selector('#js-search-filter-by-state')
end
- it 'does not render the sidebar' do
+ it 'renders the confidential drop down' do
render
- expect(rendered).not_to have_selector('#js-search-sidebar')
+ expect(rendered).to have_selector('#js-search-filter-by-confidential')
end
end
end