diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-11 03:08:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-11 03:08:58 +0300 |
commit | 5427433c6d79f9131f4025cabb7e3208380bce9a (patch) | |
tree | ea0a22450f467f1ef1e3449255017dbe0f178882 /spec | |
parent | 13bcb8221306526671a61df589f7c05505c9934c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |