diff options
Diffstat (limited to 'spec')
17 files changed, 836 insertions, 296 deletions
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index d372a94db56..ee145a62b57 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -445,4 +445,64 @@ describe Projects::SnippetsController do end end end + + describe 'DELETE #destroy' do + let!(:snippet) { create(:project_snippet, :private, project: project, author: user) } + + let(:params) do + { + namespace_id: project.namespace.to_param, + project_id: project, + id: snippet.to_param + } + end + + context 'when current user has ability to destroy the snippet' do + before do + sign_in(user) + end + + it 'removes the snippet' do + delete :destroy, params: params + + expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when snippet is succesfuly destroyed' do + it 'redirects to the project snippets page' do + delete :destroy, params: params + + expect(response).to redirect_to(project_snippets_path(project)) + end + end + + context 'when snippet is not destroyed' do + before do + allow(snippet).to receive(:destroy).and_return(false) + controller.instance_variable_set(:@snippet, snippet) + end + + it 'renders the snippet page with errors' do + delete :destroy, params: params + + expect(flash[:alert]).to eq('Failed to remove snippet.') + expect(response).to redirect_to(project_snippet_path(project, snippet)) + end + end + end + + context 'when current_user does not have ability to destroy the snippet' do + let(:another_user) { create(:user) } + + before do + sign_in(another_user) + end + + it 'responds with status 404' do + delete :destroy, params: params + + expect(response).to have_gitlab_http_status(404) + end + end + end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 510db4374c0..c8f9e4256c9 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -664,4 +664,56 @@ describe SnippetsController do expect(json_response.keys).to match_array(%w(body references)) end end + + describe 'DELETE #destroy' do + let!(:snippet) { create :personal_snippet, author: user } + + context 'when current user has ability to destroy the snippet' do + before do + sign_in(user) + end + + it 'removes the snippet' do + delete :destroy, params: { id: snippet.to_param } + + expect { snippet.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when snippet is succesfuly destroyed' do + it 'redirects to the project snippets page' do + delete :destroy, params: { id: snippet.to_param } + + expect(response).to redirect_to(dashboard_snippets_path) + end + end + + context 'when snippet is not destroyed' do + before do + allow(snippet).to receive(:destroy).and_return(false) + controller.instance_variable_set(:@snippet, snippet) + end + + it 'renders the snippet page with errors' do + delete :destroy, params: { id: snippet.to_param } + + expect(flash[:alert]).to eq('Failed to remove snippet.') + expect(response).to redirect_to(snippet_path(snippet)) + end + end + end + + context 'when current_user does not have ability to destroy the snippet' do + let(:another_user) { create(:user) } + + before do + sign_in(another_user) + end + + it 'responds with status 404' do + delete :destroy, params: { id: snippet.to_param } + + expect(response).to have_gitlab_http_status(404) + end + end + end end diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js new file mode 100644 index 00000000000..3bc89996978 --- /dev/null +++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js @@ -0,0 +1,167 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { createStore } from '~/ide/stores'; +import paneModule from '~/ide/stores/modules/pane'; +import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue'; +import Vuex from 'vuex'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('ide/components/panes/collapsible_sidebar.vue', () => { + let wrapper; + let store; + + const width = 350; + const fakeComponentName = 'fake-component'; + + const createComponent = props => { + wrapper = shallowMount(CollapsibleSidebar, { + localVue, + store, + propsData: { + extensionTabs: [], + side: 'right', + width, + ...props, + }, + slots: { + 'header-icon': '<div class=".header-icon-slot">SLOT ICON</div>', + header: '<div class=".header-slot"/>', + footer: '<div class=".footer-slot"/>', + }, + }); + }; + + const findTabButton = () => wrapper.find(`[data-qa-selector="${fakeComponentName}_tab_button"]`); + + beforeEach(() => { + store = createStore(); + store.registerModule('leftPane', paneModule()); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('with a tab', () => { + let fakeView; + let extensionTabs; + + beforeEach(() => { + const FakeComponent = localVue.component(fakeComponentName, { + render: () => {}, + }); + + fakeView = { + name: fakeComponentName, + keepAlive: true, + component: FakeComponent, + }; + + extensionTabs = [ + { + show: true, + title: fakeComponentName, + views: [fakeView], + icon: 'text-description', + buttonClasses: ['button-class-1', 'button-class-2'], + }, + ]; + }); + + describe.each` + side + ${'left'} + ${'right'} + `('when side=$side', ({ side }) => { + it('correctly renders side specific attributes', () => { + createComponent({ extensionTabs, side }); + const button = findTabButton(); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.classes()).toContain('multi-file-commit-panel'); + expect(wrapper.classes()).toContain(`ide-${side}-sidebar`); + expect(wrapper.find('.multi-file-commit-panel-inner')).not.toBe(null); + expect(wrapper.find(`.ide-${side}-sidebar-${fakeComponentName}`)).not.toBe(null); + expect(button.attributes('data-placement')).toEqual(side === 'left' ? 'right' : 'left'); + if (side === 'right') { + // this class is only needed on the right side; there is no 'is-left' + expect(button.classes()).toContain('is-right'); + } else { + expect(button.classes()).not.toContain('is-right'); + } + }); + }); + }); + + describe('when default side', () => { + let button; + + beforeEach(() => { + createComponent({ extensionTabs }); + + button = findTabButton(); + }); + + it('correctly renders tab-specific classes', () => { + store.state.rightPane.currentView = fakeComponentName; + + return wrapper.vm.$nextTick().then(() => { + expect(button.classes()).toContain('button-class-1'); + expect(button.classes()).toContain('button-class-2'); + }); + }); + + it('can show an open pane tab with an active view', () => { + store.state.rightPane.isOpen = true; + store.state.rightPane.currentView = fakeComponentName; + + return wrapper.vm.$nextTick().then(() => { + expect(button.classes()).toEqual(expect.arrayContaining(['ide-sidebar-link', 'active'])); + expect(button.attributes('data-original-title')).toEqual(fakeComponentName); + expect(wrapper.find('.js-tab-view').exists()).toBe(true); + }); + }); + + it('does not show a pane which is not open', () => { + store.state.rightPane.isOpen = false; + store.state.rightPane.currentView = fakeComponentName; + + return wrapper.vm.$nextTick().then(() => { + expect(button.classes()).not.toEqual( + expect.arrayContaining(['ide-sidebar-link', 'active']), + ); + expect(wrapper.find('.js-tab-view').exists()).toBe(false); + }); + }); + + describe('when button is clicked', () => { + it('opens view', () => { + button.trigger('click'); + expect(store.state.rightPane.isOpen).toBeTruthy(); + }); + + it('toggles open view if tab is currently active', () => { + button.trigger('click'); + expect(store.state.rightPane.isOpen).toBeTruthy(); + + button.trigger('click'); + expect(store.state.rightPane.isOpen).toBeFalsy(); + }); + }); + + it('shows header-icon', () => { + expect(wrapper.find('.header-icon-slot')).not.toBeNull(); + }); + + it('shows header', () => { + expect(wrapper.find('.header-slot')).not.toBeNull(); + }); + + it('shows footer', () => { + expect(wrapper.find('.footer-slot')).not.toBeNull(); + }); + }); + }); +}); diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js index 6908790aaa8..7e408be96fc 100644 --- a/spec/frontend/ide/components/panes/right_spec.js +++ b/spec/frontend/ide/components/panes/right_spec.js @@ -1,89 +1,124 @@ import Vue from 'vue'; -import '~/behaviors/markdown/render_gfm'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createStore } from '~/ide/stores'; import RightPane from '~/ide/components/panes/right.vue'; +import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue'; import { rightSidebarViews } from '~/ide/constants'; -describe('IDE right pane', () => { - let Component; - let vm; +const localVue = createLocalVue(); +localVue.use(Vuex); - beforeAll(() => { - Component = Vue.extend(RightPane); - }); +describe('ide/components/panes/right.vue', () => { + let wrapper; + let store; - beforeEach(() => { - const store = createStore(); + const createComponent = props => { + wrapper = shallowMount(RightPane, { + localVue, + store, + propsData: { + ...props, + }, + }); + }; - vm = createComponentWithStore(Component, store).$mount(); + beforeEach(() => { + store = createStore(); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); - describe('active', () => { - it('renders merge request button as active', done => { - vm.$store.state.rightPane.isOpen = true; - vm.$store.state.rightPane.currentView = rightSidebarViews.mergeRequestInfo.name; - vm.$store.state.currentMergeRequestId = '123'; - vm.$store.state.currentProjectId = 'gitlab-ce'; - vm.$store.state.currentMergeRequestId = 1; - vm.$store.state.projects['gitlab-ce'] = { - mergeRequests: { - 1: { - iid: 1, - title: 'Testing', - title_html: '<span class="title-html">Testing</span>', - description: 'Description', - description_html: '<p class="description-html">Description HTML</p>', - }, + it('allows tabs to be added via extensionTabs prop', () => { + createComponent({ + extensionTabs: [ + { + show: true, + title: 'FakeTab', }, - }; - - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null); - expect( - vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'), - ).toBe('Merge Request'); - }) - .then(done) - .catch(done.fail); + ], }); + + expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + show: true, + title: 'FakeTab', + }), + ]), + ); }); - describe('click', () => { - beforeEach(() => { - jest.spyOn(vm, 'open').mockReturnValue(); - }); + describe('pipelines tab', () => { + it('is always shown', () => { + createComponent(); - it('sets view to merge request', done => { - vm.$store.state.currentMergeRequestId = '123'; + expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + show: true, + title: 'Pipelines', + views: expect.arrayContaining([ + expect.objectContaining({ + name: rightSidebarViews.pipelines.name, + }), + expect.objectContaining({ + name: rightSidebarViews.jobsDetail.name, + }), + ]), + }), + ]), + ); + }); + }); - vm.$nextTick(() => { - vm.$el.querySelector('.ide-sidebar-link').click(); + describe('merge request tab', () => { + it('is shown if there is a currentMergeRequestId', () => { + store.state.currentMergeRequestId = 1; - expect(vm.open).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo); + createComponent(); - done(); - }); + expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + show: true, + title: 'Merge Request', + views: expect.arrayContaining([ + expect.objectContaining({ + name: rightSidebarViews.mergeRequestInfo.name, + }), + ]), + }), + ]), + ); }); }); - describe('live preview', () => { - it('renders live preview button', done => { - Vue.set(vm.$store.state.entries, 'package.json', { + describe('clientside live preview tab', () => { + it('is shown if there is a packageJson and clientsidePreviewEnabled', () => { + Vue.set(store.state.entries, 'package.json', { name: 'package.json', }); - vm.$store.state.clientsidePreviewEnabled = true; + store.state.clientsidePreviewEnabled = true; - vm.$nextTick(() => { - expect(vm.$el.querySelector('button[aria-label="Live preview"]')).not.toBeNull(); + createComponent(); - done(); - }); + expect(wrapper.find(CollapsibleSidebar).props('extensionTabs')).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + show: true, + title: 'Live preview', + views: expect.arrayContaining([ + expect.objectContaining({ + name: rightSidebarViews.clientSidePreview.name, + }), + ]), + }), + ]), + ); }); }); }); diff --git a/spec/frontend/ide/stores/modules/pane/actions_spec.js b/spec/frontend/ide/stores/modules/pane/actions_spec.js index 8c0aeaff5b3..8c56714e0ed 100644 --- a/spec/frontend/ide/stores/modules/pane/actions_spec.js +++ b/spec/frontend/ide/stores/modules/pane/actions_spec.js @@ -8,14 +8,7 @@ describe('IDE pane module actions', () => { describe('toggleOpen', () => { it('dispatches open if closed', done => { - testAction( - actions.toggleOpen, - TEST_VIEW, - { isOpen: false }, - [], - [{ type: 'open', payload: TEST_VIEW }], - done, - ); + testAction(actions.toggleOpen, TEST_VIEW, { isOpen: false }, [], [{ type: 'open' }], done); }); it('dispatches close if opened', done => { @@ -24,37 +17,48 @@ describe('IDE pane module actions', () => { }); describe('open', () => { - it('commits SET_OPEN', done => { - testAction(actions.open, null, {}, [{ type: types.SET_OPEN, payload: true }], [], done); - }); + describe('with a view specified', () => { + it('commits SET_OPEN and SET_CURRENT_VIEW', done => { + testAction( + actions.open, + TEST_VIEW, + {}, + [ + { type: types.SET_OPEN, payload: true }, + { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW.name }, + ], + [], + done, + ); + }); - it('commits SET_CURRENT_VIEW if view is given', done => { - testAction( - actions.open, - TEST_VIEW, - {}, - [ - { type: types.SET_OPEN, payload: true }, - { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW.name }, - ], - [], - done, - ); + it('commits KEEP_ALIVE_VIEW if keepAlive is true', done => { + testAction( + actions.open, + TEST_VIEW_KEEP_ALIVE, + {}, + [ + { type: types.SET_OPEN, payload: true }, + { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, + { type: types.KEEP_ALIVE_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, + ], + [], + done, + ); + }); }); - it('commits KEEP_ALIVE_VIEW if keepAlive is true', done => { - testAction( - actions.open, - TEST_VIEW_KEEP_ALIVE, - {}, - [ - { type: types.SET_OPEN, payload: true }, - { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, - { type: types.KEEP_ALIVE_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, - ], - [], - done, - ); + describe('without a view specified', () => { + it('commits SET_OPEN', done => { + testAction( + actions.open, + undefined, + {}, + [{ type: types.SET_OPEN, payload: true }], + [], + done, + ); + }); }); }); diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js index cbb1de4d87a..43da6388efa 100644 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ b/spec/frontend/pipelines/graph/action_component_spec.js @@ -19,7 +19,6 @@ describe('pipeline graph action component', () => { link: 'foo', actionIcon: 'cancel', }, - attachToDocument: true, }); }); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index abeb538e390..0c64d5c9fa8 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -7,7 +7,6 @@ describe('pipeline graph job item', () => { const createWrapper = propsData => { wrapper = mount(JobItem, { - attachToDocument: true, propsData, }); }; diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index de1f9142001..7f49b21100d 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -10,7 +10,6 @@ describe('Linked pipeline', () => { const createWrapper = propsData => { wrapper = mount(LinkedPipelineComponent, { - attachToDocument: true, propsData, }); }; diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js index b633d711699..a8eec274487 100644 --- a/spec/frontend/pipelines/pipeline_triggerer_spec.js +++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js @@ -24,7 +24,6 @@ describe('Pipelines Triggerer', () => { const createComponent = () => { wrapper = shallowMount(pipelineTriggerer, { propsData: mockData, - attachToDocument: true, }); }; diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index 6587cc8b318..70b94f2c8e1 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -10,7 +10,6 @@ describe('Pipeline Url Component', () => { const createComponent = props => { wrapper = shallowMount(PipelineUrlComponent, { - attachToDocument: true, propsData: props, }); }; diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb index 0d0a2df4423..355757654da 100644 --- a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb @@ -12,6 +12,59 @@ describe Gitlab::ImportExport::GroupProjectObjectBuilder do group: create(:group)) end + let(:lru_cache) { subject.send(:lru_cache) } + let(:cache_key) { subject.send(:cache_key) } + + context 'request store is not active' do + subject do + described_class.new(Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group) + end + + it 'ignore cache initialize' do + expect(lru_cache).to be_nil + expect(cache_key).to be_nil + end + end + + context 'request store is active', :request_store do + subject do + described_class.new(Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group) + end + + it 'initialize cache in memory' do + expect(lru_cache).not_to be_nil + expect(cache_key).not_to be_nil + end + + it 'cache object when first time find the object' do + group_label = create(:group_label, name: 'group label', group: project.group) + + expect(subject).to receive(:find_object).and_call_original + expect { subject.find } + .to change { lru_cache[cache_key] } + .from(nil).to(group_label) + + expect(subject.find).to eq(group_label) + end + + it 'read from cache when object has been cached' do + group_label = create(:group_label, name: 'group label', group: project.group) + + subject.find + + expect(subject).not_to receive(:find_object) + expect { subject.find }.not_to change { lru_cache[cache_key] } + + expect(subject.find).to eq(group_label) + end + end + context 'labels' do it 'finds the existing group label' do group_label = create(:group_label, name: 'group label', group: project.group) diff --git a/spec/requests/api/error_tracking_spec.rb b/spec/requests/api/error_tracking_spec.rb index af337f34a68..48ddc7f5a75 100644 --- a/spec/requests/api/error_tracking_spec.rb +++ b/spec/requests/api/error_tracking_spec.rb @@ -22,6 +22,7 @@ describe API::ErrorTracking do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq( + 'active' => setting.enabled, 'project_name' => setting.project_name, 'sentry_external_url' => setting.sentry_external_url, 'api_url' => setting.api_url diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb deleted file mode 100644 index 1751029a78c..00000000000 --- a/spec/services/create_snippet_service_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe CreateSnippetService do - let(:user) { create(:user) } - let(:admin) { create(:user, :admin) } - let(:opts) { base_opts.merge(extra_opts) } - let(:base_opts) do - { - title: 'Test snippet', - file_name: 'snippet.rb', - content: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PRIVATE - } - end - let(:extra_opts) { {} } - - context 'When public visibility is restricted' do - let(:extra_opts) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } } - - before do - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - end - - it 'non-admins are not able to create a public snippet' do - snippet = create_snippet(nil, user, opts) - expect(snippet.errors.messages).to have_key(:visibility_level) - expect(snippet.errors.messages[:visibility_level].first).to( - match('has been restricted') - ) - end - - it 'admins are able to create a public snippet' do - snippet = create_snippet(nil, admin, opts) - expect(snippet.errors.any?).to be_falsey - expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - describe "when visibility level is passed as a string" do - let(:extra_opts) { { visibility: 'internal' } } - - before do - base_opts.delete(:visibility_level) - end - - it "assigns the correct visibility level" do - snippet = create_snippet(nil, user, opts) - expect(snippet.errors.any?).to be_falsey - expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - end - end - - context 'checking spam' do - shared_examples 'marked as spam' do - let(:snippet) { create_snippet(nil, admin, opts) } - - it 'marks a snippet as a spam ' do - expect(snippet).to be_spam - end - - it 'invalidates the snippet' do - expect(snippet).to be_invalid - end - - it 'creates a new spam_log' do - expect { snippet } - .to log_spam(title: snippet.title, noteable_type: 'PersonalSnippet') - end - - it 'assigns a spam_log to an issue' do - expect(snippet.spam_log).to eq(SpamLog.last) - end - end - - let(:extra_opts) do - { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) } - end - - before do - expect_next_instance_of(AkismetService) do |akismet_service| - expect(akismet_service).to receive_messages(spam?: true) - end - end - - [true, false, nil].each do |allow_possible_spam| - context "when recaptcha_disabled flag is #{allow_possible_spam.inspect}" do - before do - stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil? - end - - it_behaves_like 'marked as spam' - end - end - end - - describe 'usage counter' do - let(:counter) { Gitlab::UsageDataCounters::SnippetCounter } - - it 'increments count' do - expect do - create_snippet(nil, admin, opts) - end.to change { counter.read(:create) }.by 1 - end - - it 'does not increment count if create fails' do - expect do - create_snippet(nil, admin, {}) - end.not_to change { counter.read(:create) } - end - end - - def create_snippet(project, user, opts) - CreateSnippetService.new(project, user, opts).execute - end -end diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb new file mode 100644 index 00000000000..6f7ce7959ff --- /dev/null +++ b/spec/services/snippets/create_service_spec.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Snippets::CreateService do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:user, :admin) } + let(:opts) { base_opts.merge(extra_opts) } + let(:base_opts) do + { + title: 'Test snippet', + file_name: 'snippet.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PRIVATE + } + end + let(:extra_opts) { {} } + let(:creator) { admin } + + subject { Snippets::CreateService.new(project, creator, opts).execute } + + let(:snippet) { subject.payload[:snippet] } + + shared_examples 'a service that creates a snippet' do + it 'creates a snippet with the provided attributes' do + expect(snippet.title).to eq(opts[:title]) + expect(snippet.file_name).to eq(opts[:file_name]) + expect(snippet.content).to eq(opts[:content]) + expect(snippet.visibility_level).to eq(opts[:visibility_level]) + end + end + + shared_examples 'public visibility level restrictions apply' do + let(:extra_opts) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } } + + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + context 'when user is not an admin' do + let(:creator) { user } + + it 'responds with an error' do + expect(subject).to be_error + end + + it 'does not create a public snippet' do + expect(subject.message).to match('has been restricted') + end + end + + context 'when user is an admin' do + it 'responds with success' do + expect(subject).to be_success + end + + it 'creates a public snippet' do + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + describe 'when visibility level is passed as a string' do + let(:extra_opts) { { visibility: 'internal' } } + + before do + base_opts.delete(:visibility_level) + end + + it 'assigns the correct visibility level' do + expect(subject).to be_success + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end + end + + shared_examples 'spam check is performed' do + shared_examples 'marked as spam' do + it 'marks a snippet as spam ' do + expect(snippet).to be_spam + end + + it 'invalidates the snippet' do + expect(snippet).to be_invalid + end + + it 'creates a new spam_log' do + expect { snippet } + .to log_spam(title: snippet.title, noteable_type: snippet.class.name) + end + + it 'assigns a spam_log to an issue' do + expect(snippet.spam_log).to eq(SpamLog.last) + end + end + + let(:extra_opts) do + { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) } + end + + before do + expect_next_instance_of(AkismetService) do |akismet_service| + expect(akismet_service).to receive_messages(spam?: true) + end + end + + [true, false, nil].each do |allow_possible_spam| + context "when recaptcha_disabled flag is #{allow_possible_spam.inspect}" do + before do + stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil? + end + + it_behaves_like 'marked as spam' + end + end + end + + shared_examples 'snippet create data is tracked' do + let(:counter) { Gitlab::UsageDataCounters::SnippetCounter } + + it 'increments count when create succeeds' do + expect { subject }.to change { counter.read(:create) }.by 1 + end + + context 'when create fails' do + let(:opts) { {} } + + it 'does not increment count' do + expect { subject }.not_to change { counter.read(:create) } + end + end + end + + shared_examples 'an error service response when save fails' do + let(:extra_opts) { { content: nil } } + + it 'responds with an error' do + expect(subject).to be_error + end + + it 'does not create the snippet' do + expect { subject }.not_to change { Snippet.count } + end + end + + context 'when Project Snippet' do + let_it_be(:project) { create(:project) } + + before do + project.add_developer(user) + end + + it_behaves_like 'a service that creates a snippet' + it_behaves_like 'public visibility level restrictions apply' + it_behaves_like 'spam check is performed' + it_behaves_like 'snippet create data is tracked' + it_behaves_like 'an error service response when save fails' + end + + context 'when PersonalSnippet' do + let(:project) { nil } + + it_behaves_like 'a service that creates a snippet' + it_behaves_like 'public visibility level restrictions apply' + it_behaves_like 'spam check is performed' + it_behaves_like 'snippet create data is tracked' + it_behaves_like 'an error service response when save fails' + end + end +end diff --git a/spec/services/snippets/destroy_service_spec.rb b/spec/services/snippets/destroy_service_spec.rb new file mode 100644 index 00000000000..bb035d275ab --- /dev/null +++ b/spec/services/snippets/destroy_service_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Snippets::DestroyService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + + describe '#execute' do + subject { Snippets::DestroyService.new(user, snippet).execute } + + context 'when snippet is nil' do + let(:snippet) { nil } + + it 'returns a ServiceResponse error' do + expect(subject).to be_error + end + end + + shared_examples 'a successful destroy' do + it 'deletes the snippet' do + expect { subject }.to change { Snippet.count }.by(-1) + end + + it 'returns ServiceResponse success' do + expect(subject).to be_success + end + end + + shared_examples 'an unsuccessful destroy' do + it 'does not delete the snippet' do + expect { subject }.to change { Snippet.count }.by(0) + end + + it 'returns ServiceResponse error' do + expect(subject).to be_error + end + end + + context 'when ProjectSnippet' do + let!(:snippet) { create(:project_snippet, project: project, author: author) } + + context 'when user is able to admin_project_snippet' do + let(:author) { user } + + before do + project.add_developer(user) + end + + it_behaves_like 'a successful destroy' + end + + context 'when user is not able to admin_project_snippet' do + let(:author) { other_user } + + it_behaves_like 'an unsuccessful destroy' + end + end + + context 'when PersonalSnippet' do + let!(:snippet) { create(:personal_snippet, author: author) } + + context 'when user is able to admin_personal_snippet' do + let(:author) { user } + + it_behaves_like 'a successful destroy' + end + + context 'when user is not able to admin_personal_snippet' do + let(:author) { other_user } + + it_behaves_like 'an unsuccessful destroy' + end + end + end +end diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb new file mode 100644 index 00000000000..b8215f9779d --- /dev/null +++ b/spec/services/snippets/update_service_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Snippets::UpdateService do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create :user, admin: true } + let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } + let(:options) do + { + title: 'Test snippet', + file_name: 'snippet.rb', + content: 'puts "hello world"', + visibility_level: visibility_level + } + end + let(:updater) { user } + + subject do + Snippets::UpdateService.new( + project, + updater, + options + ).execute(snippet) + end + + shared_examples 'a service that updates a snippet' do + it 'updates a snippet with the provided attributes' do + expect { subject }.to change { snippet.title }.from(snippet.title).to(options[:title]) + .and change { snippet.file_name }.from(snippet.file_name).to(options[:file_name]) + .and change { snippet.content }.from(snippet.content).to(options[:content]) + end + end + + shared_examples 'public visibility level restrictions apply' do + let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } + + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + context 'when user is not an admin' do + it 'responds with an error' do + expect(subject).to be_error + end + + it 'does not update snippet to public visibility' do + original_visibility = snippet.visibility_level + + expect(subject.message).to match('has been restricted') + expect(snippet.visibility_level).to eq(original_visibility) + end + end + + context 'when user is an admin' do + let(:updater) { admin } + + it 'responds with success' do + expect(subject).to be_success + end + + it 'updates the snippet to public visibility' do + old_visibility = snippet.visibility_level + + expect(subject.payload[:snippet]).not_to be_nil + expect(snippet.visibility_level).not_to eq(old_visibility) + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + context 'when visibility level is passed as a string' do + before do + options[:visibility] = 'internal' + options.delete(:visibility_level) + end + + it 'assigns the correct visibility level' do + expect(subject).to be_success + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end + end + + shared_examples 'snippet update data is tracked' do + let(:counter) { Gitlab::UsageDataCounters::SnippetCounter } + + it 'increments count when create succeeds' do + expect { subject }.to change { counter.read(:update) }.by 1 + end + + context 'when update fails' do + let(:options) { { title: '' } } + + it 'does not increment count' do + expect { subject }.not_to change { counter.read(:update) } + end + end + end + + context 'when Project Snippet' do + let_it_be(:project) { create(:project) } + let!(:snippet) { create(:project_snippet, author: user, project: project) } + + before do + project.add_developer(user) + end + + it_behaves_like 'a service that updates a snippet' + it_behaves_like 'public visibility level restrictions apply' + it_behaves_like 'snippet update data is tracked' + end + + context 'when PersonalSnippet' do + let(:project) { nil } + let!(:snippet) { create(:personal_snippet, author: user) } + + it_behaves_like 'a service that updates a snippet' + it_behaves_like 'public visibility level restrictions apply' + it_behaves_like 'snippet update data is tracked' + end + end +end diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb deleted file mode 100644 index 19b35dca6a7..00000000000 --- a/spec/services/update_snippet_service_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe UpdateSnippetService do - before do - @user = create :user - @admin = create :user, admin: true - @opts = { - title: 'Test snippet', - file_name: 'snippet.rb', - content: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PRIVATE - } - end - - context 'When public visibility is restricted' do - before do - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - - @snippet = create_snippet(@project, @user, @opts) - @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - end - - it 'non-admins should not be able to update to public visibility' do - old_visibility = @snippet.visibility_level - update_snippet(@project, @user, @snippet, @opts) - expect(@snippet.errors.messages).to have_key(:visibility_level) - expect(@snippet.errors.messages[:visibility_level].first).to( - match('has been restricted') - ) - expect(@snippet.visibility_level).to eq(old_visibility) - end - - it 'admins should be able to update to public visibility' do - old_visibility = @snippet.visibility_level - update_snippet(@project, @admin, @snippet, @opts) - expect(@snippet.visibility_level).not_to eq(old_visibility) - expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - describe "when visibility level is passed as a string" do - before do - @opts[:visibility] = 'internal' - @opts.delete(:visibility_level) - end - - it "assigns the correct visibility level" do - update_snippet(@project, @user, @snippet, @opts) - expect(@snippet.errors.any?).to be_falsey - expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - end - end - - describe 'usage counter' do - let(:counter) { Gitlab::UsageDataCounters::SnippetCounter } - let(:snippet) { create_snippet(nil, @user, @opts) } - - it 'increments count' do - expect do - update_snippet(nil, @admin, snippet, @opts) - end.to change { counter.read(:update) }.by 1 - end - - it 'does not increment count if create fails' do - expect do - update_snippet(nil, @admin, snippet, { title: '' }) - end.not_to change { counter.read(:update) } - end - end - - def create_snippet(project, user, opts) - CreateSnippetService.new(project, user, opts).execute - end - - def update_snippet(project, user, snippet, opts) - UpdateSnippetService.new(project, user, snippet, opts).execute - end -end |