Commit 148ee616 authored by Adam Hegyi's avatar Adam Hegyi

Merge branch 'pl-status-page-mvc-finder' into 'master'

Add incidents and comments finders for status page

See merge request gitlab-org/gitlab!26710
parents 59117474 257d120c
# frozen_string_literal: true
#
# Retrieves Notes specifically for the Status Page
# which are rendered as comments.
#
# Arguments:
# issue - The notes are scoped to this issue
#
# Examples:
#
# finder = StatusPage::IncidentCommentsFinder.new(issue: issue)
#
# # Latest, visible 100 notes
# notes = finder.all
#
module StatusPage
class IncidentCommentsFinder
# Only comments with this emoji are visible.
# This filter will change once we have confidential notes.
# See https://gitlab.com/gitlab-org/gitlab/issues/207468
AWARD_EMOJI = 'microphone'
MAX_LIMIT = StatusPage::Storage::MAX_COMMENTS
def initialize(issue:)
@issue = issue
end
def all
execute
.limit(MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
end
private
attr_reader :issue
def execute
notes = init_collection
notes = only_user(notes)
notes = to_publish(notes)
notes = chronological(notes)
notes
end
def init_collection
issue.notes
end
def only_user(notes)
notes.user
end
def to_publish(notes)
# Instead of adding a scope Awardable#for_award_emoji_name we're inlining
# this because this query very specific to our use-case and
# we don't want to promote this query to other folks.
#
# Note 1: This finder is used by services which are currently behind a
# beta feature flag.
#
# Note 2: We will switch to private comments once it's available
# (https://gitlab.com/groups/gitlab-org/-/epics/2697)
# rubocop: disable CodeReuse/ActiveRecord
notes
.joins(:award_emoji)
.where(award_emoji: { name: AWARD_EMOJI })
# rubocop: enable CodeReuse/ActiveRecord
end
def chronological(notes)
notes.fresh
end
end
end
# frozen_string_literal: true
#
# Retrieves Issues specifically for the Status Page
# which are rendered as incidents.
#
# Arguments:
# project_id - The issues are scoped to this project
#
# Examples:
#
# finder = StatusPage::IncidentsFinder.new(project_id: project_id)
#
# # A single issue
# issue, user_notes = finder.find_by_id(issue_id)
#
# # Most recent 20 issues
# issues = finder.all
#
module StatusPage
class IncidentsFinder
MAX_LIMIT = StatusPage::Storage::MAX_RECENT_INCIDENTS
def initialize(project_id:)
@project_id = project_id
end
def find_by_id(issue_id)
execute.find_by_id(issue_id)
end
def all
@sorted = true
execute
.limit(MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
end
private
attr_reader :project_id, :with_user_notes, :sorted
def execute
issues = init_collection
issues = public_only(issues)
issues = by_project(issues)
issues = reverse_chronological(issues) if sorted
issues
end
def init_collection
Issue
end
def public_only(issues)
issues.public_only
end
def by_project(issues)
issues.of_projects(project_id)
end
def reverse_chronological(issues)
issues.order_created_desc
end
end
end
...@@ -35,6 +35,12 @@ class StatusPageSetting < ApplicationRecord ...@@ -35,6 +35,12 @@ class StatusPageSetting < ApplicationRecord
'*' * 40 '*' * 40
end end
def enabled?
super &&
project&.feature_available?(:status_page) &&
project&.beta_feature_available?(:status_page)
end
private private
def check_secret_changes def check_secret_changes
......
...@@ -25,7 +25,7 @@ module StatusPage ...@@ -25,7 +25,7 @@ module StatusPage
end end
def feature_available? def feature_available?
project.feature_available?(:status_page) project.status_page_setting&.enabled?
end end
def upload(key, json) def upload(key, json)
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
module StatusPage module StatusPage
module Storage module Storage
# Limit the amount of the recent incidents in the JSON list
MAX_RECENT_INCIDENTS = 20
# Limit the amount of comments per incident
MAX_COMMENTS = 100
def self.details_path(id) def self.details_path(id)
"incident/#{id}.json" "incident/#{id}.json"
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::IncidentCommentsFinder do
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue) }
let_it_be(:unrelated_issue) { create(:issue, project: issue.project) }
let_it_be(:visible_emoji) { described_class::AWARD_EMOJI }
let_it_be(:other_emoji) { 'cool' }
let_it_be(:notes) do
{
to_publish: Array.new(2) { note(emoji: visible_emoji) },
system: note(system: true),
invisible_without_emoji: note,
invisible_with_other_emoji: note(emoji: other_emoji),
unrelated: note(for_issue: unrelated_issue)
}
end
let(:notes_to_publish) { notes.fetch(:to_publish) }
let(:finder) { described_class.new(issue: issue) }
describe '#all' do
let(:sorted_notes) { notes_to_publish.sort_by(&:created_at) }
subject { finder.all }
before do
stub_const("#{described_class}::MAX_LIMIT", limit)
end
context 'when limit is higher than the colletion size' do
let(:limit) { notes_to_publish.size + 1 }
it { is_expected.to eq(sorted_notes) }
end
context 'when limit is lower than the colletion size' do
let(:limit) { notes_to_publish.size - 1 }
it { is_expected.to eq(sorted_notes.first(1)) }
end
end
describe 'award emoji' do
let(:digest_path) { Rails.root.join(*%w[fixtures emojis digests.json]) }
let(:digest_json) { JSON.parse(File.read(digest_path)) }
it 'ensures that emoji exists' do
expect(digest_json).to include(visible_emoji)
end
end
private
def note(for_issue: issue, emoji: nil, **kwargs)
note = create(:note, project: for_issue.project, noteable: for_issue, **kwargs)
create(:award_emoji, awardable: note, name: emoji, user: user) if emoji
note
end
end
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::IncidentsFinder do
let_it_be(:project) { create(:project) }
let_it_be(:issues) do
{
public: create_list(:issue, 2, project: project),
confidential: create(:issue, :confidential, project: project),
unrelated: create(:issue)
}
end
let(:public_issues) { issues.fetch(:public) }
let(:finder) { described_class.new(project_id: project.id) }
describe '#find_by_id' do
subject { finder.find_by_id(issue.id) }
context 'for public issue' do
let(:issue) { public_issues.first }
it { is_expected.to eq(issue) }
end
context 'for confidential issue' do
let(:issue) { issues.fetch(:confidential) }
it { is_expected.to be_nil }
end
context 'for unrelated issue' do
let(:issue) { issues.fetch(:unrelated) }
it { is_expected.to be_nil }
end
end
describe '#all' do
let(:sorted_issues) { public_issues.sort_by(&:created_at).reverse }
subject { finder.all }
before do
stub_const("#{described_class}::MAX_LIMIT", limit)
end
context 'when limit is higher than the colletion size' do
let(:limit) { public_issues.size + 1 }
it { is_expected.to eq(sorted_issues) }
end
context 'when limit is lower than the colletion size' do
let(:limit) { public_issues.size - 1 }
it { is_expected.to eq(sorted_issues.first(1)) }
end
end
end
...@@ -73,4 +73,40 @@ describe StatusPageSetting do ...@@ -73,4 +73,40 @@ describe StatusPageSetting do
end end
end end
end end
describe '#enabled?' do
let(:status_page_setting) { build(:status_page_setting, :enabled) }
subject { status_page_setting.enabled? }
before do
stub_licensed_features(status_page: true)
end
it { is_expected.to eq(true) }
context 'when status page setting is diabled' do
before do
status_page_setting.enabled = false
end
it { is_expected.to eq(false) }
end
context 'when license is not available' do
before do
stub_licensed_features(status_page: false)
end
it { is_expected.to eq(false) }
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(status_page: false)
end
it { is_expected.to eq(false) }
end
end
end end
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'publish incidents' do RSpec.shared_examples 'publish incidents' do
let_it_be(:status_page_setting) do
create(:status_page_setting, :enabled, project: project)
end
before do before do
stub_licensed_features(status_page: true) stub_licensed_features(status_page: true)
end end
shared_examples 'feature is not available' do
it 'returns feature not available error' do
expect(result).to be_error
expect(result.message).to eq('Feature not available')
end
end
context 'when upload succeeds' do context 'when upload succeeds' do
before do before do
allow(storage_client).to receive(:upload_object).with(key, content_json) allow(storage_client).to receive(:upload_object).with(key, content_json)
...@@ -60,9 +71,22 @@ RSpec.shared_examples 'publish incidents' do ...@@ -60,9 +71,22 @@ RSpec.shared_examples 'publish incidents' do
stub_licensed_features(status_page: false) stub_licensed_features(status_page: false)
end end
it 'returns feature not available error' do it_behaves_like 'feature is not available'
expect(result).to be_error end
expect(result.message).to eq('Feature not available')
context 'when status page setting is disabled' do
before do
status_page_setting.update!(enabled: false)
end
it_behaves_like 'feature is not available'
end end
context 'when feature flag is disabled' do
before do
stub_feature_flags(status_page: false)
end
it_behaves_like 'feature is not available'
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