Commit 5321e6dc authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'ff-issues-list-service' into 'master'

Feature Flag Issues List Service

See merge request gitlab-org/gitlab!34456
parents 9affece7 90879740
# frozen_string_literal: true
module Projects
class FeatureFlagIssuesController < Projects::ApplicationController
include IssuableLinks
before_action :ensure_feature_enabled!
before_action :authorize_admin_feature_flag!
private
def list_service
::FeatureFlagIssues::ListService.new(feature_flag, current_user)
end
def feature_flag
project.operations_feature_flags.find_by_iid(params[:feature_flag_iid])
end
def ensure_feature_enabled!
render_404 unless Feature.enabled?(:feature_flags_issue_links, project)
end
end
end
...@@ -44,6 +44,9 @@ module EE ...@@ -44,6 +44,9 @@ module EE
has_many :vulnerability_links, class_name: 'Vulnerabilities::IssueLink', inverse_of: :issue has_many :vulnerability_links, class_name: 'Vulnerabilities::IssueLink', inverse_of: :issue
has_many :related_vulnerabilities, through: :vulnerability_links, source: :vulnerability has_many :related_vulnerabilities, through: :vulnerability_links, source: :vulnerability
has_many :feature_flag_issues
has_many :feature_flags, through: :feature_flag_issues, class_name: '::Operations::FeatureFlag'
validates :weight, allow_nil: true, numericality: { greater_than_or_equal_to: 0 } validates :weight, allow_nil: true, numericality: { greater_than_or_equal_to: 0 }
validate :validate_confidential_epic validate :validate_confidential_epic
......
# frozen_string_literal: true
class FeatureFlagIssue < ApplicationRecord
self.table_name = 'operations_feature_flags_issues'
belongs_to :feature_flag, class_name: '::Operations::FeatureFlag'
belongs_to :issue
end
...@@ -17,6 +17,8 @@ module Operations ...@@ -17,6 +17,8 @@ module Operations
has_many :scopes, class_name: 'Operations::FeatureFlagScope' has_many :scopes, class_name: 'Operations::FeatureFlagScope'
# strategies exists only for the second version # strategies exists only for the second version
has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy' has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy'
has_many :feature_flag_issues
has_many :issues, through: :feature_flag_issues
has_one :default_scope, -> { where(environment_scope: '*') }, class_name: 'Operations::FeatureFlagScope' has_one :default_scope, -> { where(environment_scope: '*') }, class_name: 'Operations::FeatureFlagScope'
validates :project, presence: true validates :project, presence: true
...@@ -61,6 +63,17 @@ module Operations ...@@ -61,6 +63,17 @@ module Operations
end end
end end
def related_issues(current_user, preload:)
issues = ::Issue
.select('issues.*, operations_feature_flags_issues.id AS link_id')
.joins(:feature_flag_issues)
.where('operations_feature_flags_issues.feature_flag_id = ?', id)
.order('operations_feature_flags_issues.id ASC')
.includes(preload)
Ability.issues_readable_by_user(issues, current_user)
end
private private
def version_associations def version_associations
......
# frozen_string_literal: true
class LinkedFeatureFlagIssueEntity < LinkedIssueEntity
expose :relation_path, override: true do |issue|
project_feature_flag_issue_path(issuable.project, issuable, issue.link_id)
end
expose :link_type do |_issue|
'relates_to'
end
end
# frozen_string_literal: true
class LinkedFeatureFlagIssueSerializer < BaseSerializer
entity LinkedFeatureFlagIssueEntity
end
# frozen_string_literal: true
module FeatureFlagIssues
class ListService < IssuableLinks::ListService
extend ::Gitlab::Utils::Override
private
def child_issuables
issuable.related_issues(current_user, preload: preload_for_collection)
end
override :serializer
def serializer
LinkedFeatureFlagIssueSerializer
end
end
end
...@@ -22,7 +22,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -22,7 +22,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
end end
resources :feature_flags, param: :iid resources :feature_flags, param: :iid do
resources :feature_flag_issues, only: [:index, :destroy], as: 'issues', path: 'issues'
end
resource :feature_flags_client, only: [] do resource :feature_flags_client, only: [] do
post :reset_token post :reset_token
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::FeatureFlagIssuesController do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
before_all do
project.add_developer(developer)
project.add_reporter(reporter)
end
before do
stub_licensed_features(feature_flags: true)
end
describe 'GET #index' do
def setup
feature_flag = create(:operations_feature_flag, project: project)
issue = create(:issue, project: project)
link = create(:feature_flag_issue, feature_flag: feature_flag, issue: issue)
[feature_flag, issue, link]
end
def get_request(project, feature_flag)
params = {
namespace_id: project.namespace,
project_id: project,
feature_flag_iid: feature_flag
}
get :index, params: params, format: :json
end
it 'returns linked issues' do
feature_flag, issue = setup
sign_in(developer)
get_request(project, feature_flag)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to match([a_hash_including({
'id' => issue.id
})])
end
it 'does not return linked issues for a reporter' do
feature_flag, _, _ = setup
sign_in(reporter)
get_request(project, feature_flag)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'orders by feature_flag_issue id' do
feature_flag = create(:operations_feature_flag, project: project)
issue_a = create(:issue, project: project)
issue_b = create(:issue, project: project)
create(:feature_flag_issue, feature_flag: feature_flag, issue: issue_b)
create(:feature_flag_issue, feature_flag: feature_flag, issue: issue_a)
sign_in(developer)
get_request(project, feature_flag)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.map { |issue| issue['id'] }).to eq([issue_b.id, issue_a.id])
end
it 'returns the correct relation_path when the feature flag is linked to multiple issues' do
feature_flag, issue_a, link_a = setup
issue_b = create(:issue, project: project)
link_b = create(:feature_flag_issue, feature_flag: feature_flag, issue: issue_b)
sign_in(developer)
get_request(project, feature_flag)
expect(response).to have_gitlab_http_status(:ok)
actual = json_response.sort_by { |issue| issue['id'] }.map { |issue| issue.slice('id', 'relation_path') }
expect(actual).to eq([{
'id' => issue_a.id,
'relation_path' => "/#{project.full_path}/-/feature_flags/#{feature_flag.iid}/issues/#{link_a.id}"
}, {
'id' => issue_b.id,
'relation_path' => "/#{project.full_path}/-/feature_flags/#{feature_flag.iid}/issues/#{link_b.id}"
}])
end
it 'returns the correct relation_path when multiple feature flags are linked to an issue' do
feature_flag_a, issue, link = setup
feature_flag_b = create(:operations_feature_flag, project: project)
create(:feature_flag_issue, feature_flag: feature_flag_b, issue: issue)
sign_in(developer)
get_request(project, feature_flag_a)
expect(response).to have_gitlab_http_status(:ok)
actual = json_response.map { |issue| issue.slice('id', 'relation_path') }
expect(actual).to eq([{
'id' => issue.id,
'relation_path' => "/#{project.full_path}/-/feature_flags/#{feature_flag_a.iid}/issues/#{link.id}"
}])
end
it 'returns the correct relation_path when there are multiple linked feature flags and issues' do
feature_flag_a, issue_a, _ = setup
feature_flag_b, issue_b, link_b = setup
feature_flag_c, issue_c, _ = setup
link_a = create(:feature_flag_issue, feature_flag: feature_flag_b, issue: issue_a)
link_c = create(:feature_flag_issue, feature_flag: feature_flag_b, issue: issue_c)
create(:feature_flag_issue, feature_flag: feature_flag_a, issue: issue_b)
create(:feature_flag_issue, feature_flag: feature_flag_a, issue: issue_c)
create(:feature_flag_issue, feature_flag: feature_flag_c, issue: issue_a)
create(:feature_flag_issue, feature_flag: feature_flag_c, issue: issue_b)
sign_in(developer)
get_request(project, feature_flag_b)
expect(response).to have_gitlab_http_status(:ok)
actual = json_response.sort_by { |issue| issue['id'] }.map { |issue| issue.slice('id', 'relation_path') }
expect(actual).to eq([{
'id' => issue_a.id,
'relation_path' => "/#{project.full_path}/-/feature_flags/#{feature_flag_b.iid}/issues/#{link_a.id}"
}, {
'id' => issue_b.id,
'relation_path' => "/#{project.full_path}/-/feature_flags/#{feature_flag_b.iid}/issues/#{link_b.id}"
}, {
'id' => issue_c.id,
'relation_path' => "/#{project.full_path}/-/feature_flags/#{feature_flag_b.iid}/issues/#{link_c.id}"
}])
end
it 'does not make N+1 queries' do
feature_flag, _, _ = setup
sign_in(developer)
control_count = ActiveRecord::QueryRecorder.new { get_request(project, feature_flag) }.count
issue_b = create(:issue, project: project)
issue_c = create(:issue, project: project)
create(:feature_flag_issue, feature_flag: feature_flag, issue: issue_b)
create(:feature_flag_issue, feature_flag: feature_flag, issue: issue_c)
expect { get_request(project, feature_flag) }.not_to exceed_query_limit(control_count)
end
it 'returns only issues readable by the user' do
feature_flag, _, _ = setup
issue_b = create(:issue, project: project, author: developer)
create(:feature_flag_issue, feature_flag: feature_flag, issue: issue_b)
allow(Ability).to receive(:issues_readable_by_user) do |issues, user, _filters|
issues.where(author: user)
end
sign_in(developer)
get_request(project, feature_flag)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.map { |issue| issue['id'] }).to eq([issue_b.id])
end
it 'returns not found when the feature is off' do
stub_feature_flags(feature_flags_issue_links: false)
feature_flag, _, _ = setup
sign_in(developer)
get_request(project, feature_flag)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when feature flags are unlicensed' do
before do
stub_licensed_features(feature_flags: false)
end
it 'does not return linked issues' do
feature_flag, _, _ = setup
sign_in(developer)
get_request(project, feature_flag)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
...@@ -48,6 +48,14 @@ RSpec.describe Projects::FeatureFlagsController do ...@@ -48,6 +48,14 @@ RSpec.describe Projects::FeatureFlagsController do
is_expected.to have_gitlab_http_status(:not_found) is_expected.to have_gitlab_http_status(:not_found)
end end
end end
context 'when the user is a reporter' do
let(:user) { reporter }
it 'responds with not found' do
is_expected.to have_gitlab_http_status(:not_found)
end
end
end end
describe 'GET #index.json' do describe 'GET #index.json' do
......
# frozen_string_literal: true
FactoryBot.define do
factory :feature_flag_issue do
feature_flag factory: :operations_feature_flag
issue
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LinkedFeatureFlagIssueEntity do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
before_all do
project.add_developer(developer)
end
describe '#as_json' do
it 'returns json' do
issue = create(:issue, project: project)
feature_flag = create(:operations_feature_flag, project: project)
link = create(:feature_flag_issue, feature_flag: feature_flag, issue: issue)
allow(issue).to receive(:link_id).and_return(link.id)
request = double('request')
allow(request).to receive(:current_user).and_return(developer)
allow(request).to receive(:issuable).and_return(feature_flag)
entity = described_class.new(issue, request: request, current_user: developer)
expect(entity.as_json.slice(:link_type, :relation_path)).to eq({
link_type: 'relates_to',
relation_path: "/#{project.full_path}/-/feature_flags/#{feature_flag.iid}/issues/#{link.id}"
})
end
end
end
...@@ -31,6 +31,8 @@ issues: ...@@ -31,6 +31,8 @@ issues:
- closed_by - closed_by
- epic_issue - epic_issue
- epic - epic
- feature_flag_issues
- feature_flags
- designs - designs
- design_versions - design_versions
- description_versions - description_versions
...@@ -569,6 +571,9 @@ self_managed_prometheus_alert_events: ...@@ -569,6 +571,9 @@ self_managed_prometheus_alert_events:
epic_issues: epic_issues:
- issue - issue
- epic - epic
feature_flag_issues:
- issue
- feature_flag
tracing_setting: tracing_setting:
- project - project
reviews: reviews:
......
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