Commit 44f37239 authored by Michael Kozono's avatar Michael Kozono

Merge branch '352495-query-related-epics-of-an-epic' into 'master'

Add method to query related epics of an epic

See merge request gitlab-org/gitlab!81422
parents 350f097e c01f8b2a
......@@ -367,6 +367,12 @@ module EE
)
])
end
def epics_readable_by_user(epics, user = nil)
DeclarativePolicy.user_scope do
epics.select { |epic| epic.readable_by?(user) }
end
end
end
def text_color
......@@ -571,5 +577,30 @@ module EE
errors.add :confidential, _('A non-confidential epic cannot be assigned to a confidential parent epic')
end
end
def related_epics(current_user, preload: nil)
select_for_related_epics =
::Epic.select(['epics.*', 'related_epic_links.id AS related_epic_link_id',
'related_epic_links.link_type as related_epic_link_type_value',
'related_epic_links.target_id as related_epic_link_source_id',
'related_epic_links.created_at as related_epic_link_created_at',
'related_epic_links.updated_at as related_epic_link_updated_at'])
target_epics = select_for_related_epics
.joins("INNER JOIN related_epic_links ON related_epic_links.target_id = epics.id")
.where(related_epic_links: { source_id: id })
source_epics = select_for_related_epics
.joins("INNER JOIN related_epic_links ON related_epic_links.source_id = epics.id")
.where(related_epic_links: { target_id: id })
related_epics = ::Epic.from_union([target_epics, source_epics])
.preload(preload)
.reorder('related_epic_link_id')
related_epics = yield related_epics if block_given?
self.class.epics_readable_by_user(related_epics, current_user)
end
end
end
......@@ -862,4 +862,170 @@ RSpec.describe Epic do
end
end
end
describe '.epics_readable_by_user' do
let_it_be(:visible_epic) { create(:epic) }
let_it_be(:confidential_epic) { create(:epic, :confidential) }
subject { described_class.epics_readable_by_user(epics, user) }
before do
stub_licensed_features(epics: true)
end
let(:epics) { [visible_epic]}
context 'with an admin when admin mode is enabled', :enable_admin_mode do
let(:user) { build(:user, admin: true) }
it { expect(subject).to match_array(epics) }
end
context 'with an admin when admin mode is disabled' do
let(:user) { build(:user, admin: true) }
it 'returns the epics readable by the admin' do
expect(visible_epic).to receive(:readable_by?).with(user).and_return(true)
expect(subject).to match_array(epics)
end
it 'returns no epics when not given access' do
allow(visible_epic).to receive(:readable_by?).with(user).and_return(false)
expect(subject).to be_empty
end
end
context 'with a regular user' do
let(:user) { build(:user) }
it 'returns the epics readable by the user' do
expect(visible_epic).to receive(:readable_by?).with(user).and_return(true)
expect(subject).to match_array(epics)
end
it 'returns an empty array when no epics are readable' do
expect(visible_epic).to receive(:readable_by?).with(user).and_return(false)
expect(subject).to be_empty
end
end
context 'without a regular user' do
let(:user) { nil }
let(:epics) { [confidential_epic, visible_epic] }
it 'returns epics that are publicly visible' do
expect(subject).to contain_exactly(visible_epic)
end
end
it 'avoids N+1 queries when authorizing a list of epics', :request_store do
user = create(:user)
group = create(:group, :private).tap { |group| group.add_maintainer(user) }
epic = create(:epic, group: group)
control = ActiveRecord::QueryRecorder.new { described_class.epics_readable_by_user([epic], user) }
new_group1 = create(:group, :public)
new_group3 = create(:group, :public)
new_group2 = create(:group, :private, parent: group)
new_epic1 = create(:epic, group: new_group1)
new_epic2 = create(:epic, group: new_group2)
new_epic3 = create(:epic, group: new_group3)
expect { described_class.epics_readable_by_user([epic, new_epic1, new_epic2, new_epic3], user) }
.not_to exceed_query_limit(control).with_threshold(4)
# Permission checks perform N+1 queries.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/353915 for more info.
end
context 'with group hierarchy' do
let_it_be(:user) { create(:user) }
let_it_be(:ancestor) { create(:group, :private) }
let_it_be(:base_group) { create(:group, :private, parent: ancestor) }
let_it_be(:subgroup) { create(:group, :private, parent: base_group) }
let_it_be(:epic1) { create(:epic, group: ancestor) }
let_it_be(:epic2) { create(:epic, group: base_group) }
let_it_be(:epic3) { create(:epic, group: subgroup) }
let_it_be(:epics) { [epic1, epic2, epic3] }
context 'when user is not a member' do
it 'returns no epic' do
expect(described_class.epics_readable_by_user(epics, user)).to be_empty
end
end
context 'when user is a reporter in the ancestor group' do
before do
ancestor.add_reporter(user)
end
it 'returns epics from all groups' do
expect(described_class.epics_readable_by_user(epics, user)).to match_array(epics)
end
end
context 'when user is a reporter in the base group' do
before do
base_group.add_reporter(user)
end
it 'returns epics in main group and its descendants' do
expect(described_class.epics_readable_by_user(epics, user)).to contain_exactly(epic2, epic3)
end
end
context 'when user is a reporter in the subgroup' do
before do
subgroup.add_reporter(user)
end
it 'returns epics in subgroup' do
expect(described_class.epics_readable_by_user(epics, user)).to contain_exactly(epic3)
end
end
end
end
describe '#related_epics' do
let_it_be_with_reload(:epic) { create(:epic) }
let_it_be(:user) { create(:user) }
let_it_be(:public_epic) { create(:epic) }
let_it_be(:confidential_epic) { create(:epic, :confidential) }
let_it_be(:sub_epic) { create(:epic, group: create(:group, parent: epic.group)) }
let_it_be(:private_epic) { create(:epic, group: create(:group, :private)) }
before do
epic.group.add_reporter(user)
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
[public_epic, confidential_epic, sub_epic, private_epic].each do |source_epic|
create(:related_epic_link, source: source_epic, target: epic)
end
end
it 'returns readable related epics of the epic' do
expect(epic.related_epics(user)).to contain_exactly(public_epic, sub_epic)
end
end
context 'when epics feature is disabled' do
before do
stub_licensed_features(epics: false)
end
it 'returns empty result' do
expect(epic.related_epics(user)).to be_empty
end
end
end
end
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