From c792263edfaf826c58f4aa41d26904464a17a3e7 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 23 Sep 2019 18:06:14 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../user_suggests_changes_on_diff_spec.rb | 58 +++++ .../api/schemas/public_api/v4/release.json | 3 + .../public_api/v4/release/release_for_guest.json | 6 + .../ide/components/file_templates/dropdown_spec.js | 175 +++++++++++++ .../releases/components/release_block_spec.js | 7 +- .../ide/components/file_templates/dropdown_spec.js | 201 --------------- .../releases/components/release_block_spec.js | 170 ------------- spec/lib/backup/manager_spec.rb | 45 +++- spec/lib/banzai/filter/video_link_filter_spec.rb | 2 +- spec/lib/gitlab/diff/position_spec.rb | 20 ++ spec/lib/gitlab/file_type_detection_spec.rb | 273 ++++++++++++++++++--- spec/lib/gitlab/usage_data_spec.rb | 43 ++-- spec/models/repository_spec.rb | 52 +++- spec/models/suggestion_spec.rb | 10 - spec/requests/api/releases_spec.rb | 21 +- spec/support/helpers/repo_helpers.rb | 4 +- 16 files changed, 638 insertions(+), 452 deletions(-) create mode 100644 spec/frontend/ide/components/file_templates/dropdown_spec.js delete mode 100644 spec/javascripts/ide/components/file_templates/dropdown_spec.js delete mode 100644 spec/javascripts/releases/components/release_block_spec.js (limited to 'spec') diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb index 4363b359038..3d26ff3ed94 100644 --- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb +++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb @@ -14,6 +14,10 @@ describe 'User comments on a diff', :js do expect(suggested_content).to eq(expected_suggested_content) end + def expect_appliable_suggestions(amount) + expect(all('button', text: 'Apply suggestion').size).to eq(amount) + end + let(:project) { create(:project, :repository) } let(:merge_request) do create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') @@ -89,6 +93,60 @@ describe 'User comments on a diff', :js do end end + context 'multiple suggestions in expanded lines' do + it 'suggestions are appliable' do + diff_file = merge_request.diffs(paths: ['files/ruby/popen.rb']).diff_files.first + hash = Digest::SHA1.hexdigest(diff_file.file_path) + + expanded_changes = [ + { + line_code: "#{hash}_1_1", + file_path: diff_file.file_path + }, + { + line_code: "#{hash}_5_5", + file_path: diff_file.file_path + } + ] + changes = sample_compare(expanded_changes).changes.last(expanded_changes.size) + + page.within("[id='#{hash}']") do + find("button[data-original-title='Show full file']").click + wait_for_requests + + click_diff_line(find("[id='#{changes.first[:line_code]}']")) + + page.within('.js-discussion-note-form') do + fill_in('note_note', with: "```suggestion\n# change to a comment\n```") + click_button('Comment') + wait_for_requests + end + + click_diff_line(find("[id='#{changes.last[:line_code]}']")) + + page.within('.js-discussion-note-form') do + fill_in('note_note', with: "```suggestion\n# 2nd change to a comment\n```") + click_button('Comment') + wait_for_requests + end + + expect_appliable_suggestions(2) + end + + # Making sure it's not a Front-end cache. + visit(diffs_project_merge_request_path(project, merge_request)) + + expect_appliable_suggestions(2) + + page.within("[id='#{hash}']") do + all('button', text: 'Apply suggestion').last.click + wait_for_requests + + expect(page).to have_content('Applied') + end + end + end + context 'multiple suggestions in a single note' do it 'suggestions are presented' do click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']")) diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json index 662e61a9c06..3ca6167d0c6 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release.json +++ b/spec/fixtures/api/schemas/public_api/v4/release.json @@ -19,6 +19,9 @@ "type": "array", "items": { "$ref": "milestone.json" } }, + "commit_path": { "type": "string" }, + "tag_path": { "type": "string" }, + "name": { "type": "string" }, "assets": { "required": ["count", "links", "sources"], "properties": { diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json index 0c1e8fd5fb3..57814b8bf73 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json +++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json @@ -8,6 +8,12 @@ "created_at": { "type": "date" }, "released_at": { "type": "date" }, "upcoming_release": { "type": "boolean" }, + "milestones": { + "type": "array", + "items": { "$ref": "../milestone.json" } + }, + "commit_path": { "type": "string" }, + "tag_path": { "type": "string" }, "author": { "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }] }, diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js new file mode 100644 index 00000000000..83d797469ad --- /dev/null +++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js @@ -0,0 +1,175 @@ +import Vuex from 'vuex'; +import $ from 'jquery'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Dropdown from '~/ide/components/file_templates/dropdown.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('IDE file templates dropdown component', () => { + let wrapper; + let element; + let fetchTemplateTypesMock; + + const defaultProps = { + label: 'label', + }; + + const findItemButtons = () => wrapper.findAll('button'); + const findSearch = () => wrapper.find('input[type="search"]'); + const triggerDropdown = () => $(element).trigger('show.bs.dropdown'); + + const createComponent = ({ props, state } = {}) => { + fetchTemplateTypesMock = jest.fn(); + const fakeStore = new Vuex.Store({ + modules: { + fileTemplates: { + namespaced: true, + state: { + templates: [], + isLoading: false, + ...state, + }, + actions: { + fetchTemplateTypes: fetchTemplateTypesMock, + }, + }, + }, + }); + + wrapper = shallowMount(Dropdown, { + propsData: { + ...defaultProps, + ...props, + }, + store: fakeStore, + localVue, + sync: false, + }); + + ({ element } = wrapper); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('calls clickItem on click', () => { + const itemData = { name: 'test.yml ' }; + createComponent({ props: { data: [itemData] } }); + const item = findItemButtons().at(0); + item.trigger('click'); + + expect(wrapper.emitted().click[0][0]).toBe(itemData); + }); + + it('renders dropdown title', () => { + const title = 'Test title'; + createComponent({ props: { title } }); + + expect(wrapper.find('.dropdown-title').text()).toContain(title); + }); + + describe('in async mode', () => { + const defaultAsyncProps = { ...defaultProps, isAsyncData: true }; + + it('calls `fetchTemplateTypes` on dropdown event', () => { + createComponent({ props: defaultAsyncProps }); + + triggerDropdown(); + + expect(fetchTemplateTypesMock).toHaveBeenCalled(); + }); + + it('does not call `fetchTemplateTypes` on dropdown event if destroyed', () => { + createComponent({ props: defaultAsyncProps }); + wrapper.destroy(); + + triggerDropdown(); + + expect(fetchTemplateTypesMock).not.toHaveBeenCalled(); + }); + + it('shows loader when isLoading is true', () => { + createComponent({ props: defaultAsyncProps, state: { isLoading: true } }); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('renders templates', () => { + const templates = [{ name: 'file-1' }, { name: 'file-2' }]; + createComponent({ + props: { ...defaultAsyncProps, data: [{ name: 'should-never-appear ' }] }, + state: { + templates, + }, + }); + const items = findItemButtons(); + + expect(items.wrappers.map(x => x.text())).toEqual(templates.map(x => x.name)); + }); + + it('searches template data', () => { + const templates = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }]; + const matches = ['match 1', 'match 2']; + createComponent({ + props: { ...defaultAsyncProps, data: matches, searchable: true }, + state: { templates }, + }); + findSearch().setValue('match'); + return wrapper.vm.$nextTick().then(() => { + const items = findItemButtons(); + + expect(items.length).toBe(matches.length); + expect(items.wrappers.map(x => x.text())).toEqual(matches); + }); + }); + + it('does not render input when `searchable` is true & `showLoading` is true', () => { + createComponent({ + props: { ...defaultAsyncProps, searchable: true }, + state: { isLoading: true }, + }); + + expect(findSearch().exists()).toBe(false); + }); + }); + + describe('in sync mode', () => { + it('renders props data', () => { + const data = [{ name: 'file-1' }, { name: 'file-2' }]; + createComponent({ + props: { data }, + state: { + templates: [{ name: 'should-never-appear ' }], + }, + }); + + const items = findItemButtons(); + + expect(items.length).toBe(data.length); + expect(items.wrappers.map(x => x.text())).toEqual(data.map(x => x.name)); + }); + + it('renders input when `searchable` is true', () => { + createComponent({ props: { searchable: true } }); + + expect(findSearch().exists()).toBe(true); + }); + + it('searches data', () => { + const data = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }]; + const matches = ['match 1', 'match 2']; + createComponent({ props: { searchable: true, data } }); + findSearch().setValue('match'); + return wrapper.vm.$nextTick().then(() => { + const items = findItemButtons(); + + expect(items.length).toBe(matches.length); + expect(items.wrappers.map(x => x.text())).toEqual(matches); + }); + }); + }); +}); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index 4be5d500fd9..229d3799ee1 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -12,7 +12,6 @@ describe('Release block', () => { propsData: { release: releaseProp, }, - sync: false, }); }; @@ -37,10 +36,16 @@ describe('Release block', () => { it('renders commit sha', () => { expect(wrapper.text()).toContain(release.commit.short_id); + + wrapper.setProps({ release: { ...release, commit_path: '/commit/example' } }); + expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true); }); it('renders tag name', () => { expect(wrapper.text()).toContain(release.tag_name); + + wrapper.setProps({ release: { ...release, tag_path: '/tag/example' } }); + expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true); }); it('renders release date', () => { diff --git a/spec/javascripts/ide/components/file_templates/dropdown_spec.js b/spec/javascripts/ide/components/file_templates/dropdown_spec.js deleted file mode 100644 index 898796f4fa0..00000000000 --- a/spec/javascripts/ide/components/file_templates/dropdown_spec.js +++ /dev/null @@ -1,201 +0,0 @@ -import $ from 'jquery'; -import Vue from 'vue'; -import { createStore } from '~/ide/stores'; -import Dropdown from '~/ide/components/file_templates/dropdown.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; - -describe('IDE file templates dropdown component', () => { - let Component; - let vm; - - beforeAll(() => { - Component = Vue.extend(Dropdown); - }); - - beforeEach(() => { - const store = createStore(); - - vm = createComponentWithStore(Component, store, { - label: 'Test', - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - resetStore(vm.$store); - }); - - describe('async', () => { - beforeEach(() => { - vm.isAsyncData = true; - }); - - it('calls async store method on Bootstrap dropdown event', () => { - spyOn(vm, 'fetchTemplateTypes').and.stub(); - - $(vm.$el).trigger('show.bs.dropdown'); - - expect(vm.fetchTemplateTypes).toHaveBeenCalled(); - }); - - it('renders templates when async', done => { - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test'); - - done(); - }); - }); - - it('renders loading icon when isLoading is true', done => { - vm.$store.state.fileTemplates.isLoading = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.loading-container')).not.toBe(null); - - done(); - }); - }); - - it('searches template data', () => { - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - vm.searchable = true; - vm.search = 'hello'; - - expect(vm.outputData).toEqual([]); - }); - - it('does not filter data is searchable is false', () => { - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - vm.search = 'hello'; - - expect(vm.outputData).toEqual([ - { - name: 'test', - }, - ]); - }); - - it('calls clickItem on click', done => { - spyOn(vm, 'clickItem').and.stub(); - - vm.$store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - - vm.$nextTick(() => { - vm.$el.querySelector('.dropdown-content button').click(); - - expect(vm.clickItem).toHaveBeenCalledWith({ - name: 'test', - }); - - done(); - }); - }); - - it('renders input when searchable is true', done => { - vm.searchable = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); - - done(); - }); - }); - - it('does not render input when searchable is true & showLoading is true', done => { - vm.searchable = true; - vm.$store.state.fileTemplates.isLoading = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input')).toBe(null); - - done(); - }); - }); - }); - - describe('sync', () => { - beforeEach(done => { - vm.data = [ - { - name: 'test sync', - }, - ]; - - vm.$nextTick(done); - }); - - it('renders props data', () => { - expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync'); - }); - - it('renders input when searchable is true', done => { - vm.searchable = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); - - done(); - }); - }); - - it('calls clickItem on click', done => { - spyOn(vm, 'clickItem').and.stub(); - - vm.$nextTick(() => { - vm.$el.querySelector('.dropdown-content button').click(); - - expect(vm.clickItem).toHaveBeenCalledWith({ - name: 'test sync', - }); - - done(); - }); - }); - - it('searches template data', () => { - vm.searchable = true; - vm.search = 'hello'; - - expect(vm.outputData).toEqual([]); - }); - - it('does not filter data is searchable is false', () => { - vm.search = 'hello'; - - expect(vm.outputData).toEqual([ - { - name: 'test sync', - }, - ]); - }); - - it('renders dropdown title', done => { - vm.title = 'Test title'; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title'); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js deleted file mode 100644 index 11a385fa64d..00000000000 --- a/spec/javascripts/releases/components/release_block_spec.js +++ /dev/null @@ -1,170 +0,0 @@ -import Vue from 'vue'; -import component from '~/releases/components/release_block.vue'; -import timeagoMixin from '~/vue_shared/mixins/timeago'; - -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('Release block', () => { - const Component = Vue.extend(component); - - const release = { - name: 'Bionic Beaver', - tag_name: '18.04', - description: '## changelog\n\n* line 1\n* line2', - description_html: '

