Commit 5e0cb86c authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '37081-epics-premium-final' into 'master'

Move epics to premium

Closes #37081

See merge request gitlab-org/gitlab!25184
parents 3a9007d2 a65ea302
# Epics API **(ULTIMATE)**
# Epics API **(PREMIUM)**
Every API call to epic must be authenticated.
......
......@@ -30,6 +30,7 @@ export default {
computed: {
...mapState([
'canUpdate',
'allowSubEpics',
'sidebarCollapsed',
'participants',
'startDateSourcingMilestoneTitle',
......@@ -186,7 +187,7 @@ export default {
@toggleCollapse="toggleSidebar({ sidebarCollapsed })"
/>
<sidebar-labels :can-update="canUpdate" :sidebar-collapsed="sidebarCollapsed" />
<div class="block ancestors">
<div v-if="allowSubEpics" class="block ancestors">
<ancestors-tree :ancestors="ancestors" :is-fetching="false" />
</div>
<div class="block participants">
......
......@@ -3,7 +3,7 @@ import { mapActions } from 'vuex';
import Cookies from 'js-cookie';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import createStore from './store';
import EpicApp from './components/epic_app.vue';
......@@ -54,7 +54,10 @@ export default (epicCreate = false) => {
store,
components: { EpicApp },
created() {
this.setEpicMeta(epicMeta);
this.setEpicMeta({
...epicMeta,
allowSubEpics: parseBoolean(el.dataset.allowSubEpics),
});
this.setEpicData(epicData);
},
methods: {
......
import $ from 'jquery';
import { parseBoolean } from '~/lib/utils/common_utils';
import initRelatedItemsTree from 'ee/related_items_tree/related_items_tree_bundle';
import initRoadmap from 'ee/roadmap/roadmap_bundle';
export default class EpicTabs {
constructor() {
......@@ -8,13 +8,32 @@ export default class EpicTabs {
this.wrapper = document.querySelector('.content-wrapper .container-fluid:not(.breadcrumbs)');
this.epicTabs = this.wrapper.querySelector('.js-epic-tabs-container');
this.discussionFilterContainer = this.epicTabs.querySelector('.js-discussion-filter-container');
const allowSubEpics = parseBoolean(this.epicTabs.dataset.allowSubEpics);
initRelatedItemsTree();
// We need to execute Roadmap tab related
// logic only when sub-epics feature is available.
if (allowSubEpics) {
this.roadmapTabLoaded = false;
this.loadRoadmapBundle();
this.bindEvents();
}
}
/**
* This method loads Roadmap app bundle asynchronously.
*
* @param {boolean} allowSubEpics
*/
loadRoadmapBundle() {
import('ee/roadmap/roadmap_bundle')
.then(roadmapBundle => {
this.initRoadmap = roadmapBundle.default;
})
.catch(() => {});
}
bindEvents() {
const $roadmapTab = $('#roadmap-tab', this.epicTabs);
......@@ -26,7 +45,7 @@ export default class EpicTabs {
onRoadmapShow() {
this.wrapper.classList.remove('container-limited');
if (!this.roadmapTabLoaded) {
initRoadmap();
this.initRoadmap();
this.roadmapTabLoaded = true;
}
}
......
......@@ -19,6 +19,7 @@ export default () => ({
canUpdate: false,
canDestroy: false,
canAdmin: false,
allowSubEpics: false,
// Epic Information
epicId: 0,
......
......@@ -17,7 +17,7 @@ export default {
EpicActionsSplitButton,
},
computed: {
...mapState(['parentItem', 'descendantCounts']),
...mapState(['parentItem', 'descendantCounts', 'allowSubEpics']),
totalEpicsCount() {
return this.descendantCounts.openedEpics + this.descendantCounts.closedEpics;
},
......@@ -51,7 +51,7 @@ export default {
<div class="card-header d-flex px-2">
<div class="d-inline-flex flex-grow-1 lh-100 align-middle">
<gl-tooltip :target="() => $refs.countBadge">
<p class="font-weight-bold m-0">
<p v-if="allowSubEpics" class="font-weight-bold m-0">
{{ __('Epics') }} &#8226;
<span class="text-secondary-400 font-weight-normal"
>{{
......@@ -75,11 +75,11 @@ export default {
</p>
</gl-tooltip>
<div ref="countBadge" class="issue-count-badge">
<span class="d-inline-flex align-items-center">
<span v-if="allowSubEpics" class="d-inline-flex align-items-center">
<icon :size="16" name="epic" class="text-secondary mr-1" />
{{ totalEpicsCount }}
</span>
<span class="ml-2 d-inline-flex align-items-center">
<span class="d-inline-flex align-items-center" :class="{ 'ml-2': allowSubEpics }">
<icon :size="16" name="issues" class="text-secondary mr-1" />
{{ totalIssuesCount }}
</span>
......@@ -88,6 +88,7 @@ export default {
<div class="d-inline-flex js-button-container">
<template v-if="parentItem.userPermissions.adminEpic">
<epic-actions-split-button
v-if="allowSubEpics"
class="qa-add-epics-button"
@showAddEpicForm="showAddEpicForm"
@showCreateEpicForm="showCreateEpicForm"
......
......@@ -16,7 +16,15 @@ export default () => {
return false;
}
const { id, iid, fullPath, autoCompleteEpics, autoCompleteIssues, userSignedIn } = el.dataset;
const {
id,
iid,
fullPath,
autoCompleteEpics,
autoCompleteIssues,
userSignedIn,
allowSubEpics,
} = el.dataset;
const initialData = JSON.parse(el.dataset.initial);
Vue.component('tree-root', TreeRoot);
......@@ -46,6 +54,7 @@ export default () => {
autoCompleteEpics: parseBoolean(autoCompleteEpics),
autoCompleteIssues: parseBoolean(autoCompleteIssues),
userSignedIn: parseBoolean(userSignedIn),
allowSubEpics: parseBoolean(allowSubEpics),
});
},
methods: {
......
......@@ -12,6 +12,7 @@ export default {
autoCompleteIssues,
projectsEndpoint,
userSignedIn,
allowSubEpics,
},
) {
state.epicsEndpoint = epicsEndpoint;
......@@ -20,6 +21,7 @@ export default {
state.autoCompleteIssues = autoCompleteIssues;
state.projectsEndpoint = projectsEndpoint;
state.userSignedIn = userSignedIn;
state.allowSubEpics = allowSubEpics;
},
[types.SET_INITIAL_PARENT_ITEM](state, data) {
......
......@@ -36,6 +36,7 @@ export default () => ({
showCreateEpicForm: false,
autoCompleteEpics: false,
autoCompleteIssues: false,
allowSubEpics: false,
removeItemModalProps: {
parentItem: {},
item: {},
......
......@@ -3,8 +3,8 @@
class Groups::EpicLinksController < Groups::ApplicationController
include EpicRelations
before_action :check_epics_available!, only: :index
before_action :check_subepics_available!, only: [:create, :destroy, :update]
before_action :check_epics_available!, only: [:index, :destroy]
before_action :check_subepics_available!, only: [:create, :update]
def update
result = EpicLinks::UpdateService.new(child_epic, current_user, params[:epic]).execute
......
......@@ -66,6 +66,7 @@ class License < ApplicationRecord
design_management
disable_name_update_for_users
email_additional_text
epics
extended_audit_events
external_authorization_service_api_management
feature_flags
......@@ -111,7 +112,6 @@ class License < ApplicationRecord
credentials_inventory
dast
dependency_scanning
epics
group_ip_restriction
group_level_compliance_dashboard
incident_management
......
......@@ -30,6 +30,6 @@ class LinkedEpicEntity < Grape::Entity
end
expose :can_admin do |epic|
can?(request.current_user, :admin_epic, epic)
can?(request.current_user, :admin_epic_link, epic)
end
end
......@@ -26,8 +26,8 @@ module EpicLinks
def permission_to_remove_relation?
child_epic.present? &&
parent_epic.present? &&
can?(current_user, :admin_epic_link, parent_epic) &&
can?(current_user, :admin_epic_link, child_epic)
can?(current_user, :admin_epic, parent_epic) &&
can?(current_user, :admin_epic, child_epic)
end
def not_found_message
......
......@@ -3,6 +3,9 @@
- @content_class = "limit-container-width" unless fluid_layout
- epic_reference = @epic.to_reference
- sub_epics_feature_available = @group.feature_available?(:subepics)
- allow_sub_epics = sub_epics_feature_available ? 'true' : 'false'
- add_to_breadcrumbs _("Epics"), group_epics_path(@group)
- breadcrumb_title epic_reference
......@@ -11,14 +14,19 @@
- page_card_attributes @epic.card_attributes
#epic-app-root{ data: epic_show_app_data(@epic) }
#epic-app-root{ data: epic_show_app_data(@epic),
'data-allow-sub-epics' => allow_sub_epics }
.epic-tabs-holder
.epic-tabs-container.js-epic-tabs-container
.epic-tabs-container.js-epic-tabs-container{ data: { allow_sub_epics: allow_sub_epics } }
%ul.epic-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.tree-tab
%a#tree-tab.active{ href: '#tree', data: { toggle: 'tab' } }
- if sub_epics_feature_available
= _('Epics and Issues')
- else
= _('Issues')
- if sub_epics_feature_available
%li.roadmap-tab
%a#roadmap-tab{ href: '#roadmap', data: { toggle: 'tab' } }
= _('Roadmap')
......@@ -33,7 +41,9 @@
auto_complete_epics: 'true',
auto_complete_issues: 'true',
user_signed_in: current_user.present? ? 'true' : 'false',
allow_sub_epics: allow_sub_epics,
initial: issuable_initial_data(@epic).to_json } }
- if sub_epics_feature_available
#roadmap.tab-pane
.row
%section.col-md-12
......
---
title: Add single-level Epics to EE Premium
merge_request: 25184
author:
type: added
......@@ -176,6 +176,8 @@ describe Groups::EpicLinksController do
epic1.update(parent: parent_epic)
end
let(:features_when_forbidden) { { epics: false } }
subject { delete :destroy, params: { group_id: group, epic_id: parent_epic.to_param, id: epic1.id } }
it_behaves_like 'unlicensed subepics action'
......
......@@ -98,6 +98,7 @@ describe 'Epic Issues', :js do
visit_epic
end
context 'handling epics' do
it 'user can display create new epic form by clicking the dropdown item' do
expect(page).not_to have_selector('input[placeholder="New epic title"]')
......@@ -106,7 +107,9 @@ describe 'Epic Issues', :js do
expect(page).to have_selector('input[placeholder="New epic title"]')
end
end
context 'handling epic issues' do
it 'user can see all issues of the group and delete the associations' do
within('.related-items-tree-container ul.related-items-list') do
expect(page).to have_selector('li.js-item-type-issue', count: 2)
......@@ -124,23 +127,6 @@ describe 'Epic Issues', :js do
end
end
it 'user can see all epics of the group and delete the associations' do
within('.related-items-tree-container ul.related-items-list') do
expect(page).to have_selector('li.js-item-type-epic', count: 2)
expect(page).to have_content(nested_epics[0].title)
expect(page).to have_content(nested_epics[1].title)
first('li.js-item-type-epic button.js-issue-item-remove-button').click
end
first('#item-remove-confirmation .modal-footer .btn-danger').click
wait_for_requests
within('.related-items-tree-container ul.related-items-list') do
expect(page).to have_selector('li.js-item-type-epic', count: 1)
end
end
it 'user cannot add new issues to the epic from another group' do
add_issues("#{issue_invalid.to_reference(full: true)}")
......@@ -167,6 +153,26 @@ describe 'Epic Issues', :js do
expect(page).to have_selector('.gl-field-error')
expect(find('.gl-field-error')).to have_text("Issue cannot be found.")
end
end
context 'handling epic links' do
context 'when subepics feature is enabled' do
it 'user can see all epics of the group and delete the associations' do
within('.related-items-tree-container ul.related-items-list') do
expect(page).to have_selector('li.js-item-type-epic', count: 2)
expect(page).to have_content(nested_epics[0].title)
expect(page).to have_content(nested_epics[1].title)
first('li.js-item-type-epic button.js-issue-item-remove-button').click
end
first('#item-remove-confirmation .modal-footer .btn-danger').click
wait_for_requests
within('.related-items-tree-container ul.related-items-list') do
expect(page).to have_selector('li.js-item-type-epic', count: 1)
end
end
it 'user cannot add new epic that does not exist' do
add_epics("&123")
......@@ -175,6 +181,18 @@ describe 'Epic Issues', :js do
expect(find('.gl-field-error')).to have_text("Epic cannot be found.")
end
it 'user can add new epics to the epic' do
references = "#{epic_to_add.to_reference(full: true)}"
add_epics(references)
expect(page).not_to have_selector('.gl-field-error')
expect(page).not_to have_content("Epic cannot be found.")
within('.related-items-tree-container ul.related-items-list') do
expect(page).to have_selector('li.js-item-type-epic', count: 3)
end
end
context 'when epics are nested too deep' do
let(:epic1) { create(:epic, group: group, parent_id: epic.id) }
let(:epic2) { create(:epic, group: group, parent_id: epic1.id) }
......@@ -182,9 +200,6 @@ describe 'Epic Issues', :js do
let(:epic4) { create(:epic, group: group, parent_id: epic3.id) }
before do
stub_licensed_features(epics: true, subepics: true)
sign_in(user)
visit group_epic_path(group, epic4)
wait_for_requests
......@@ -202,6 +217,18 @@ describe 'Epic Issues', :js do
expect(find('.gl-field-error')).to have_text("This epic can't be added because the parent is already at the maximum depth from its most distant ancestor")
end
end
end
context 'when subepics feature is disabled' do
it 'user can not add new epics to the epic' do
stub_licensed_features(epics: true, subepics: false)
visit_epic
expect(page).not_to have_selector('.related-items-tree-container .js-add-epics-button')
end
end
end
context 'with epic_new_issue feature flag enabled' do
before do
......@@ -222,17 +249,5 @@ describe 'Epic Issues', :js do
end
end
end
it 'user can add new epics to the epic' do
references = "#{epic_to_add.to_reference(full: true)}"
add_epics(references)
expect(page).not_to have_selector('.gl-field-error')
expect(page).not_to have_content("Epic cannot be found.")
within('.related-items-tree-container ul.related-items-list') do
expect(page).to have_selector('li.js-item-type-epic', count: 3)
end
end
end
end
......@@ -5,7 +5,9 @@ require 'spec_helper'
describe 'Epic show', :js do
let(:user) { create(:user, name: 'Rick Sanchez', username: 'rick.sanchez') }
let(:group) { create(:group, :public) }
let(:public_project) { create(:project, :public, group: group) }
let(:label) { create(:group_label, group: group, title: 'bug') }
let(:public_issue) { create(:issue, project: public_project) }
let(:note_text) { 'Contemnit enim disserendi elegantiam.' }
let(:epic_title) { 'Sample epic' }
......@@ -22,43 +24,24 @@ describe 'Epic show', :js do
let!(:not_child) { create(:epic, group: group, title: 'not child epic', description: markdown, author: user, start_date: 50.days.ago, end_date: 10.days.ago) }
let!(:child_epic_a) { create(:epic, group: group, title: 'Child epic A', description: markdown, parent: epic, start_date: 50.days.ago, end_date: 10.days.ago) }
let!(:child_epic_b) { create(:epic, group: group, title: 'Child epic B', description: markdown, parent: epic, start_date: 100.days.ago, end_date: 20.days.ago) }
let!(:child_issue_a) { create(:epic_issue, epic: epic, issue: public_issue, relative_position: 1) }
before do
group.add_developer(user)
stub_licensed_features(epics: true)
stub_licensed_features(epics: true, subepics: true)
sign_in(user)
visit group_epic_path(group, epic)
end
describe 'when sub-epics feature is available' do
describe 'Epic metadata' do
it 'shows epic status, date and author in header' do
page.within('.epic-page-container .detail-page-header-body') do
expect(find('.issuable-status-box > span')).to have_content('Open')
expect(find('.issuable-meta')).to have_content('Opened just now by')
expect(find('.issuable-meta .js-user-avatar-link-username')).to have_content('Rick Sanchez')
end
end
it 'shows epic title and description' do
page.within('.epic-page-container .detail-page-description') do
expect(find('.title-container .title')).to have_content(epic_title)
expect(find('.description .md')).to have_content(markdown.squish)
end
end
it 'shows epic tabs' do
it 'shows epic tabs `Epics and Issues` and `Roadmap`' do
page.within('.js-epic-tabs-container') do
expect(find('.epic-tabs #tree-tab')).to have_content('Epics and Issues')
expect(find('.epic-tabs #roadmap-tab')).to have_content('Roadmap')
end
end
it 'shows epic thread filter dropdown' do
page.within('.js-noteable-awards') do
expect(find('.js-discussion-filter-container #discussion-filter-dropdown')).to have_content('Show all activity')
end
end
end
describe 'Epics and Issues tab' do
......@@ -101,4 +84,56 @@ describe 'Epic show', :js do
expect(find('.content-wrapper .container-fluid:not(.breadcrumbs)')[:class]).not_to include('container-limited')
end
end
end
describe 'when sub-epics feature not is available' do
before do
stub_licensed_features(epics: true, subepics: false)
visit group_epic_path(group, epic)
end
describe 'Epic metadata' do
it 'shows epic tab `Issues`' do
page.within('.js-epic-tabs-container') do
expect(find('.epic-tabs #tree-tab')).to have_content('Issues')
end
end
end
describe 'Issues tab' do
it 'shows Related items tree with child epics' do
page.within('.js-epic-tabs-content #tree') do
expect(page).to have_selector('.related-items-tree-container')
page.within('.related-items-tree-container') do
expect(page.find('.issue-count-badge')).to have_content('1')
end
end
end
end
end
describe 'Epic metadata' do
it 'shows epic status, date and author in header' do
page.within('.epic-page-container .detail-page-header-body') do
expect(find('.issuable-status-box > span')).to have_content('Open')
expect(find('.issuable-meta')).to have_content('Opened just now by')
expect(find('.issuable-meta .js-user-avatar-link-username')).to have_content('Rick Sanchez')
end
end
it 'shows epic title and description' do
page.within('.epic-page-container .detail-page-description') do
expect(find('.title-container .title')).to have_content(epic_title)
expect(find('.description .md')).to have_content(markdown.squish)
end
end
it 'shows epic thread filter dropdown' do
page.within('.js-noteable-awards') do
expect(find('.js-discussion-filter-container #discussion-filter-dropdown')).to have_content('Show all activity')
end
end
end
end
......@@ -203,8 +203,26 @@ describe('EpicSidebarComponent', () => {
expect(vm.$el.querySelector('.js-labels-block')).not.toBeNull();
});
describe('when sub-epics feature is available', () => {
it('renders ancestors list', done => {
store.dispatch('toggleSidebarFlag', false);
store.dispatch('setEpicMeta', {
...mockEpicMeta,
allowSubEpics: false,
});
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.block.ancestors')).toBeNull();
})
.then(done)
.catch(done.fail);
});
});
describe('when sub-epics feature is not available', () => {
it('does not render ancestors list', done => {
store.dispatch('toggleSidebarFlag', false);
vm.$nextTick()
.then(() => {
......@@ -229,6 +247,7 @@ describe('EpicSidebarComponent', () => {
.then(done)
.catch(done.fail);
});
});
it('renders participants list element', () => {
expect(vm.$el.querySelector('.block.participants')).not.toBeNull();
......
......@@ -5,9 +5,12 @@ const metaFixture = getJSONFixture('epic/mock_meta.json');
const meta = JSON.parse(metaFixture.meta);
const initial = JSON.parse(metaFixture.initial);
export const mockEpicMeta = convertObjectPropsToCamelCase(meta, {
export const mockEpicMeta = {
...convertObjectPropsToCamelCase(meta, {
deep: true,
});
}),
allowSubEpics: true,
};
export const mockEpicData = convertObjectPropsToCamelCase(
Object.assign({}, getJSONFixture('epic/mock_data.json'), initial, {
......
......@@ -9,6 +9,7 @@ import EpicActionsSplitButton from 'ee/related_items_tree/components/epic_action
import Icon from '~/vue_shared/components/icon.vue';
import {
mockInitialConfig,
mockParentItem,
mockQueryResponse,
} from '../../../javascripts/related_items_tree/mock_data';
......@@ -17,6 +18,7 @@ const createComponent = ({ slots } = {}) => {
const store = createDefaultStore();
const children = epicUtils.processQueryResponse(mockQueryResponse.data.group);
store.dispatch('setInitialConfig', mockInitialConfig);
store.dispatch('setInitialParentItem', mockParentItem);
store.dispatch('setItemChildren', {
parentItem: mockParentItem,
......@@ -167,6 +169,7 @@ describe('RelatedItemsTree', () => {
expect(badgesContainerEl.isVisible()).toBe(true);
});
describe('when sub-epics feature is available', () => {
it('renders epics count and icon', () => {
const epicsEl = wrapper.findAll('.issue-count-badge > span').at(0);
const epicIcon = epicsEl.find(Icon);
......@@ -176,6 +179,34 @@ describe('RelatedItemsTree', () => {
expect(epicIcon.props('name')).toBe('epic');
});
it('renders `Add an epic` dropdown button', () => {
expect(findEpicsSplitButton().isVisible()).toBe(true);
});
});
describe('when sub-epics feature is not available', () => {
beforeEach(() => {
wrapper.vm.$store.commit('SET_INITIAL_CONFIG', {
...mockInitialConfig,
allowSubEpics: false,
});
return wrapper.vm.$nextTick();
});
it('does not render epics count and icon', () => {
const countBadgesEl = wrapper.findAll('.issue-count-badge > span');
const badgeIcon = countBadgesEl.at(0).find(Icon);
expect(countBadgesEl.length).toBe(1);
expect(badgeIcon.props('name')).toBe('issues');
});
it('does not render `Add an epic` dropdown button', () => {
expect(findEpicsSplitButton().exists()).toBe(false);
});
});
it('renders issues count and icon', () => {
const issuesEl = wrapper.findAll('.issue-count-badge > span').at(1);
const issueIcon = issuesEl.find(Icon);
......@@ -185,10 +216,6 @@ describe('RelatedItemsTree', () => {
expect(issueIcon.props('name')).toBe('issues');
});
it('renders `Add an epic` dropdown button', () => {
expect(findEpicsSplitButton().isVisible()).toBe(true);
});
it('renders `Add an issue` dropdown button', () => {
const addIssueBtn = findAddIssuesButton();
......
......@@ -19,6 +19,7 @@ describe('RelatedItemsTree', () => {
issuesEndpoint: '/bar',
autoCompleteEpics: true,
autoCompleteIssues: false,
allowSubEpics: true,
};
mutations[types.SET_INITIAL_CONFIG](state, data);
......@@ -27,6 +28,7 @@ describe('RelatedItemsTree', () => {
expect(state).toHaveProperty('issuesEndpoint', '/bar');
expect(state).toHaveProperty('autoCompleteEpics', true);
expect(state).toHaveProperty('autoCompleteIssues', false);
expect(state).toHaveProperty('allowSubEpics', true);
});
});
......
......@@ -7,6 +7,7 @@ export const mockInitialConfig = {
autoCompleteEpics: true,
autoCompleteIssues: false,
userSignedIn: true,
allowSubEpics: true,
};
export const mockParentItem = {
......
......@@ -1451,7 +1451,8 @@ describe Project do
before do
allow(License).to receive(:current).and_return(global_license)
allow(global_license).to receive(:features).and_return([
:epics, # Gold only
:subepics, # Gold only
:epics, # Silver and up
:service_desk, # Silver and up
:audit_events, # Bronze and up
:geo # Global feature, should not be checked at namespace level
......@@ -1477,7 +1478,7 @@ describe Project do
let(:plan_license) { :silver }
it 'filters for silver features' do
is_expected.to contain_exactly(:service_desk, :audit_events, :geo)
is_expected.to contain_exactly(:service_desk, :audit_events, :geo, :epics)
end
end
......@@ -1485,7 +1486,7 @@ describe Project do
let(:plan_license) { :gold }
it 'filters for gold features' do
is_expected.to contain_exactly(:epics, :service_desk, :audit_events, :geo)
is_expected.to contain_exactly(:epics, :service_desk, :audit_events, :geo, :subepics)
end
end
......@@ -1502,7 +1503,7 @@ describe Project do
let(:project) { create(:project, :public, group: group) }
it 'includes all features in global license' do
is_expected.to contain_exactly(:epics, :service_desk, :audit_events, :geo)
is_expected.to contain_exactly(:epics, :service_desk, :audit_events, :geo, :subepics)
end
end
end
......@@ -1510,7 +1511,7 @@ describe Project do
context 'when namespace should not be checked' do
it 'includes all features in global license' do
is_expected.to contain_exactly(:epics, :service_desk, :audit_events, :geo)
is_expected.to contain_exactly(:epics, :service_desk, :audit_events, :geo, :subepics)
end
end
......
......@@ -45,9 +45,9 @@ describe EpicLinks::DestroyService do
described_class.new(child_epic, user).execute
end
context 'when subepics feature is disabled' do
context 'when epics feature is disabled' do
before do
stub_licensed_features(epics: true, subepics: false)
stub_licensed_features(epics: false)
end
subject { remove_epic_relation(child_epic) }
......@@ -55,9 +55,9 @@ describe EpicLinks::DestroyService do
include_examples 'returns not found error'
end
context 'when subepics feature is enabled' do
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true, subepics: true)
stub_licensed_features(epics: true)
end
context 'when the user has no permissions to remove epic relation' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment