Commit a569e40e authored by James Fargher's avatar James Fargher

Merge branch...

Merge branch '294266-parse-example-alert-payload-to-return-list-of-payload-alert-fields-service' into 'master'

Implement payload field extract service and model

See merge request gitlab-org/gitlab!50948
parents 1efd4771 4e3981c6
# frozen_string_literal: true
module AlertManagement
class AlertPayloadField
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :project, :path, :label, :type
ARRAY_TYPE = 'array'
DATETIME_TYPE = 'datetime'
STRING_TYPE = 'string'
SUPPORTED_TYPES = [ARRAY_TYPE, DATETIME_TYPE, STRING_TYPE].freeze
validates :project, presence: true
validates :label, presence: true
validates :type, inclusion: { in: SUPPORTED_TYPES }
validate :ensure_path_is_non_empty_list_of_strings
private
def ensure_path_is_non_empty_list_of_strings
return if path_is_non_empty_list_of_strings?
errors.add(:path, 'must be a list of strings')
end
def path_is_non_empty_list_of_strings?
path.is_a?(Array) && !path.empty? && path.all? { |segment| segment.is_a?(String) }
end
end
end
# frozen_string_literal: true
module AlertManagement
class ExtractAlertPayloadFieldsService < BaseContainerService
alias_method :project, :container
def execute
return error('Feature not available') unless available?
return error('Insufficient permissions') unless allowed?
payload = parse_payload
return error('Failed to parse payload') unless payload && payload.is_a?(Hash)
return error('Payload size exceeded') unless valid_payload_size?(payload)
fields = Gitlab::AlertManagement::AlertPayloadFieldExtractor
.new(project).extract(payload)
success(fields)
end
private
def parse_payload
Gitlab::Json.parse(params[:payload])
rescue JSON::ParserError
end
def valid_payload_size?(payload)
Gitlab::Utils::DeepSize.new(payload).valid?
end
def success(fields)
ServiceResponse.success(payload: { payload_alert_fields: fields })
end
def error(message)
ServiceResponse.error(message: message)
end
def available?
::Gitlab::AlertManagement.custom_mapping_available?(project)
end
def allowed?
current_user&.can?(:admin_operations, project)
end
end
end
# frozen_string_literal: true
module Gitlab
module AlertManagement
class AlertPayloadFieldExtractor
def initialize(project)
@project = project
end
def extract(payload)
deep_traverse(payload.deep_stringify_keys)
.map { |path, value| field(path, value) }
.compact
end
private
attr_reader :project
def field(path, value)
type = type_of(value)
return unless type
label = path.last.humanize
::AlertManagement::AlertPayloadField.new(
project: project,
path: path,
label: label,
type: type
)
end
# Code duplication with Gitlab::InlineHash#merge_keys ahead!
# See https://gitlab.com/gitlab-org/gitlab/-/issues/299856
def deep_traverse(hash)
return to_enum(__method__, hash) unless block_given?
pairs = hash.map { |k, v| [[k], v] }
until pairs.empty?
key, value = pairs.shift
if value.is_a?(Hash)
value.each { |k, v| pairs.unshift [key + [k], v] }
else
yield key, value
end
end
end
def type_of(value)
case value
when Array
::AlertManagement::AlertPayloadField::ARRAY_TYPE
when /^\d{4}/ # assume it's a datetime
::AlertManagement::AlertPayloadField::DATETIME_TYPE
when String
::AlertManagement::AlertPayloadField::STRING_TYPE
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :alert_management_alert_payload_field, class: 'AlertManagement::AlertPayloadField' do
project
path { ['title'] }
label { 'Title' }
type { 'string' }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::AlertPayloadFieldExtractor do
let(:project) { build_stubbed(:project) }
let(:extractor) { described_class.new(project) }
let(:payload) { {} }
let(:json) { Gitlab::Json.parse(Gitlab::Json.generate(payload)) }
let(:field) { fields.first }
subject(:fields) { extractor.extract(json) }
describe '#extract' do
before do
payload.merge!(
str: 'value',
nested: {
key: 'level1',
deep: {
key: 'level2'
}
},
time: '2020-12-09T12:34:56',
time_iso_8601_and_rfc_3339: '2021-01-27T13:01:11+00:00', # ISO 8601 and RFC 3339
time_iso_8601: '2021-01-27T13:01:11Z', # ISO 8601
time_iso_8601_short: '20210127T130111Z', # ISO 8601
time_rfc_3339: '2021-01-27 13:01:11+00:00', # RFC 3339
discarded_null: nil,
discarded_bool_true: true,
discarded_bool_false: false,
arr: %w[one two three]
)
end
it 'returns all the possible field combination and types suggestions' do
expect(fields).to contain_exactly(
a_field(['str'], 'Str', 'string'),
a_field(%w(nested key), 'Key', 'string'),
a_field(%w(nested deep key), 'Key', 'string'),
a_field(['time'], 'Time', 'datetime'),
a_field(['time_iso_8601_and_rfc_3339'], 'Time iso 8601 and rfc 3339', 'datetime'),
a_field(['time_iso_8601'], 'Time iso 8601', 'datetime'),
a_field(['time_iso_8601_short'], 'Time iso 8601 short', 'datetime'),
a_field(['time_rfc_3339'], 'Time rfc 3339', 'datetime'),
a_field(['arr'], 'Arr', 'array')
)
end
end
private
def a_field(path, label, type)
have_attributes(project: project, path: path, label: label, type: type)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::AlertPayloadField do
let(:alert_payload_field) { build(:alert_management_alert_payload_field) }
describe 'validations' do
subject { alert_payload_field }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:label) }
it { is_expected.to validate_inclusion_of(:type).in_array(described_class::SUPPORTED_TYPES) }
context 'validates path' do
shared_examples 'has invalid path' do
it 'is invalid' do
expect(alert_payload_field.valid?).to eq(false)
expect(alert_payload_field.errors.full_messages).to eq(['Path must be a list of strings'])
end
end
context 'when path is nil' do
let(:alert_payload_field) { build(:alert_management_alert_payload_field, path: nil) }
it_behaves_like 'has invalid path'
end
context 'when path is empty array' do
let(:alert_payload_field) { build(:alert_management_alert_payload_field, path: []) }
it_behaves_like 'has invalid path'
end
context 'when path does not contain only strings' do
let(:alert_payload_field) { build(:alert_management_alert_payload_field, path: ['title', 1]) }
it_behaves_like 'has invalid path'
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::ExtractAlertPayloadFieldsService do
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:user) { user_with_permissions }
let(:payload) { { foo: 'bar' } }
let(:payload_json) { Gitlab::Json.generate(payload) }
let(:params) { { payload: payload_json } }
let(:service) do
described_class.new(container: project, current_user: user, params: params)
end
subject(:response) { service.execute }
context 'with license' do
before do
stub_licensed_features(multiple_alert_http_integrations: true)
end
context 'with feature flag enabled' do
before do
stub_feature_flags(multiple_http_integrations_custom_mapping: project)
end
context 'with permissions' do
before do
project.add_maintainer(user_with_permissions)
end
context 'when payload is valid JSON' do
context 'when payload has an acceptable size' do
it 'responds with success' do
is_expected.to be_success
end
it 'returns parsed fields' do
fields = response.payload[:payload_alert_fields]
field = fields.first
expect(fields.count).to eq(1)
expect(field.label).to eq('Foo')
expect(field.type).to eq('string')
expect(field.path).to eq(%w[foo])
end
end
context 'when limits are exceeded' do
before do
allow(Gitlab::Utils::DeepSize)
.to receive(:new)
.with(Gitlab::Json.parse(payload_json))
.and_return(double(valid?: false))
end
it 'returns payload size exceeded error' do
is_expected.to be_error
expect(response.message).to eq('Payload size exceeded')
end
end
end
context 'when payload is not a valid JSON' do
let(:payload) { 'not a JSON' }
it 'returns payload parse failure error' do
is_expected.to be_error
expect(response.message).to eq('Failed to parse payload')
end
end
end
context 'without permissions' do
let_it_be(:user) { user_without_permissions }
it 'returns insufficient permissions error' do
is_expected.to be_error
expect(response.message).to eq('Insufficient permissions')
end
end
end
context 'with feature flag disabled' do
before do
stub_feature_flags(multiple_http_integrations_custom_mapping: false)
end
it 'returns feature not available error' do
is_expected.to be_error
expect(response.message).to eq('Feature 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