Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-28 21:09:35 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-28 21:09:35 +0300
commit95e18e32833de71b46d73ead66c8f13e261af3f4 (patch)
treebf61062dc1ae8ec2a25b28cd6385190661d3b37c /spec
parent37ae6b54ba524c438d1b756ce3ca29bbcec4e897 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/snippets/search_snippets_spec.rb2
-rw-r--r--spec/finders/merge_requests_finder_spec.rb11
-rw-r--r--spec/frontend/__mocks__/@toast-ui/vue-editor/index.js17
-rw-r--r--spec/frontend/boards/boards_store_spec.js60
-rw-r--r--spec/frontend/diffs/store/utils_spec.js12
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js (renamed from spec/javascripts/feature_highlight/feature_highlight_helper_spec.js)31
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_options_spec.js44
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_spec.js (renamed from spec/javascripts/feature_highlight/feature_highlight_spec.js)35
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js31
-rw-r--r--spec/frontend/sidebar/participants_spec.js206
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js135
-rw-r--r--spec/frontend/sidebar/sidebar_subscriptions_spec.js36
-rw-r--r--spec/frontend/sidebar/subscriptions_spec.js106
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js37
-rw-r--r--spec/helpers/search_helper_spec.rb1
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_buttons_spec.js32
-rw-r--r--spec/javascripts/sidebar/participants_spec.js202
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js134
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js38
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js100
-rw-r--r--spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb46
-rw-r--r--spec/lib/gitlab/danger/changelog_spec.rb8
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb19
-rw-r--r--spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb81
-rw-r--r--spec/models/merge_request_spec.rb22
-rw-r--r--spec/models/user_spec.rb12
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb51
-rw-r--r--spec/requests/api/issues/issues_spec.rb26
-rw-r--r--spec/requests/api/search_spec.rb10
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb6
-rw-r--r--spec/services/issues/create_service_spec.rb25
32 files changed, 911 insertions, 667 deletions
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index 691716d3576..d3e02d43813 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -11,7 +11,7 @@ describe 'Search Snippets' do
visit dashboard_snippets_path
submit_search('Middle')
- select_search_scope('Titles and Filenames')
+ select_search_scope('Titles and Descriptions')
expect(page).to have_link(public_snippet.title)
expect(page).to have_link(private_snippet.title)
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 42211f7ac9d..b6f2c7bb992 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -174,15 +174,16 @@ describe MergeRequestsFinder do
deployment1 = create(
:deployment,
project: project_with_repo,
- sha: project_with_repo.commit.id,
- merge_requests: [merge_request1, merge_request2]
+ sha: project_with_repo.commit.id
)
- create(
+ deployment2 = create(
:deployment,
project: project_with_repo,
- sha: project_with_repo.commit.id,
- merge_requests: [merge_request3]
+ sha: project_with_repo.commit.id
)
+ deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
+ deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id))
+
params = { deployment_id: deployment1.id }
merge_requests = described_class.new(user, params).execute
diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
new file mode 100644
index 00000000000..5ca620c24cf
--- /dev/null
+++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
@@ -0,0 +1,17 @@
+export const Editor = {
+ props: {
+ initialValue: {
+ type: String,
+ required: true,
+ },
+ },
+ render(h) {
+ return h('div');
+ },
+};
+
+export const Viewer = {
+ render(h) {
+ return h('div');
+ },
+};
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 05a44138275..116851a9903 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -1041,6 +1041,66 @@ describe('boardsStore', () => {
});
});
+ describe('addListIssue', () => {
+ let list;
+ const issue1 = new ListIssue({
+ title: 'Testing',
+ id: 2,
+ iid: 2,
+ confidential: false,
+ labels: [
+ {
+ color: '#ff0000',
+ description: 'testing;',
+ id: 5000,
+ priority: undefined,
+ textColor: 'white',
+ title: 'Test',
+ },
+ ],
+ assignees: [],
+ });
+ const issue2 = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [
+ {
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing',
+ },
+ ],
+ assignees: [
+ {
+ id: 1,
+ name: 'name',
+ username: 'username',
+ avatar_url: 'http://avatar_url',
+ },
+ ],
+ real_path: 'path/to/issue',
+ });
+
+ beforeEach(() => {
+ list = new List(listObj);
+ list.addIssue(issue1);
+ setupDefaultResponses();
+ });
+
+ it('adds issues that are not already on the list', () => {
+ expect(list.findIssue(issue2.id)).toBe(undefined);
+ expect(list.issues).toEqual([issue1]);
+
+ boardsStore.addListIssue(list, issue2);
+ expect(list.findIssue(issue2.id)).toBe(issue2);
+ expect(list.issues.length).toBe(2);
+ expect(list.issues).toEqual([issue1, issue2]);
+ });
+ });
+
describe('updateIssue', () => {
let issue;
let patchSpy;
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 1adcdab272a..422332bab28 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -503,11 +503,16 @@ describe('DiffsStoreUtils', () => {
},
};
+ // When multi line comments are fully implemented `line_code` will be
+ // included in all requests. Until then we need to ensure the logic does
+ // not change when it is included only in the "comparison" argument.
+ const lineRange = { start_line_code: 'abc_1_1', end_line_code: 'abc_1_2' };
+
it('returns true when the discussion is up to date', () => {
expect(
utils.isDiscussionApplicableToLine({
discussion: discussions.upToDateDiscussion1,
- diffPosition,
+ diffPosition: { ...diffPosition, line_range: lineRange },
latestDiff: true,
}),
).toBe(true);
@@ -517,7 +522,7 @@ describe('DiffsStoreUtils', () => {
expect(
utils.isDiscussionApplicableToLine({
discussion: discussions.outDatedDiscussion1,
- diffPosition,
+ diffPosition: { ...diffPosition, line_range: lineRange },
latestDiff: true,
}),
).toBe(false);
@@ -534,6 +539,7 @@ describe('DiffsStoreUtils', () => {
diffPosition: {
...diffPosition,
lineCode: 'ABC_1',
+ line_range: lineRange,
},
latestDiff: true,
}),
@@ -551,6 +557,7 @@ describe('DiffsStoreUtils', () => {
diffPosition: {
...diffPosition,
line_code: 'ABC_1',
+ line_range: lineRange,
},
latestDiff: true,
}),
@@ -568,6 +575,7 @@ describe('DiffsStoreUtils', () => {
diffPosition: {
...diffPosition,
lineCode: 'ABC_1',
+ line_range: lineRange,
},
latestDiff: false,
}),
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index ba35f7bf7c6..2c3c3e3267a 100644
--- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -1,6 +1,4 @@
import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
-import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import axios from '~/lib/utils/axios_utils';
import { getSelector, dismiss, inserted } from '~/feature_highlight/feature_highlight_helper';
import { togglePopover } from '~/shared/popover';
@@ -17,34 +15,23 @@ describe('feature highlight helper', () => {
});
describe('dismiss', () => {
- let mock;
const context = {
hide: () => {},
attr: () => '/-/callouts/dismiss',
};
beforeEach(() => {
- mock = new MockAdapter(axios);
-
- spyOn(togglePopover, 'call').and.callFake(() => {});
- spyOn(context, 'hide').and.callFake(() => {});
+ jest.spyOn(axios, 'post').mockResolvedValue();
+ jest.spyOn(togglePopover, 'call').mockImplementation(() => {});
+ jest.spyOn(context, 'hide').mockImplementation(() => {});
dismiss.call(context);
});
- afterEach(() => {
- mock.restore();
- });
-
- it('calls persistent dismissal endpoint', done => {
- const spy = jasmine.createSpy('dismiss-endpoint-hit');
- mock.onPost('/-/callouts/dismiss').reply(spy);
-
- getSetTimeoutPromise()
- .then(() => {
- expect(spy).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ it('calls persistent dismissal endpoint', () => {
+ expect(axios.post).toHaveBeenCalledWith(
+ '/-/callouts/dismiss',
+ expect.objectContaining({ feature_name: undefined }),
+ );
});
it('calls hide popover', () => {
@@ -65,7 +52,7 @@ describe('feature highlight helper', () => {
},
};
- spyOn($.fn, 'on').and.callFake(event => {
+ jest.spyOn($.fn, 'on').mockImplementation(event => {
expect(event).toEqual('click');
done();
});
diff --git a/spec/frontend/feature_highlight/feature_highlight_options_spec.js b/spec/frontend/feature_highlight/feature_highlight_options_spec.js
index 8b75c46fd4c..f82f984cb7f 100644
--- a/spec/frontend/feature_highlight/feature_highlight_options_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_options_spec.js
@@ -3,34 +3,20 @@ import domContentLoaded from '~/feature_highlight/feature_highlight_options';
describe('feature highlight options', () => {
describe('domContentLoaded', () => {
- it('should not call highlightFeatures when breakpoint is xs', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should not call highlightFeatures when breakpoint is sm', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should not call highlightFeatures when breakpoint is md', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should not call highlightFeatures when breakpoint is not xl', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should call highlightFeatures when breakpoint is xl', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl');
-
- expect(domContentLoaded()).toBe(true);
- });
+ it.each`
+ breakPoint | shouldCall
+ ${'xs'} | ${false}
+ ${'sm'} | ${false}
+ ${'md'} | ${false}
+ ${'lg'} | ${false}
+ ${'xl'} | ${true}
+ `(
+ 'when breakpoint is $breakPoint should call highlightFeatures is $shouldCall',
+ ({ breakPoint, shouldCall }) => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue(breakPoint);
+
+ expect(domContentLoaded()).toBe(shouldCall);
+ },
+ );
});
});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/frontend/feature_highlight/feature_highlight_spec.js
index 40ac4bbb6a0..79c4050c8c4 100644
--- a/spec/javascripts/feature_highlight/feature_highlight_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_spec.js
@@ -4,6 +4,8 @@ import * as featureHighlight from '~/feature_highlight/feature_highlight';
import * as popover from '~/shared/popover';
import axios from '~/lib/utils/axios_utils';
+jest.mock('~/shared/popover');
+
describe('feature highlight', () => {
beforeEach(() => {
setFixtures(`
@@ -28,7 +30,7 @@ describe('feature highlight', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet('/test').reply(200);
- spyOn(window, 'addEventListener');
+ jest.spyOn(window, 'addEventListener').mockImplementation(() => {});
featureHighlight.setupFeatureHighlightPopover('test', 0);
});
@@ -44,27 +46,21 @@ describe('feature highlight', () => {
});
it('setup mouseenter', () => {
- const toggleSpy = spyOn(popover.togglePopover, 'call');
$(selector).trigger('mouseenter');
- expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
+ expect(popover.mouseenter).toHaveBeenCalledWith(expect.any(Object));
});
- it('setup debounced mouseleave', done => {
- const toggleSpy = spyOn(popover.togglePopover, 'call');
+ it('setup debounced mouseleave', () => {
$(selector).trigger('mouseleave');
- // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
- setTimeout(() => {
- expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false);
- done();
- }, 0);
+ expect(popover.debouncedMouseleave).toHaveBeenCalled();
});
it('setup show.bs.popover', () => {
$(selector).trigger('show.bs.popover');
- expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), {
+ expect(window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), {
once: true,
});
});
@@ -72,23 +68,6 @@ describe('feature highlight', () => {
it('removes disabled attribute', () => {
expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
});
-
- it('displays popover', () => {
- expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeFalsy();
- $(selector).trigger('mouseenter');
-
- expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeTruthy();
- });
-
- it('toggles when clicked', () => {
- $(selector).trigger('mouseenter');
- const popoverId = $(selector).attr('aria-describedby');
- const toggleSpy = spyOn(popover.togglePopover, 'call');
-
- $(`#${popoverId} .dismiss-feature-highlight`).click();
-
- expect(toggleSpy).toHaveBeenCalled();
- });
});
describe('findHighestPriorityFeature', () => {
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..66f9237ce97
--- /dev/null
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -0,0 +1,31 @@
+import { shallowMount } from '@vue/test-utils';
+import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+
+describe('EditFormButtons', () => {
+ let wrapper;
+
+ const mountComponent = propsData => shallowMount(EditFormButtons, { propsData });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays "Unlock" when locked', () => {
+ wrapper = mountComponent({
+ isLocked: true,
+ updateLockedAttribute: () => {},
+ });
+
+ expect(wrapper.text()).toContain('Unlock');
+ });
+
+ it('displays "Lock" when unlocked', () => {
+ wrapper = mountComponent({
+ isLocked: false,
+ updateLockedAttribute: () => {},
+ });
+
+ expect(wrapper.text()).toContain('Lock');
+ });
+});
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
new file mode 100644
index 00000000000..ebe94582588
--- /dev/null
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -0,0 +1,206 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Participants from '~/sidebar/components/participants/participants.vue';
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
+
+describe('Participants', () => {
+ let wrapper;
+
+ const getMoreParticipantsButton = () => wrapper.find('button');
+
+ const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
+
+ const mountComponent = propsData =>
+ shallowMount(Participants, {
+ propsData,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('collapsed sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('does not show loading spinner not loading', () => {
+ wrapper = mountComponent({
+ loading: false,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(false);
+ });
+
+ it('shows participant count when given', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ });
+
+ expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+
+ it('shows full participant count when there are hidden participants', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 1,
+ });
+
+ expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+ });
+
+ describe('expanded sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
+ const numberOfLessParticipants = 2;
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: false,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
+ });
+ });
+
+ it('when only showing all participants, each has an avatar', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: true,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
+ });
+ });
+
+ it('does not have more participants link when they can all be shown', () => {
+ const numberOfLessParticipants = 100;
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+
+ expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
+ expect(getMoreParticipantsButton().exists()).toBe(false);
+ });
+
+ it('when too many participants, has more participants link to show more', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: false,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
+ });
+ });
+
+ it('when too many participants and already showing them, has more participants link to show less', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: true,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(getMoreParticipantsButton().text()).toBe('- show less');
+ });
+ });
+
+ it('clicking more participants link emits event', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
+
+ getMoreParticipantsButton().trigger('click');
+
+ expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
+ });
+
+ it('clicking on participants icon emits `toggleSidebar` event', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ const spy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.find('.sidebar-collapsed-icon').trigger('click');
+
+ return Vue.nextTick(() => {
+ expect(spy).toHaveBeenCalledWith('toggleSidebar');
+
+ spy.mockRestore();
+ });
+ });
+ });
+
+ describe('when not showing participants label', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ participants: PARTICIPANT_LIST,
+ showParticipantLabel: false,
+ });
+ });
+
+ it('does not show sidebar collapsed icon', () => {
+ expect(wrapper.contains('.sidebar-collapsed-icon')).toBe(false);
+ });
+
+ it('does not show participants label title', () => {
+ expect(wrapper.contains('.title')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
new file mode 100644
index 00000000000..0892d452966
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -0,0 +1,135 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as urlUtility from '~/lib/utils/url_utility';
+import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+
+describe('Sidebar mediator', () => {
+ const { mediator: mediatorMockData } = Mock;
+ let mock;
+ let mediator;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mediator = new SidebarMediator(mediatorMockData);
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ mock.restore();
+ });
+
+ it('assigns yourself ', () => {
+ mediator.assignYourself();
+
+ expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
+ expect(mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
+ });
+
+ it('saves assignees', () => {
+ mock.onPut(mediatorMockData.endpoint).reply(200, {});
+
+ return mediator.saveAssignees('issue[assignee_ids]').then(resp => {
+ expect(resp.status).toEqual(200);
+ });
+ });
+
+ it('fetches the data', () => {
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
+ mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
+
+ const mockGraphQlData = Mock.graphQlResponseData;
+ const graphQlSpy = jest.spyOn(gqClient, 'query').mockReturnValue({
+ data: mockGraphQlData,
+ });
+ const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve());
+
+ return mediator.fetch().then(() => {
+ expect(spy).toHaveBeenCalledWith(mockData, mockGraphQlData);
+
+ spy.mockRestore();
+ graphQlSpy.mockRestore();
+ });
+ });
+
+ it('processes fetched data', () => {
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
+ mediator.processFetchedData(mockData);
+
+ expect(mediator.store.assignees).toEqual(mockData.assignees);
+ expect(mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
+ expect(mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
+ expect(mediator.store.participants).toEqual(mockData.participants);
+ expect(mediator.store.subscribed).toEqual(mockData.subscribed);
+ expect(mediator.store.timeEstimate).toEqual(mockData.time_estimate);
+ expect(mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
+ });
+
+ it('sets moveToProjectId', () => {
+ const projectId = 7;
+ const spy = jest.spyOn(mediator.store, 'setMoveToProjectId').mockReturnValue(Promise.resolve());
+
+ mediator.setMoveToProjectId(projectId);
+
+ expect(spy).toHaveBeenCalledWith(projectId);
+
+ spy.mockRestore();
+ });
+
+ it('fetches autocomplete projects', () => {
+ const searchTerm = 'foo';
+ mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
+ const getterSpy = jest
+ .spyOn(mediator.service, 'getProjectsAutocomplete')
+ .mockReturnValue(Promise.resolve({ data: {} }));
+ const setterSpy = jest
+ .spyOn(mediator.store, 'setAutocompleteProjects')
+ .mockReturnValue(Promise.resolve());
+
+ return mediator.fetchAutocompleteProjects(searchTerm).then(() => {
+ expect(getterSpy).toHaveBeenCalledWith(searchTerm);
+ expect(setterSpy).toHaveBeenCalled();
+
+ getterSpy.mockRestore();
+ setterSpy.mockRestore();
+ });
+ });
+
+ it('moves issue', () => {
+ const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
+ const moveToProjectId = 7;
+ mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
+ mediator.store.setMoveToProjectId(moveToProjectId);
+ const moveIssueSpy = jest
+ .spyOn(mediator.service, 'moveIssue')
+ .mockReturnValue(Promise.resolve({ data: { web_url: mockData.web_url } }));
+ const urlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+
+ return mediator.moveIssue().then(() => {
+ expect(moveIssueSpy).toHaveBeenCalledWith(moveToProjectId);
+ expect(urlSpy).toHaveBeenCalledWith(mockData.web_url);
+
+ moveIssueSpy.mockRestore();
+ urlSpy.mockRestore();
+ });
+ });
+
+ it('toggle subscription', () => {
+ mediator.store.setSubscribedState(false);
+ mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
+ const spy = jest
+ .spyOn(mediator.service, 'toggleSubscription')
+ .mockReturnValue(Promise.resolve());
+
+ return mediator.toggleSubscription().then(() => {
+ expect(spy).toHaveBeenCalled();
+ expect(mediator.store.subscribed).toEqual(true);
+
+ spy.mockRestore();
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_subscriptions_spec.js b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
new file mode 100644
index 00000000000..18aaeabe3dd
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+
+describe('Sidebar Subscriptions', () => {
+ let wrapper;
+ let mediator;
+
+ beforeEach(() => {
+ mediator = new SidebarMediator(Mock.mediator);
+ wrapper = shallowMount(SidebarSubscriptions, {
+ propsData: {
+ mediator,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ });
+
+ it('calls the mediator toggleSubscription on event', () => {
+ const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve());
+
+ wrapper.vm.onToggleSubscription();
+
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+});
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js
new file mode 100644
index 00000000000..cce35666985
--- /dev/null
+++ b/spec/frontend/sidebar/subscriptions_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import eventHub from '~/sidebar/event_hub';
+import ToggleButton from '~/vue_shared/components/toggle_button.vue';
+
+describe('Subscriptions', () => {
+ let wrapper;
+
+ const findToggleButton = () => wrapper.find(ToggleButton);
+
+ const mountComponent = propsData =>
+ shallowMount(Subscriptions, {
+ propsData,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ subscribed: undefined,
+ });
+
+ expect(findToggleButton().attributes('isloading')).toBe('true');
+ });
+
+ it('is toggled "off" when currently not subscribed', () => {
+ wrapper = mountComponent({
+ subscribed: false,
+ });
+
+ expect(findToggleButton().attributes('value')).toBeFalsy();
+ });
+
+ it('is toggled "on" when currently subscribed', () => {
+ wrapper = mountComponent({
+ subscribed: true,
+ });
+
+ expect(findToggleButton().attributes('value')).toBe('true');
+ });
+
+ it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
+ const id = 42;
+ wrapper = mountComponent({ subscribed: true, id });
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+ const wrapperEmitSpy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.toggleSubscription();
+
+ expect(eventHubSpy).toHaveBeenCalledWith('toggleSubscription', id);
+ expect(wrapperEmitSpy).toHaveBeenCalledWith('toggleSubscription', id);
+ eventHubSpy.mockRestore();
+ wrapperEmitSpy.mockRestore();
+ });
+
+ it('tracks the event when toggled', () => {
+ wrapper = mountComponent({ subscribed: true });
+
+ const wrapperTrackSpy = jest.spyOn(wrapper.vm, 'track');
+
+ wrapper.vm.toggleSubscription();
+
+ expect(wrapperTrackSpy).toHaveBeenCalledWith('toggle_button', {
+ property: 'notifications',
+ value: 0,
+ });
+ wrapperTrackSpy.mockRestore();
+ });
+
+ it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
+ wrapper = mountComponent({ subscribed: true });
+ const spy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.onClickCollapsedIcon();
+
+ expect(spy).toHaveBeenCalledWith('toggleSidebar');
+ spy.mockRestore();
+ });
+
+ describe('given project emails are disabled', () => {
+ const subscribeDisabledDescription = 'Notifications have been disabled';
+
+ beforeEach(() => {
+ wrapper = mountComponent({
+ subscribed: false,
+ projectEmailsDisabled: true,
+ subscribeDisabledDescription,
+ });
+ });
+
+ it('sets the correct display text', () => {
+ expect(wrapper.find('.issuable-header-text').text()).toContain(subscribeDisabledDescription);
+ expect(wrapper.find({ ref: 'tooltip' }).attributes('data-original-title')).toBe(
+ subscribeDisabledDescription,
+ );
+ });
+
+ it('does not render the toggle button', () => {
+ expect(wrapper.contains('.js-issuable-subscribe-button')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
new file mode 100644
index 00000000000..eb2baac3e76
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -0,0 +1,37 @@
+import { shallowMount } from '@vue/test-utils';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+
+describe('Rich Content Editor', () => {
+ let wrapper;
+
+ const value = '## Some Markdown';
+ const findEditor = () => wrapper.find({ ref: 'editor' });
+
+ beforeEach(() => {
+ wrapper = shallowMount(RichContentEditor, {
+ propsData: { value },
+ });
+ });
+
+ describe('when content is loaded', () => {
+ it('renders an editor', () => {
+ expect(findEditor().exists()).toBe(true);
+ });
+
+ it('renders the correct content', () => {
+ expect(findEditor().props().initialValue).toBe(value);
+ });
+ });
+
+ describe('when content is changed', () => {
+ it('emits an input event with the changed content', () => {
+ const changedMarkdown = '## Changed Markdown';
+ const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown);
+
+ findEditor().setMethods({ invoke: getMarkdownMock });
+ findEditor().vm.$emit('change');
+
+ expect(wrapper.emitted().input[0][0]).toBe(changedMarkdown);
+ });
+ });
+});
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 18c94602596..6a06b012c6c 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -112,7 +112,6 @@ describe SearchHelper do
'milestones' | 'milestone'
'notes' | 'comment'
'projects' | 'project'
- 'snippet_blobs' | 'snippet result'
'snippet_titles' | 'snippet'
'users' | 'user'
'wiki_blobs' | 'wiki result'
diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
deleted file mode 100644
index c532554efb4..00000000000
--- a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
-
-describe('EditFormButtons', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editFormButtons);
- const toggleForm = () => {};
- const updateLockedAttribute = () => {};
-
- vm1 = mountComponent(Component, {
- isLocked: true,
- toggleForm,
- updateLockedAttribute,
- });
-
- vm2 = mountComponent(Component, {
- isLocked: false,
- toggleForm,
- updateLockedAttribute,
- });
- });
-
- it('renders unlock or lock text based on locked state', () => {
- expect(vm1.$el.innerHTML.includes('Unlock')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Lock')).toBe(true);
- });
-});
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
deleted file mode 100644
index 7e80e86f8ca..00000000000
--- a/spec/javascripts/sidebar/participants_spec.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import participants from '~/sidebar/components/participants/participants.vue';
-
-const PARTICIPANT = {
- id: 1,
- state: 'active',
- username: 'marcene',
- name: 'Allie Will',
- web_url: 'foo.com',
- avatar_url: 'gravatar.com/avatar/xxx',
-};
-
-const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
-
-describe('Participants', function() {
- let vm;
- let Participants;
-
- beforeEach(() => {
- Participants = Vue.extend(participants);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('collapsed sidebar state', () => {
- it('shows loading spinner when loading', () => {
- vm = mountComponent(Participants, {
- loading: true,
- });
-
- expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
- });
-
- it('shows participant count when given', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- });
- const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
-
- expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
- });
-
- it('shows full participant count when there are hidden participants', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 1,
- });
- const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
-
- expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
- });
- });
-
- describe('expanded sidebar state', () => {
- it('shows loading spinner when loading', () => {
- vm = mountComponent(Participants, {
- loading: true,
- });
-
- expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
- });
-
- it('when only showing visible participants, shows an avatar only for each participant under the limit', done => {
- const numberOfLessParticipants = 2;
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
- vm.isShowingMoreParticipants = false;
-
- Vue.nextTick()
- .then(() => {
- const participantEls = vm.$el.querySelectorAll('.js-participants-author');
-
- expect(participantEls.length).toBe(numberOfLessParticipants);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('when only showing all participants, each has an avatar', done => {
- const numberOfLessParticipants = 2;
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
- vm.isShowingMoreParticipants = true;
-
- Vue.nextTick()
- .then(() => {
- const participantEls = vm.$el.querySelectorAll('.js-participants-author');
-
- expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not have more participants link when they can all be shown', () => {
- const numberOfLessParticipants = 100;
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
- expect(moreParticipantLink).toBeNull();
- });
-
- it('when too many participants, has more participants link to show more', done => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- vm.isShowingMoreParticipants = false;
-
- Vue.nextTick()
- .then(() => {
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('when too many participants and already showing them, has more participants link to show less', done => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- vm.isShowingMoreParticipants = true;
-
- Vue.nextTick()
- .then(() => {
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(moreParticipantLink.textContent.trim()).toBe('- show less');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('clicking more participants link emits event', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(vm.isShowingMoreParticipants).toBe(false);
-
- moreParticipantLink.click();
-
- expect(vm.isShowingMoreParticipants).toBe(true);
- });
-
- it('clicking on participants icon emits `toggleSidebar` event', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- spyOn(vm, '$emit');
-
- const participantsIconEl = vm.$el.querySelector('.sidebar-collapsed-icon');
-
- participantsIconEl.click();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
- });
- });
-
- describe('when not showing participants label', () => {
- beforeEach(() => {
- vm = mountComponent(Participants, {
- participants: PARTICIPANT_LIST,
- showParticipantLabel: false,
- });
- });
-
- it('does not show sidebar collapsed icon', () => {
- expect(vm.$el.querySelector('.sidebar-collapsed-icon')).not.toBeTruthy();
- });
-
- it('does not show participants label title', () => {
- expect(vm.$el.querySelector('.title')).not.toBeTruthy();
- });
- });
-});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
deleted file mode 100644
index 2aa30fd1cc6..00000000000
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
-import Mock from './mock_data';
-
-const { mediator: mediatorMockData } = Mock;
-
-describe('Sidebar mediator', function() {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- this.mediator = new SidebarMediator(mediatorMockData);
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
- mock.restore();
- });
-
- it('assigns yourself ', () => {
- this.mediator.assignYourself();
-
- expect(this.mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
- expect(this.mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
- });
-
- it('saves assignees', done => {
- mock.onPut(mediatorMockData.endpoint).reply(200, {});
- this.mediator
- .saveAssignees('issue[assignee_ids]')
- .then(resp => {
- expect(resp.status).toEqual(200);
- done();
- })
- .catch(done.fail);
- });
-
- it('fetches the data', done => {
- const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
- mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
-
- const mockGraphQlData = Mock.graphQlResponseData;
- spyOn(gqClient, 'query').and.returnValue({
- data: mockGraphQlData,
- });
-
- spyOn(this.mediator, 'processFetchedData').and.callThrough();
-
- this.mediator
- .fetch()
- .then(() => {
- expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData, mockGraphQlData);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('processes fetched data', () => {
- const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
- this.mediator.processFetchedData(mockData);
-
- expect(this.mediator.store.assignees).toEqual(mockData.assignees);
- expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
- expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
- expect(this.mediator.store.participants).toEqual(mockData.participants);
- expect(this.mediator.store.subscribed).toEqual(mockData.subscribed);
- expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate);
- expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
- });
-
- it('sets moveToProjectId', () => {
- const projectId = 7;
- spyOn(this.mediator.store, 'setMoveToProjectId').and.callThrough();
-
- this.mediator.setMoveToProjectId(projectId);
-
- expect(this.mediator.store.setMoveToProjectId).toHaveBeenCalledWith(projectId);
- });
-
- it('fetches autocomplete projects', done => {
- const searchTerm = 'foo';
- mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
- spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough();
- spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough();
-
- this.mediator
- .fetchAutocompleteProjects(searchTerm)
- .then(() => {
- expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
- expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('moves issue', done => {
- const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
- const moveToProjectId = 7;
- mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
- this.mediator.store.setMoveToProjectId(moveToProjectId);
- spyOn(this.mediator.service, 'moveIssue').and.callThrough();
- const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl');
-
- this.mediator
- .moveIssue()
- .then(() => {
- expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(visitUrl).toHaveBeenCalledWith(mockData.web_url);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('toggle subscription', done => {
- this.mediator.store.setSubscribedState(false);
- mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
- spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
-
- this.mediator
- .toggleSubscription()
- .then(() => {
- expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
- expect(this.mediator.store.subscribed).toEqual(true);
- })
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
deleted file mode 100644
index ee4516f3bcd..00000000000
--- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
-
-describe('Sidebar Subscriptions', function() {
- let vm;
- let SidebarSubscriptions;
-
- beforeEach(() => {
- SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
- // Set up the stores, services, etc
- // eslint-disable-next-line no-new
- new SidebarMediator(Mock.mediator);
- });
-
- afterEach(() => {
- vm.$destroy();
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
- });
-
- it('calls the mediator toggleSubscription on event', () => {
- const mediator = new SidebarMediator();
- spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve());
- vm = mountComponent(SidebarSubscriptions, {
- mediator,
- });
-
- vm.onToggleSubscription();
-
- expect(mediator.toggleSubscription).toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
deleted file mode 100644
index cdb39efbef8..00000000000
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTracking } from 'spec/helpers/tracking_helper';
-import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
-import eventHub from '~/sidebar/event_hub';
-
-describe('Subscriptions', function() {
- let vm;
- let Subscriptions;
-
- beforeEach(() => {
- Subscriptions = Vue.extend(subscriptions);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('shows loading spinner when loading', () => {
- vm = mountComponent(Subscriptions, {
- loading: true,
- subscribed: undefined,
- });
-
- expect(vm.$refs.toggleButton.isLoading).toBe(true);
- expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass(
- 'is-loading',
- );
- });
-
- it('is toggled "off" when currently not subscribed', () => {
- vm = mountComponent(Subscriptions, {
- subscribed: false,
- });
-
- expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass(
- 'is-checked',
- );
- });
-
- it('is toggled "on" when currently subscribed', () => {
- vm = mountComponent(Subscriptions, {
- subscribed: true,
- });
-
- expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass(
- 'is-checked',
- );
- });
-
- it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
- vm = mountComponent(Subscriptions, { subscribed: true });
- spyOn(eventHub, '$emit');
- spyOn(vm, '$emit');
- spyOn(vm, 'track');
-
- vm.toggleSubscription();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
- expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
- });
-
- it('tracks the event when toggled', () => {
- vm = mountComponent(Subscriptions, { subscribed: true });
- const spy = mockTracking('_category_', vm.$el, spyOn);
- vm.toggleSubscription();
-
- expect(spy).toHaveBeenCalled();
- });
-
- it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
- vm = mountComponent(Subscriptions, { subscribed: true });
- spyOn(vm, '$emit');
-
- vm.onClickCollapsedIcon();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
- });
-
- describe('given project emails are disabled', () => {
- const subscribeDisabledDescription = 'Notifications have been disabled';
-
- beforeEach(() => {
- vm = mountComponent(Subscriptions, {
- subscribed: false,
- projectEmailsDisabled: true,
- subscribeDisabledDescription,
- });
- });
-
- it('sets the correct display text', () => {
- expect(vm.$el.textContent).toContain(subscribeDisabledDescription);
- expect(vm.$refs.tooltip.dataset.originalTitle).toBe(subscribeDisabledDescription);
- });
-
- it('does not render the toggle button', () => {
- expect(vm.$refs.toggleButton).toBeUndefined();
- });
- });
-});
diff --git a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
new file mode 100644
index 00000000000..c3bb975727b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeRequests, schema: 20200312134637 do
+ let(:environments) { table(:environments) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:deployments) { table(:deployments) }
+ let(:deployment_merge_requests) { table(:deployment_merge_requests) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ subject(:migration) { described_class.new }
+
+ it 'correctly backfills environment_id column' do
+ namespace = namespaces.create!(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+
+ production = environments.create!(project_id: project.id, name: 'production', slug: 'production')
+ staging = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
+
+ mr = merge_requests.create!(source_branch: 'x', target_branch: 'master', target_project_id: project.id)
+
+ deployment1 = deployments.create!(environment_id: staging.id, iid: 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
+ deployment2 = deployments.create!(environment_id: production.id, iid: 2, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
+ deployment3 = deployments.create!(environment_id: production.id, iid: 3, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
+
+ # mr is tracked twice in production through deployment2 and deployment3
+ deployment_merge_requests.create!(deployment_id: deployment1.id, merge_request_id: mr.id)
+ deployment_merge_requests.create!(deployment_id: deployment2.id, merge_request_id: mr.id)
+ deployment_merge_requests.create!(deployment_id: deployment3.id, merge_request_id: mr.id)
+
+ expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3)
+
+ migration.perform(1, mr.id)
+
+ expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero
+ expect(deployment_merge_requests.count).to eq(2)
+
+ production_deployments = deployment_merge_requests.where(environment_id: production.id)
+ expect(production_deployments.count).to eq(1)
+ expect(production_deployments.first.deployment_id).to eq(deployment2.id)
+
+ expect(deployment_merge_requests.where(environment_id: staging.id).count).to eq(1)
+ end
+end
diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb
index ba23c3828de..8929374fb87 100644
--- a/spec/lib/gitlab/danger/changelog_spec.rb
+++ b/spec/lib/gitlab/danger/changelog_spec.rb
@@ -86,14 +86,6 @@ describe Gitlab::Danger::Changelog do
end
end
- describe '#presented_no_changelog_labels' do
- subject { changelog.presented_no_changelog_labels }
-
- it 'returns the labels formatted' do
- is_expected.to eq('~backstage, ~ci-build, ~meta')
- end
- end
-
describe '#ee_changelog?' do
subject { changelog.ee_changelog? }
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 0f5b00e4bb5..c2c881fd589 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -399,9 +399,28 @@ describe Gitlab::Danger::Helper do
end
end
+ describe '#labels_list' do
+ let(:labels) { ['telemetry', 'telemetry::reviewed'] }
+
+ it 'composes the labels string' do
+ expect(helper.labels_list(labels)).to eq('~"telemetry", ~"telemetry::reviewed"')
+ end
+
+ context 'when passing a separator' do
+ it 'composes the labels string with the given separator' do
+ expect(helper.labels_list(labels, sep: ' ')).to eq('~"telemetry" ~"telemetry::reviewed"')
+ end
+ end
+
+ it 'returns empty string for empty array' do
+ expect(helper.labels_list([])).to eq('')
+ end
+ end
+
describe '#prepare_labels_for_mr' do
it 'composes the labels string' do
mr_labels = ['telemetry', 'telemetry::reviewed']
+
expect(helper.prepare_labels_for_mr(mr_labels)).to eq('/label ~"telemetry" ~"telemetry::reviewed"')
end
diff --git a/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb b/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb
new file mode 100644
index 00000000000..296ae07cc21
--- /dev/null
+++ b/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200312134637_backfill_environment_id_on_deployment_merge_requests.rb')
+
+describe BackfillEnvironmentIdOnDeploymentMergeRequests do
+ let(:environments) { table(:environments) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:deployments) { table(:deployments) }
+ let(:deployment_merge_requests) { table(:deployment_merge_requests) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ let(:migration_worker) { double('BackgroundMigrationWorker') }
+
+ before do
+ stub_const('BackgroundMigrationWorker', migration_worker)
+ end
+
+ it 'schedules nothing when there are no entries' do
+ expect(migration_worker).not_to receive(:perform_in)
+
+ migrate!
+ end
+
+ it 'batches the workload' do
+ stub_const("#{described_class.name}::BATCH_SIZE", 10)
+
+ namespace = namespaces.create!(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+
+ environment = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
+
+ # Batching is based on DeploymentMergeRequest.merge_request_id, in order to test it
+ # we must generate more than described_class::BATCH_SIZE merge requests, deployments,
+ # and deployment_merge_requests entries
+ entries = 13
+ expect(entries).to be > described_class::BATCH_SIZE
+
+ # merge requests and deployments bulk generation
+ mrs_params = []
+ deployments_params = []
+ entries.times do |i|
+ mrs_params << { source_branch: 'x', target_branch: 'master', target_project_id: project.id }
+
+ deployments_params << { environment_id: environment.id, iid: i + 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1 }
+ end
+
+ all_mrs = merge_requests.insert_all(mrs_params)
+ all_deployments = deployments.insert_all(deployments_params)
+
+ # deployment_merge_requests bulk generation
+ dmr_params = []
+ entries.times do |index|
+ mr_id = all_mrs.rows[index].first
+ deployment_id = all_deployments.rows[index].first
+
+ dmr_params << { deployment_id: deployment_id, merge_request_id: mr_id }
+ end
+
+ deployment_merge_requests.insert_all(dmr_params)
+
+ first_batch_limit = dmr_params[described_class::BATCH_SIZE][:merge_request_id]
+ second_batch_limit = dmr_params.last[:merge_request_id]
+
+ expect(migration_worker).to receive(:perform_in)
+ .with(
+ 0,
+ 'BackfillEnvironmentIdDeploymentMergeRequests',
+ [1, first_batch_limit]
+ )
+ expect(migration_worker).to receive(:perform_in)
+ .with(
+ described_class::DELAY,
+ 'BackfillEnvironmentIdDeploymentMergeRequests',
+ [first_batch_limit + 1, second_batch_limit]
+ )
+
+ migrate!
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8ec2ed2bf77..1a168e564dd 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -3700,41 +3700,41 @@ describe MergeRequest do
describe '#recent_visible_deployments' do
let(:merge_request) { create(:merge_request) }
- let(:environment) do
- create(:environment, project: merge_request.target_project)
- end
-
it 'returns visible deployments' do
+ envs = create_list(:environment, 3, project: merge_request.target_project)
+
created = create(
:deployment,
:created,
project: merge_request.target_project,
- environment: environment
+ environment: envs[0]
)
success = create(
:deployment,
:success,
project: merge_request.target_project,
- environment: environment
+ environment: envs[1]
)
failed = create(
:deployment,
:failed,
project: merge_request.target_project,
- environment: environment
+ environment: envs[2]
)
- merge_request.deployment_merge_requests.create!(deployment: created)
- merge_request.deployment_merge_requests.create!(deployment: success)
- merge_request.deployment_merge_requests.create!(deployment: failed)
+ merge_request_relation = MergeRequest.where(id: merge_request.id)
+ created.link_merge_requests(merge_request_relation)
+ success.link_merge_requests(merge_request_relation)
+ failed.link_merge_requests(merge_request_relation)
expect(merge_request.recent_visible_deployments).to eq([failed, success])
end
it 'only returns a limited number of deployments' do
20.times do
+ environment = create(:environment, project: merge_request.target_project)
deploy = create(
:deployment,
:success,
@@ -3742,7 +3742,7 @@ describe MergeRequest do
environment: environment
)
- merge_request.deployment_merge_requests.create!(deployment: deploy)
+ deploy.link_merge_requests(MergeRequest.where(id: merge_request.id))
end
expect(merge_request.recent_visible_deployments.count).to eq(10)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 12f753d34d2..10da73104a4 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -538,18 +538,6 @@ describe User, :do_not_mock_admin_mode do
expect(user).to be_valid
end
- context 'when feature flag is turned off' do
- before do
- stub_feature_flags(email_restrictions: false)
- end
-
- it 'does accept the email address' do
- user = build(:user, email: 'info+1@test.com')
-
- expect(user).to be_valid
- end
- end
-
context 'when created_by_id is set' do
it 'does accept the email address' do
user = build(:user, email: 'info+1@test.com', created_by_id: 1)
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index b820b227fff..ef2415a0cde 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -439,7 +439,7 @@ describe API::Deployments do
let!(:merge_request3) { create(:merge_request, source_project: project2, target_project: project2) }
it 'returns the relevant merge requests linked to a deployment for a project' do
- deployment.merge_requests << [merge_request1, merge_request2]
+ deployment.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
subject
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 3ec5f380390..5c925d2a32e 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -3,26 +3,26 @@
require 'spec_helper'
describe API::Issues do
- let_it_be(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:author) { create(:author) }
- let_it_be(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
- let(:issue_title) { 'foo' }
- let(:issue_description) { 'closed' }
- let(:no_milestone_title) { 'None' }
- let(:any_milestone_title) { 'Any' }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:author) { create(:author) }
+ let_it_be(:assignee) { create(:assignee) }
+ let_it_be(:issue_title) { 'foo' }
+ let_it_be(:issue_description) { 'closed' }
+ let_it_be(:no_milestone_title) { 'None' }
+ let_it_be(:any_milestone_title) { 'Any' }
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe 'GET /groups/:id/issues' do
- let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
- let!(:private_mrs_project) do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
+ let_it_be(:private_mrs_project) do
create(:project, :public, :repository, creator_id: user.id, namespace: group, merge_requests_access_level: ProjectFeature::PRIVATE)
end
@@ -455,6 +455,29 @@ describe API::Issues do
it_behaves_like 'labeled issues with labels and label_name params'
end
+ context 'with archived projects' do
+ let_it_be(:archived_issue) do
+ create(
+ :issue, author: user, assignees: [user],
+ project: create(:project, :public, :archived, creator_id: user.id, namespace: group)
+ )
+ end
+
+ it 'returns only non archived projects issues' do
+ get api(base_url, user)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns issues from archived projects if non_archived it set to false' do
+ get api(base_url, user), params: { non_archived: false }
+
+ expect_paginated_array_response(
+ [archived_issue.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id]
+ )
+ end
+ end
+
it 'returns an array of issues found by iids' do
get api(base_url, user), params: { iids: [group_issue.iid] }
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 00169c1529f..06878f57d43 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -780,28 +780,20 @@ describe API::Issues do
end
context 'filtering by non_archived' do
- let_it_be(:group1) { create(:group) }
- let_it_be(:archived_project) { create(:project, :archived, namespace: group1) }
- let_it_be(:active_project) { create(:project, namespace: group1) }
- let_it_be(:issue1) { create(:issue, project: active_project) }
- let_it_be(:issue2) { create(:issue, project: active_project) }
- let_it_be(:issue3) { create(:issue, project: archived_project) }
+ let_it_be(:archived_project) { create(:project, :archived, creator_id: user.id, namespace: user.namespace) }
+ let_it_be(:archived_issue) { create(:issue, author: user, project: archived_project) }
+ let_it_be(:active_issue) { create(:issue, author: user, project: project) }
- before do
- archived_project.add_developer(user)
- active_project.add_developer(user)
- end
-
- it 'returns issues from non archived projects only by default' do
- get api("/groups/#{group1.id}/issues", user), params: { scope: 'all' }
+ it 'returns issues from non archived projects by default' do
+ get api('/issues', user)
- expect_paginated_array_response([issue2.id, issue1.id])
+ expect_paginated_array_response(active_issue.id, issue.id, closed_issue.id)
end
- it 'returns issues from archived and non archived projects when non_archived is false' do
- get api("/groups/#{group1.id}/issues", user), params: { non_archived: false, scope: 'all' }
+ it 'returns issues from archived project with non_archived set as false' do
+ get api("/issues", user), params: { non_archived: false }
- expect_paginated_array_response([issue3.id, issue2.id, issue1.id])
+ expect_paginated_array_response(active_issue.id, archived_issue.id, issue.id, closed_issue.id)
end
end
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index e0a673514a8..0bdb9ea6bf9 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -129,16 +129,6 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
end
-
- context 'for snippet_blobs scope' do
- before do
- create(:snippet, :public, title: 'awesome snippet', content: 'snippet content')
-
- get api('/search', user), params: { scope: 'snippet_blobs', search: 'content' }
- end
-
- it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
- end
end
end
diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
index 419d74c298a..dfc3898af24 100644
--- a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
@@ -33,5 +33,11 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
expect(cop.offenses.map(&:line)).to eq([1])
end
end
+
+ it 'does not register an offense when a `NOT VALID` foreign key is added' do
+ inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id, validate: false); end')
+
+ expect(cop.offenses).to be_empty
+ end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 4465e1b1bd1..7a251e03e51 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -110,6 +110,31 @@ describe Issues::CreateService do
end
end
+ context 'when labels is nil' do
+ let(:opts) do
+ { title: 'Title',
+ description: 'Description',
+ labels: nil }
+ end
+
+ it 'does not assign label' do
+ expect(issue.labels).to be_empty
+ end
+ end
+
+ context 'when labels is nil and label_ids is present' do
+ let(:opts) do
+ { title: 'Title',
+ description: 'Description',
+ labels: nil,
+ label_ids: labels.map(&:id) }
+ end
+
+ it 'assigns group labels' do
+ expect(issue.labels).to match_array labels
+ end
+ end
+
context 'when milestone belongs to different project' do
let(:milestone) { create(:milestone) }