changelog

', - author_name: 'Release bot', - author_email: 'release-bot@example.com', - released_at: '2012-05-28T05:00:00-07:00', - author: { - avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', - id: 482476, - name: 'John Doe', - path: '/johndoe', - state: 'active', - status_tooltip_html: null, - username: 'johndoe', - web_url: 'https://gitlab.com/johndoe', - }, - commit: { - id: '2695effb5807a22ff3d138d593fd856244e155e7', - short_id: '2695effb', - title: 'Initial commit', - created_at: '2017-07-26T11:08:53.000+02:00', - parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'], - message: 'Initial commit', - author_name: 'John Smith', - author_email: 'john@example.com', - authored_date: '2012-05-28T04:42:42-07:00', - committer_name: 'Jack Smith', - committer_email: 'jack@example.com', - committed_date: '2012-05-28T04:42:42-07:00', - }, - assets: { - count: 6, - sources: [ - { - format: 'zip', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip', - }, - { - format: 'tar.gz', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz', - }, - { - format: 'tar.bz2', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2', - }, - { - format: 'tar', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar', - }, - ], - links: [ - { - name: 'release-18.04.dmg', - url: 'https://my-external-hosting.example.com/scrambled-url/', - external: true, - }, - { - name: 'binary-linux-amd64', - url: - 'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50', - external: false, - }, - ], - }, - }; - let vm; - - const factory = props => mountComponent(Component, { release: props }); - - beforeEach(() => { - vm = factory(release); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it("renders the block with an id equal to the release's tag name", () => { - expect(vm.$el.id).toBe('18.04'); - }); - - it('renders release name', () => { - expect(vm.$el.textContent).toContain(release.name); - }); - - it('renders commit sha', () => { - expect(vm.$el.textContent).toContain(release.commit.short_id); - }); - - it('renders tag name', () => { - expect(vm.$el.textContent).toContain(release.tag_name); - }); - - it('renders release date', () => { - expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at)); - }); - - it('renders number of assets provided', () => { - expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count); - }); - - it('renders dropdown with the sources', () => { - expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual( - release.assets.sources.length, - ); - - expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual( - release.assets.sources[0].url, - ); - - expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain( - release.assets.sources[0].format, - ); - }); - - it('renders list with the links provided', () => { - expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual( - release.assets.links.length, - ); - - expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual( - release.assets.links[0].url, - ); - - expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain( - release.assets.links[0].name, - ); - }); - - it('renders author avatar', () => { - expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull(); - }); - - describe('external label', () => { - it('renders external label when link is external', () => { - expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain('external source'); - }); - - it('does not render external label when link is not external', () => { - expect(vm.$el.querySelector('.js-assets-list li:nth-child(2) a').textContent).not.toContain( - 'external source', - ); - }); - }); - - describe('with upcoming_release flag', () => { - beforeEach(() => { - vm = factory(Object.assign({}, release, { upcoming_release: true })); - }); - - it('renders upcoming release badge', () => { - expect(vm.$el.textContent).toContain('Upcoming Release'); - }); - }); -}); diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index fee7ffc60ee..35594cd2fb8 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -21,6 +21,49 @@ describe Backup::Manager do $progress = @old_progress # rubocop:disable Style/GlobalVars end + describe '#pack' do + let(:backup_contents) { ['backup_contents'] } + let(:tar_system_options) { { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] } } + let(:tar_cmdline) { ['tar', '-cf', '-', *backup_contents, tar_system_options] } + + let(:backup_information) do + { + backup_created_at: Time.zone.parse('2019-01-01'), + gitlab_version: '12.3' + } + end + + before do + allow(ActiveRecord::Base.connection).to receive(:reconnect!) + allow(Kernel).to receive(:system).and_return(true) + + allow(subject).to receive(:backup_contents).and_return(backup_contents) + allow(subject).to receive(:backup_information).and_return(backup_information) + allow(subject).to receive(:upload) + end + + context 'when BACKUP is not set' do + let(:tar_file) { '1546300800_2019_01_01_12.3_gitlab_backup.tar' } + + it 'uses the default tar file name' do + subject.pack + + expect(Kernel).to have_received(:system).with(*tar_cmdline) + end + end + + context 'when BACKUP is set' do + let(:tar_file) { 'custom_gitlab_backup.tar' } + + it 'uses the given value as tar file name' do + stub_env('BACKUP', '/ignored/path/custom') + subject.pack + + expect(Kernel).to have_received(:system).with(*tar_cmdline) + end + end + end + describe '#remove_old' do let(:files) do [ @@ -238,7 +281,7 @@ describe Backup::Manager do allow(Kernel).to receive(:system).and_return(true) allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) - stub_env('BACKUP', '1451606400_2016_01_01_1.2.3') + stub_env('BACKUP', '/ignored/path/1451606400_2016_01_01_1.2.3') end it 'unpacks the file' do diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb index cd932f502f3..b5be204d680 100644 --- a/spec/lib/banzai/filter/video_link_filter_spec.rb +++ b/spec/lib/banzai/filter/video_link_filter_spec.rb @@ -18,7 +18,7 @@ describe Banzai::Filter::VideoLinkFilter do let(:project) { create(:project, :repository) } context 'when the element src has a video extension' do - UploaderHelper::VIDEO_EXT.each do |ext| + UploaderHelper::SAFE_VIDEO_EXT.each do |ext| it "replaces the image tag 'path/video.#{ext}' with a video tag" do container = filter(link_to_image("/path/video.#{ext}")).children.first diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 399787635c0..839780b53fe 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -130,6 +130,26 @@ describe Gitlab::Diff::Position do expect(diff_file.new_path).to eq(subject.new_path) expect(diff_file.diff_refs).to eq(subject.diff_refs) end + + context 'different folded positions in the same diff file' do + def diff_file(args = {}) + described_class + .new(args_for_text.merge(args)) + .diff_file(project.repository) + end + + it 'expands the diff file', :request_store do + expect_any_instance_of(Gitlab::Diff::File) + .to receive(:unfold_diff_lines).and_call_original + + diff_file(old_line: 1, new_line: 1, diff_refs: commit.diff_refs) + + expect_any_instance_of(Gitlab::Diff::File) + .to receive(:unfold_diff_lines).and_call_original + + diff_file(old_line: 5, new_line: 5, diff_refs: commit.diff_refs) + end + end end describe "#diff_line" do diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb index 22ec7d414e8..1edf882afe2 100644 --- a/spec/lib/gitlab/file_type_detection_spec.rb +++ b/spec/lib/gitlab/file_type_detection_spec.rb @@ -2,38 +2,103 @@ require 'spec_helper' describe Gitlab::FileTypeDetection do - def upload_fixture(filename) - fixture_file_upload(File.join('spec', 'fixtures', filename)) - end + context 'when class is an uploader' do + shared_examples '#image? for an uploader' do + it 'returns true for an image file' do + uploader.store!(upload_fixture('dk.png')) - describe '#image_or_video?' do - context 'when class is an uploader' do - let(:uploader) do - example_uploader = Class.new(CarrierWave::Uploader::Base) do - include Gitlab::FileTypeDetection + expect(uploader).to be_image + end - storage :file - end + it 'returns false if filename has a dangerous image extension' do + uploader.store!(upload_fixture('unsanitized.svg')) - example_uploader.new + expect(uploader).to be_dangerous_image + expect(uploader).not_to be_image end - it 'returns true for an image file' do + it 'returns false for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).not_to be_image + end + + it 'returns false if filename is blank' do uploader.store!(upload_fixture('dk.png')) - expect(uploader).to be_image_or_video + allow(uploader).to receive(:filename).and_return(nil) + + expect(uploader).not_to be_image end + end + shared_examples '#video? for an uploader' do it 'returns true for a video file' do uploader.store!(upload_fixture('video_sample.mp4')) - expect(uploader).to be_image_or_video + expect(uploader).to be_video + end + + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).not_to be_video + end + + it 'returns false if filename is blank' do + uploader.store!(upload_fixture('dk.png')) + + allow(uploader).to receive(:filename).and_return(nil) + + expect(uploader).not_to be_video + end + end + + shared_examples '#dangerous_image? for an uploader' do + it 'returns true if filename has a dangerous extension' do + uploader.store!(upload_fixture('unsanitized.svg')) + + expect(uploader).to be_dangerous_image + end + + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).not_to be_dangerous_image + end + + it 'returns false for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).not_to be_dangerous_image + end + + it 'returns false if filename is blank' do + uploader.store!(upload_fixture('dk.png')) + + allow(uploader).to receive(:filename).and_return(nil) + + expect(uploader).not_to be_dangerous_image + end + end + + shared_examples '#dangerous_video? for an uploader' do + it 'returns false for a safe video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).not_to be_dangerous_video + end + + it 'returns false if filename is a dangerous image extension' do + uploader.store!(upload_fixture('unsanitized.svg')) + + expect(uploader).not_to be_dangerous_video end - it 'returns false for other extensions' do - uploader.store!(upload_fixture('doc_sample.txt')) + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) - expect(uploader).not_to be_image_or_video + expect(uploader).not_to be_dangerous_video end it 'returns false if filename is blank' do @@ -41,42 +106,190 @@ describe Gitlab::FileTypeDetection do allow(uploader).to receive(:filename).and_return(nil) - expect(uploader).not_to be_image_or_video + expect(uploader).not_to be_dangerous_video end end - context 'when class is a regular class' do - let(:custom_class) do - custom_class = Class.new do - include Gitlab::FileTypeDetection - end + let(:uploader) do + example_uploader = Class.new(CarrierWave::Uploader::Base) do + include Gitlab::FileTypeDetection - custom_class.new + storage :file end + example_uploader.new + end + + def upload_fixture(filename) + fixture_file_upload(File.join('spec', 'fixtures', filename)) + end + + describe '#image?' do + include_examples '#image? for an uploader' + end + + describe '#video?' do + include_examples '#video? for an uploader' + end + + describe '#image_or_video?' do + include_examples '#image? for an uploader' + include_examples '#video? for an uploader' + end + + describe '#dangerous_image?' do + include_examples '#dangerous_image? for an uploader' + end + + describe '#dangerous_video?' do + include_examples '#dangerous_video? for an uploader' + end + + describe '#dangerous_image_or_video?' do + include_examples '#dangerous_image? for an uploader' + include_examples '#dangerous_video? for an uploader' + end + end + + context 'when class is a regular class' do + shared_examples '#image? for a regular class' do it 'returns true for an image file' do allow(custom_class).to receive(:filename).and_return('dk.png') - expect(custom_class).to be_image_or_video + expect(custom_class).to be_image end + it 'returns false if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') + + expect(custom_class).to be_dangerous_image + expect(custom_class).not_to be_image + end + + it 'returns false for any non image file' do + allow(custom_class).to receive(:filename).and_return('video_sample.mp4') + + expect(custom_class).not_to be_image + end + + it 'returns false if filename is blank' do + allow(custom_class).to receive(:filename).and_return(nil) + + expect(custom_class).not_to be_image + end + end + + shared_examples '#video? for a regular class' do it 'returns true for a video file' do allow(custom_class).to receive(:filename).and_return('video_sample.mp4') - expect(custom_class).to be_image_or_video + expect(custom_class).to be_video + end + + it 'returns false for any non-video file' do + allow(custom_class).to receive(:filename).and_return('dk.png') + + expect(custom_class).not_to be_video + end + + it 'returns false if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') + + expect(custom_class).to be_dangerous_image + expect(custom_class).not_to be_video + end + + it 'returns false if filename is blank' do + allow(custom_class).to receive(:filename).and_return(nil) + + expect(custom_class).not_to be_video + end + end + + shared_examples '#dangerous_image? for a regular class' do + it 'returns true if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') + + expect(custom_class).to be_dangerous_image + end + + it 'returns false for an image file' do + allow(custom_class).to receive(:filename).and_return('dk.png') + + expect(custom_class).not_to be_dangerous_image + end + + it 'returns false for any non image file' do + allow(custom_class).to receive(:filename).and_return('video_sample.mp4') + + expect(custom_class).not_to be_dangerous_image + end + + it 'returns false if filename is blank' do + allow(custom_class).to receive(:filename).and_return(nil) + + expect(custom_class).not_to be_dangerous_image + end + end + + shared_examples '#dangerous_video? for a regular class' do + it 'returns false for a safe video file' do + allow(custom_class).to receive(:filename).and_return('video_sample.mp4') + + expect(custom_class).not_to be_dangerous_video + end + + it 'returns false for an image file' do + allow(custom_class).to receive(:filename).and_return('dk.png') + + expect(custom_class).not_to be_dangerous_video end - it 'returns false for other extensions' do - allow(custom_class).to receive(:filename).and_return('doc_sample.txt') + it 'returns false if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') - expect(custom_class).not_to be_image_or_video + expect(custom_class).not_to be_dangerous_video end it 'returns false if filename is blank' do allow(custom_class).to receive(:filename).and_return(nil) - expect(custom_class).not_to be_image_or_video + expect(custom_class).not_to be_dangerous_video end end + + let(:custom_class) do + custom_class = Class.new do + include Gitlab::FileTypeDetection + end + + custom_class.new + end + + describe '#image?' do + include_examples '#image? for a regular class' + end + + describe '#video?' do + include_examples '#video? for a regular class' + end + + describe '#image_or_video?' do + include_examples '#image? for a regular class' + include_examples '#video? for a regular class' + end + + describe '#dangerous_image?' do + include_examples '#dangerous_image? for a regular class' + end + + describe '#dangerous_video?' do + include_examples '#dangerous_video? for a regular class' + end + + describe '#dangerous_image_or_video?' do + include_examples '#dangerous_image? for a regular class' + include_examples '#dangerous_video? for a regular class' + end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 842dc4d511c..e09390a0047 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -64,31 +64,29 @@ describe Gitlab::UsageData do avg_cycle_analytics influxdb_metrics_enabled prometheus_metrics_enabled - cycle_analytics_views - productivity_analytics_views )) - - expect(subject).to include( - snippet_create: a_kind_of(Integer), - snippet_update: a_kind_of(Integer), - snippet_comment: a_kind_of(Integer), - merge_request_comment: a_kind_of(Integer), - merge_request_create: a_kind_of(Integer), - commit_comment: a_kind_of(Integer), - wiki_pages_create: a_kind_of(Integer), - wiki_pages_update: a_kind_of(Integer), - wiki_pages_delete: a_kind_of(Integer), - web_ide_views: a_kind_of(Integer), - web_ide_commits: a_kind_of(Integer), - web_ide_merge_requests: a_kind_of(Integer), - navbar_searches: a_kind_of(Integer), - cycle_analytics_views: a_kind_of(Integer), - productivity_analytics_views: a_kind_of(Integer), - source_code_pushes: a_kind_of(Integer) - ) end it 'gathers usage counts' do + smau_keys = %i( + snippet_create + snippet_update + snippet_comment + merge_request_comment + merge_request_create + commit_comment + wiki_pages_create + wiki_pages_update + wiki_pages_delete + web_ide_views + web_ide_commits + web_ide_merge_requests + navbar_searches + cycle_analytics_views + productivity_analytics_views + source_code_pushes + ) + expected_keys = %i( assignee_lists boards @@ -154,12 +152,13 @@ describe Gitlab::UsageData do uploads web_hooks user_preferences - ) + ).push(*smau_keys) count_data = subject[:counts] expect(count_data[:boards]).to eq(1) expect(count_data[:projects]).to eq(4) + expect(count_data.values_at(*smau_keys)).to all(be_an(Integer)) expect(count_data.keys).to include(*expected_keys) expect(expected_keys - count_data.keys).to be_empty end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 011b46c7f1a..28be8056993 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1223,36 +1223,66 @@ describe Repository do end describe '#branch_exists?' do - it 'uses branch_names' do - allow(repository).to receive(:branch_names).and_return(['foobar']) + let(:branch) { repository.root_ref } - expect(repository.branch_exists?('foobar')).to eq(true) - expect(repository.branch_exists?('master')).to eq(false) + subject { repository.branch_exists?(branch) } + + it 'delegates to branch_names when the cache is empty' do + repository.expire_branches_cache + + expect(repository).to receive(:branch_names).and_call_original + is_expected.to eq(true) + end + + it 'uses redis set caching when the cache is filled' do + repository.branch_names # ensure the branch name cache is filled + + expect(repository) + .to receive(:branch_names_include?) + .with(branch) + .and_call_original + + is_expected.to eq(true) end end describe '#tag_exists?' do - it 'uses tag_names' do - allow(repository).to receive(:tag_names).and_return(['foobar']) + let(:tag) { repository.tags.first.name } + + subject { repository.tag_exists?(tag) } + + it 'delegates to tag_names when the cache is empty' do + repository.expire_tags_cache + + expect(repository).to receive(:tag_names).and_call_original + is_expected.to eq(true) + end + + it 'uses redis set caching when the cache is filled' do + repository.tag_names # ensure the tag name cache is filled + + expect(repository) + .to receive(:tag_names_include?) + .with(tag) + .and_call_original - expect(repository.tag_exists?('foobar')).to eq(true) - expect(repository.tag_exists?('master')).to eq(false) + is_expected.to eq(true) end end - describe '#branch_names', :use_clean_rails_memory_store_caching do + describe '#branch_names', :clean_gitlab_redis_cache do let(:fake_branch_names) { ['foobar'] } it 'gets cached across Repository instances' do allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names) - expect(repository.branch_names).to eq(fake_branch_names) + expect(repository.branch_names).to match_array(fake_branch_names) fresh_repository = Project.find(project.id).repository expect(fresh_repository.object_id).not_to eq(repository.object_id) expect(fresh_repository.raw_repository).not_to receive(:branch_names) - expect(fresh_repository.branch_names).to eq(fake_branch_names) + expect(fresh_repository.branch_names).to match_array(fake_branch_names) end end diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb index 8d4e9070b19..2ac3ae0a5ad 100644 --- a/spec/models/suggestion_spec.rb +++ b/spec/models/suggestion_spec.rb @@ -38,16 +38,6 @@ describe Suggestion do end describe '#appliable?' do - context 'when note does not support suggestions' do - it 'returns false' do - expect_next_instance_of(DiffNote) do |note| - allow(note).to receive(:supports_suggestion?) { false } - end - - expect(suggestion).not_to be_appliable - end - end - context 'when patch is already applied' do let(:suggestion) { create(:suggestion, :applied) } diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 206e898381d..0bb238d08c0 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -54,6 +54,15 @@ describe API::Releases do expect(response).to match_response_schema('public_api/v4/releases') end + + it 'returns rendered helper paths' do + get api("/projects/#{project.id}/releases", maintainer) + + expect(json_response.first['commit_path']).to eq("/#{release_2.project.full_path}/commit/#{release_2.commit.id}") + expect(json_response.first['tag_path']).to eq("/#{release_2.project.full_path}/-/tags/#{release_2.tag}") + expect(json_response.second['commit_path']).to eq("/#{release_1.project.full_path}/commit/#{release_1.commit.id}") + expect(json_response.second['tag_path']).to eq("/#{release_1.project.full_path}/-/tags/#{release_1.tag}") + end end it 'returns an upcoming_release status for a future release' do @@ -103,11 +112,13 @@ describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end - it "does not expose tag, commit and source code" do + it "does not expose tag, commit, source code or helper paths" do get api("/projects/#{project.id}/releases", guest) expect(response).to match_response_schema('public_api/v4/release/releases_for_guest') expect(json_response[0]['assets']['count']).to eq(release.links.count) + expect(json_response[0]['commit_path']).to be_nil + expect(json_response[0]['tag_path']).to be_nil end context 'when project is public' do @@ -119,11 +130,13 @@ describe API::Releases do expect(response).to have_gitlab_http_status(:ok) end - it "exposes tag, commit and source code" do + it "exposes tag, commit, source code and helper paths" do get api("/projects/#{project.id}/releases", guest) expect(response).to match_response_schema('public_api/v4/releases') - expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count) + expect(json_response.first['assets']['count']).to eq(release.links.count + release.sources.count) + expect(json_response.first['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}") + expect(json_response.first['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}") end end end @@ -172,6 +185,8 @@ describe API::Releases do expect(json_response['author']['name']).to eq(maintainer.name) expect(json_response['commit']['id']).to eq(commit.id) expect(json_response['assets']['count']).to eq(4) + expect(json_response['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}") + expect(json_response['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}") end it 'matches response schema' do diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb index b5defba332a..255a15b1ab0 100644 --- a/spec/support/helpers/repo_helpers.rb +++ b/spec/support/helpers/repo_helpers.rb @@ -92,7 +92,7 @@ eos ) end - def sample_compare + def sample_compare(extra_changes = []) changes = [ { line_code: 'a5cc2925ca8258af241be7e5b0381edf30266302_20_20', @@ -102,7 +102,7 @@ eos line_code: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_6', file_path: '.gitmodules' } - ] + ] + extra_changes commits = %w( 5937ac0a7beb003549fc5fd26fc247adbce4a52e -- cgit v1.2.3