summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb60
-rw-r--r--spec/controllers/snippets_controller_spec.rb52
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js167
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js151
-rw-r--r--spec/frontend/ide/stores/modules/pane/actions_spec.js76
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js1
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js1
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js1
-rw-r--r--spec/lib/gitlab/import_export/group_project_object_builder_spec.rb53
-rw-r--r--spec/requests/api/error_tracking_spec.rb1
-rw-r--r--spec/services/create_snippet_service_spec.rb117
-rw-r--r--spec/services/snippets/create_service_spec.rb170
-rw-r--r--spec/services/snippets/destroy_service_spec.rb77
-rw-r--r--spec/services/snippets/update_service_spec.rb123
-rw-r--r--spec/services/update_snippet_service_spec.rb80
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