Fixes #3616 - Provide meaningful modal if report profile tries to use dates out side the filtered date range

This commit is contained in:
Mantas Masalskis 2021-11-30 13:40:08 +01:00
parent d2f24fc0fc
commit 04d1459121
16 changed files with 1481 additions and 1471 deletions

View file

@ -11,7 +11,6 @@
- bundle exec rails test test/integration/elasticsearch_active_test.rb - bundle exec rails test test/integration/elasticsearch_active_test.rb
- bundle exec rails test test/integration/elasticsearch_test.rb - bundle exec rails test test/integration/elasticsearch_test.rb
- bundle exec rspec --tag searchindex --tag ~type:system --profile 10 - bundle exec rspec --tag searchindex --tag ~type:system --profile 10
- bundle exec rails test test/integration/report_test.rb
es:7: es:7:
<<: *template_integration_es <<: *template_integration_es

View file

@ -0,0 +1,9 @@
class App.ControllerTechnicalErrorModal extends App.ControllerModal
head: "StatusCode: #{status}"
contentCode: ''
buttonClose: false
buttonSubmit: 'Ok'
onSubmit: (e) -> @close(e)
content: ->
"<pre><code>#{@contentCode}</code></pre>"

View file

@ -171,6 +171,13 @@ class Graph extends App.Controller
backends: @params.backendSelected backends: @params.backendSelected
) )
processData: true processData: true
error: (xhr) =>
return if !_.include([401, 403, 404, 422, 502], xhr.status)
@bodyModal = new App.ControllerTechnicalErrorModal(
head: 'Cannot generate report'
contentCode: xhr.responseJSON.error
)
success: (data) => success: (data) =>
@update(data) @update(data)
@delay(@render, interval, 'report-update', 'page') @delay(@render, interval, 'report-update', 'page')

View file

@ -102,12 +102,18 @@ class _ajaxSingleton
# do not show any error message with code 502 # do not show any error message with code 502
return if status is 502 return if status is 502
try
json = JSON.parse(detail)
text = json.error_human || json.error
text = detail if !text
escaped = App.Utils.htmlEscape(text)
# show error message # show error message
new App.ControllerModal( new App.ControllerTechnicalErrorModal(
contentCode: escaped
head: "StatusCode: #{status}" head: "StatusCode: #{status}"
contentInline: "<pre>#{App.Utils.htmlEscape(detail)}</pre>"
buttonClose: true
buttonSubmit: false
) )
) )

View file

@ -358,6 +358,26 @@ ol, ul {
z-index: 1; z-index: 1;
} }
code {
background: hsla(0, 0%, 0%, 0.2);
border-radius: 3px;
box-decoration-break: clone;
}
code,
.hljs {
padding: 2px 4px;
font-size: 0.88em;
}
.hljs {
background: none;
}
pre code.hljs {
font-size: 1em;
}
pre { pre {
display: block; display: block;
padding: 9.5px; padding: 9.5px;
@ -371,30 +391,26 @@ pre {
border: 1px solid hsl(0,0%,90%); border: 1px solid hsl(0,0%,90%);
border-radius: 3px; border-radius: 3px;
} }
.modal-content pre {
background: hsl(0, 0%, 97%);
border: 1px solid hsl(0, 0%, 87%);
}
pre code { pre code {
padding: 0; padding: 0;
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
white-space: pre-wrap; white-space: pre-wrap;
background-color: transparent;
border-radius: 0;
}
.hljs,
code {
background: none; background: none;
padding: 2px 4px; border-radius: 0;
font-size: 0.88em; border: none;
} overflow-x: auto;
code:not(.hljs) { &.hljs {
border: 1px solid rgba(0,0,0,.2); padding: 0;
border-radius: 3px; background: none;
white-space: nowrap; }
}
pre code.hljs {
font-size: 1em;
} }
.textarea::placeholder, .textarea::placeholder,

View file

@ -22,6 +22,7 @@ class ReportsController < ApplicationController
get_params = params_all get_params = params_all
return if !get_params return if !get_params
begin
result = {} result = {}
get_params[:metric][:backend].each do |backend| get_params[:metric][:backend].each do |backend|
condition = get_params[:profile].condition condition = get_params[:profile].condition
@ -43,6 +44,13 @@ class ReportsController < ApplicationController
current_user: current_user current_user: current_user
) )
end end
rescue => e
if e.message.include? 'Conflicting date range'
raise Exceptions::UnprocessableEntity, 'Conflicting date ranges. Please check your selected report profile.'
end
raise e
end
render json: { render json: {
data: result data: result

View file

@ -438,6 +438,8 @@ example for aggregations within one year
data = selector2query(selectors, options, aggs_interval) data = selector2query(selectors, options, aggs_interval)
verify_date_range(url, data)
response = make_request(url, data: data) response = make_request(url, data: data)
if !response.success? if !response.success?
@ -1208,4 +1210,87 @@ helper method for making HTTP calls and raising error if response was not succes
) )
end end
# verifies date range ElasticSearch payload
#
# @param url [String] of ElasticSearch
# @param payload [Hash] Elasticsearch query payload
#
# @return [Boolean] or raises error
def self.verify_date_range(url, payload)
ranges_payload = payload.dig(:query, :bool, :must)
return true if ranges_payload.nil?
ranges = ranges_payload
.select { |elem| elem.key? :range }
.map { |elem| [elem[:range].keys.first, convert_es_date_range(elem)] }
.each_with_object({}) { |elem, sum| (sum[elem.first] ||= []) << elem.last }
return true if ranges.all? { |_, ranges_by_key| verify_single_key_range(ranges_by_key) }
error_prefix = "Unable to process request to elasticsearch URL '#{url}'."
error_suffix = "Payload:\n#{payload.to_json}"
error_message = 'Conflicting date ranges'
result = "#{error_prefix} #{error_message} #{error_suffix}"
Rails.logger.error result.first(40_000)
raise result
end
# checks if all ranges are overlaping
#
# @param ranges [Array<Range<DateTime>>] to use in search
#
# @return [Boolean]
def self.verify_single_key_range(ranges)
ranges
.each_with_index
.all? do |range, i|
ranges
.slice((i + 1)..)
.all? { |elem| elem.overlaps? range }
end
end
# Converts paylaod component to dates range
#
# @param elem [Hash] payload component
#
# @return [Range<DateTime>]
def self.convert_es_date_range(elem)
range = elem[:range].first.last
from = parse_es_range_date range[:from] || range[:gt] || '-9999-01-01'
to = parse_es_range_date range[:to] || range[:lt] || '9999-01-01'
from..to
end
# Parses absolute date or converts relative date
#
# @param input [String] string representation of date
#
# @return [Range<DateTime>]
def self.parse_es_range_date(input)
match = input.match(%r{^now(-|\+)(\d+)(\w{1})$})
return DateTime.parse input if !match
map = {
d: 'day',
y: 'year',
M: 'month',
h: 'hour',
m: 'minute',
}
range = match.captures[1].to_i.send map[match.captures[2].to_sym]
case match.captures[0]
when '-'
range.ago
when '+'
range.from_now
end
end
end end

View file

@ -6,5 +6,20 @@ FactoryBot.define do
active { true } active { true }
created_by_id { 1 } created_by_id { 1 }
updated_by_id { 1 } updated_by_id { 1 }
trait :condition_created_at do
transient do
ticket_created_at { nil }
end
condition do
{
'ticket.created_at' => {
operator: 'before (absolute)',
value: ticket_created_at.iso8601
}
}
end
end
end end
end end

View file

@ -0,0 +1,192 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
# rubocop:disable RSpec/ExampleLength
require 'rails_helper'
require 'lib/report_examples'
RSpec.describe Report::TicketFirstSolution, searchindex: true do
include_examples 'with report examples'
describe '.aggs' do
it 'gets monthly aggregated results' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0]
end
it 'gets monthly aggregated results with high priority' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket.priority_id' => {
'operator' => 'is',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
end
it 'gets monthly aggregated results not in merged state' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket_state.name' => {
'operator' => 'is not',
'value' => 'merged',
}
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0]
end
it 'gets monthly aggregated results with not high priority' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket.priority_id' => {
'operator' => 'is not',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0]
end
it 'gets weekly aggregated results' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-10-26T00:00:00Z'),
range_end: Time.zone.parse('2015-10-31T23:59:59Z'),
interval: 'week',
selector: {},
)
expect(result).to eq [0, 0, 1, 0, 0, 1, 1]
end
it 'gets daily aggregated results' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-10-01T00:00:00Z'),
range_end: Time.zone.parse('2015-11-01T23:59:59Z'),
interval: 'day',
selector: {},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1]
end
it 'gets hourly aggregated results' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-10-28T00:00:00Z'),
range_end: Time.zone.parse('2015-10-28T23:59:59Z'),
interval: 'hour',
selector: {},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
end
end
describe '.items' do
it 'gets items in year range' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {},
)
expect(result).to match_tickets ticket_5, ticket_6, ticket_7
end
it 'gets items in year range with high priority' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket.priority_id' => {
'operator' => 'is',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
}
)
expect(result).to match_tickets ticket_5
end
it 'gets items in year range not in merged state' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket_state.name' => {
'operator' => 'is not',
'value' => 'merged',
}
}
)
expect(result).to match_tickets ticket_5, ticket_6, ticket_7
end
it 'gets items in year range with not high priority' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket.priority_id' => {
'operator' => 'is not',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
}
)
expect(result).to match_tickets ticket_6, ticket_7
end
it 'gets items in week range' do
result = described_class.items(
range_start: Time.zone.parse('2015-10-26T00:00:00Z'),
range_end: Time.zone.parse('2015-11-01T23:59:59Z'),
selector: {}
)
expect(result).to match_tickets ticket_5, ticket_6, ticket_7
end
it 'gets items in day range' do
result = described_class.items(
range_start: Time.zone.parse('2015-10-01T00:00:00Z'),
range_end: Time.zone.parse('2015-10-31T23:59:59Z'),
selector: {}
)
expect(result).to match_tickets ticket_5, ticket_6
end
it 'gets items in hour range' do
result = described_class.items(
range_start: Time.zone.parse('2015-10-28T00:00:00Z'),
range_end: Time.zone.parse('2015-10-28T23:59:59Z'),
interval: 'hour',
selector: {},
)
expect(result).to match_tickets ticket_5
end
end
end
# rubocop:enable RSpec/ExampleLength

View file

@ -1,33 +1,333 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'rails_helper' require 'rails_helper'
require 'lib/report_examples'
RSpec.describe Report::TicketGenericTime do RSpec.describe Report::TicketGenericTime, searchindex: true do
include_examples 'with report examples'
=begin describe '.aggs' do
it 'gets monthly aggregated results by created_at' do
result = Report::TicketGenericTime.items( result = described_class.aggs(
range_start: '2015-01-01T00:00:00Z', range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: '2015-12-31T23:59:59Z', range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: selector, # ticket selector to get only a collection of tickets interval: 'month', # year, quarter, month, week, day, hour, minute, second
selector: {}, # ticket selector to get only a collection of tickets
params: { field: 'created_at' }, params: { field: 'created_at' },
) )
returns expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 1, 0]
end
{ it 'gets monthly aggregated results by created_at not merged' do
count: 123, result = described_class.aggs(
ticket_ids: [4,5,1,5,0,51,5,56,7,4], range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
assets: assets, range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month', # year, quarter, month, week, day, hour, minute, second
selector: {
'state' => {
'operator' => 'is not',
'value' => 'merged'
} }
},
params: { field: 'created_at' },
)
=end expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 1, 0]
end
end
describe 'items' do describe '.items' do
it 'gets items in year range by created_at' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {}, # ticket selector to get only a collection of tickets
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
end
it 'gets items in year range by created_at not merged' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'state' => {
'operator' => 'is not',
'value' => 'merged'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
end
it 'gets items in year range by created_at before oct 31st' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'created_at' => {
'operator' => 'before (absolute)',
'value' => '2015-10-31T00:00:00Z'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
end
it 'gets items in year range by created_at after oct 31st' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'created_at' => {
'operator' => 'after (absolute)',
'value' => '2015-10-31T00:00:00Z'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6
end
it 'gets items in 1 day from now' do
result = described_class.items(
range_start: 1.year.ago.beginning_of_year,
range_end: 1.year.from_now.at_end_of_year,
selector: {
'created_at' => {
'operator' => 'after (relative)',
'range' => 'day',
'value' => '1'
}
}, # ticket selector to get only a collection of tickets
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_after_72h
end
it 'gets items in 1 month from now' do
result = described_class.items(
range_start: 1.year.ago.beginning_of_year,
range_end: 1.year.from_now.at_end_of_year,
selector: {
'created_at' => {
'operator' => 'after (relative)',
'range' => 'month',
'value' => '1'
}
}, # ticket selector to get only a collection of tickets
params: { field: 'created_at' },
)
expect(result).to match_tickets []
end
it 'gets items in 1 month ago' do
result = described_class.items(
range_start: 1.year.ago.beginning_of_year,
range_end: 1.year.from_now.at_end_of_year,
selector: {
'created_at' => {
'operator' => 'before (relative)',
'range' => 'month',
'value' => '1'
}
}, # ticket selector to get only a collection of tickets
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_before_40d
end
it 'gets items in 5 months ago' do
result = described_class.items(
range_start: 1.year.ago.beginning_of_year,
range_end: 1.year.from_now.at_end_of_year,
selector: {
'created_at' => {
'operator' => 'before (relative)',
'range' => 'month',
'value' => '5'
}
}, # ticket selector to get only a collection of tickets
params: { field: 'created_at' },
)
expect(result).to match_tickets []
end
it 'gets items with aaa+bbb' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains all',
'value' => 'aaa, bbb'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_1
end
it 'gets items with not aaa+bbb' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains all not',
'value' => 'aaa, bbb'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2
end
it 'gets items with aaa' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains all',
'value' => 'aaa'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_2, ticket_1
end
it 'gets items with not aaa' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains all not',
'value' => 'aaa'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3
end
it 'gets items with one not aaa' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains one not',
'value' => 'aaa'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3
end
it 'gets items with one not aaa+bbb' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains one not',
'value' => 'aaa, bbb'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6, ticket_4, ticket_3
end
it 'gets items with one aaa' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains one',
'value' => 'aaa'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_2, ticket_1
end
it 'gets items with one aaa+bbb' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'tags' => {
'operator' => 'contains one',
'value' => 'aaa, bbb'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_5, ticket_2, ticket_1
end
it 'gets items with test' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'title' => {
'operator' => 'contains',
'value' => 'Test'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1
end
it 'gets items with not test' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'title' => {
'operator' => 'contains not',
'value' => 'Test'
}
},
params: { field: 'created_at' },
)
expect(result).to match_tickets []
end
# Regression test for issue #2246 - Records in Reporting not updated when single ActiveRecord can not be found # Regression test for issue #2246 - Records in Reporting not updated when single ActiveRecord can not be found
it 'correctly handles missing tickets' do it 'correctly handles missing tickets', searchindex: false do
class_double('SearchIndexBackend', selectors: { ticket_ids: [-1] }).as_stubbed_const class_double('SearchIndexBackend', selectors: { ticket_ids: [-1] }, drop_index: nil, drop_pipeline: nil).as_stubbed_const
expect do expect do
described_class.items( described_class.items(
@ -39,4 +339,78 @@ returns
end.not_to raise_error end.not_to raise_error
end end
end end
context 'when additional attribute exists', db_strategy: :reset do
before do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'test_category',
display: 'Test 1',
data_type: 'tree_select',
data_option: {
maxlength: 200,
null: false,
default: '',
options: [
{ 'name' => 'aa', 'value' => 'aa', 'children' => [{ 'name' => 'aa', 'value' => 'aa::aa' }, { 'name' => 'bb', 'value' => 'aa::bb' }, { 'name' => 'cc', 'value' => 'aa::cc' }] },
{ 'name' => 'bb', 'value' => 'bb', 'children' => [{ 'name' => 'aa', 'value' => 'bb::aa' }, { 'name' => 'bb', 'value' => 'bb::bb' }, { 'name' => 'cc', 'value' => 'bb::cc' }] },
{ 'name' => 'cc', 'value' => 'cc', 'children' => [{ 'name' => 'aa', 'value' => 'cc::aa' }, { 'name' => 'bb', 'value' => 'cc::bb' }, { 'name' => 'cc', 'value' => 'cc::cc' }] },
]
},
active: true,
screens: {},
position: 20,
created_by_id: 1,
updated_by_id: 1,
editable: false,
to_migrate: false,
)
ObjectManager::Attribute.migration_execute
ticket_with_category
rebuild_searchindex
end
let(:ticket_with_category) do
travel_to DateTime.new 2015, 10, 28, 9, 30
ticket = create(:ticket,
group: group_2,
customer: customer,
test_category: 'cc::bb',
state_name: 'new',
priority_name: '2 normal')
ticket.tag_add('aaa', 1)
ticket.tag_add('bbb', 1)
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel 5.hours
ticket.update! group: group_1
travel_back
ticket
end
describe '.items' do
it 'gets items with test_category cc:bb' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'test_category' => {
'operator' => 'is',
'value' => 'cc::bb'
},
},
params: { field: 'created_at' },
)
expect(result).to match_tickets ticket_with_category
end
end
end
end end

View file

@ -0,0 +1,148 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
#
# rubocop:disable RSpec/ExampleLength
require 'rails_helper'
require 'lib/report_examples'
RSpec.describe Report::TicketMoved, searchindex: true do
include_examples 'with report examples'
describe '.aggs' do
it 'gets monthly aggregated results not in merged state' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket_state.name' => {
'operator' => 'is not',
'value' => 'merged',
}
},
params: {
type: 'in',
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
end
it 'gets monthly aggregated results in users group' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket.group_id' => {
'operator' => 'is',
'value' => [Group.lookup(name: 'Users').id],
}
},
params: {
type: 'in',
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
end
it 'gets monthly aggregated results not in merged state and outgoing' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket_state.name' => {
'operator' => 'is not',
'value' => 'merged',
}
},
params: {
type: 'out',
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
end
it 'gets monthly aggregated results in users group and outgoing' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket.group_id' => {
'operator' => 'is',
'value' => [Group.lookup(name: 'Users').id],
}
},
params: {
type: 'out',
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
end
end
describe '.items' do
it 'gets items in year range in users group' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket.group_id' => {
'operator' => 'is',
'value' => [Group.lookup(name: 'Users').id],
}
},
params: {
type: 'in',
},
)
expect(result).to match_tickets ticket_1
end
it 'gets items in year range not merged and outgoing' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket_state.name' => {
'operator' => 'is not',
'value' => 'merged',
}
}, # ticket selector to get only a collection of tickets
params: {
type: 'out',
},
)
expect(result).to match_tickets []
end
it 'gets items in year range in users group and outgoing' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket.group_id' => {
'operator' => 'is',
'value' => [Group.lookup(name: 'Users').id],
}
},
params: {
type: 'out',
},
)
expect(result).to match_tickets ticket_2
end
end
end
# rubocop:enable RSpec/ExampleLength

View file

@ -0,0 +1,129 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
# rubocop:disable RSpec/ExampleLength
require 'rails_helper'
require 'lib/report_examples'
RSpec.describe Report::TicketReopened, searchindex: true do
include_examples 'with report examples'
describe '.aggs' do
it 'gets monthly aggregated results' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
end
it 'gets monthly aggregated results with high priority' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket.priority_id' => {
'operator' => 'is',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
}
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
end
it 'gets monthly aggregated results with not high priority' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket.priority_id' => {
'operator' => 'is not',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
end
it 'gets monthly aggregated results with not merged' do
result = described_class.aggs(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
interval: 'month',
selector: {
'ticket_state.name' => {
'operator' => 'is not',
'value' => 'merged',
}
},
)
expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
end
end
describe '.items' do
it 'gets items in year range' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {},
)
expect(result).to match_tickets ticket_5
end
it 'gets items in year range with high priority' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket.priority_id' => {
'operator' => 'is',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
},
)
expect(result).to match_tickets ticket_5
end
it 'gets items in year range with not high priority' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket.priority_id' => {
'operator' => 'is not',
'value' => [Ticket::Priority.lookup(name: '3 high').id],
}
},
)
expect(result).to match_tickets []
end
it 'gets items in year range with not merged' do
result = described_class.items(
range_start: Time.zone.parse('2015-01-01T00:00:00Z'),
range_end: Time.zone.parse('2015-12-31T23:59:59Z'),
selector: {
'ticket_state.name' => {
'operator' => 'is not',
'value' => 'merged',
}
},
)
expect(result).to match_tickets ticket_5
end
end
end
# rubocop:enable RSpec/ExampleLength

245
spec/lib/report_examples.rb Normal file
View file

@ -0,0 +1,245 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
RSpec.shared_context 'with report examples' do
before do |example|
next if !example.metadata[:searchindex]
configure_elasticsearch(required: true, rebuild: true) do
ticket_1
ticket_2
ticket_3
ticket_4
ticket_5
ticket_6
ticket_7
ticket_8
ticket_9
ticket_after_72h
ticket_before_40d
end
end
let(:group_1) { Group.lookup(name: 'Users') }
let(:group_2) { create(:group) }
let(:customer) { User.lookup(email: 'nicole.braun@zammad.org') }
let(:ticket_1) do
travel_to DateTime.new 2015, 10, 28, 9, 30
ticket = create(:ticket,
group: group_2,
customer: customer,
state_name: 'new',
priority_name: '2 normal')
ticket.tag_add('aaa', 1)
ticket.tag_add('bbb', 1)
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel 5.hours
ticket.update! group: group_1
travel_back
ticket
end
let(:ticket_2) do
travel_to DateTime.new 2015, 10, 28, 9, 30, 1
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'new',
priority_name: '2 normal')
ticket.tag_add('aaa', 1)
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel 5.hours - 1.second
ticket.update! group: group_2
travel_back
ticket
end
let(:ticket_3) do
travel_to DateTime.new 2015, 10, 28, 10, 30
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'open',
priority_name: '3 high')
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel_back
ticket
end
let(:ticket_4) do
travel_to DateTime.new 2015, 10, 28, 10, 30, 1
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'closed',
priority_name: '2 normal',
close_at: (1.hour - 1.second).from_now)
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel_back
ticket
end
let(:ticket_5) do
travel_to DateTime.new 2015, 10, 28, 11, 30
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'closed',
priority_name: '3 high',
close_at: 10.minutes.from_now)
ticket.tag_add('bbb', 1)
create(:ticket_article,
:outbound_email,
ticket: ticket)
ticket.update! state: Ticket::State.lookup(name: 'open')
travel 3.hours
travel_back
ticket
end
let(:ticket_6) do
travel_to DateTime.new 2015, 10, 31, 12, 30
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'closed',
priority_name: '2 normal',
close_at: 5.minutes.from_now)
create(:ticket_article,
:outbound_email,
ticket: ticket)
travel_back
ticket
end
let(:ticket_7) do
travel_to DateTime.new 2015, 11, 1, 12, 30
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'closed',
priority_name: '2 normal',
close_at: Time.zone.now)
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel_back
ticket
end
let(:ticket_8) do
travel_to DateTime.new 2015, 11, 2, 12, 30
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'merged',
priority_name: '2 normal',
close_at: Time.zone.now)
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel_back
ticket
end
let(:ticket_9) do
travel_to DateTime.new 2037, 11, 2, 12, 30
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'merged',
priority_name: '2 normal',
close_at: Time.zone.now)
create(:ticket_article,
:inbound_email,
ticket: ticket)
travel_back
ticket
end
let(:ticket_after_72h) do
travel 72.hours do
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'closed',
priority_name: '2 normal',
close_at: 5.minutes.from_now)
create(:ticket_article,
:outbound_email,
ticket: ticket)
ticket
end
end
let(:ticket_before_40d) do
travel(-40.days) do
ticket = create(:ticket,
group: group_1,
customer: customer,
state_name: 'closed',
priority_name: '2 normal',
close_at: 5.minutes.from_now)
create(:ticket_article,
:outbound_email,
ticket: ticket)
ticket
end
end
matcher :match_tickets do
match do
if expected_tickets.blank?
actual_ticket_ids.blank?
else
# GenericTime returns string ids :o
actual_ticket_ids.map(&:to_i) == expected_tickets.map(&:id)
end
end
def expected_tickets
Array(expected)
end
def actual_ticket_ids
actual[:ticket_ids]
end
end
end

View file

@ -2,11 +2,12 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe SearchIndexBackend, searchindex: true do RSpec.describe SearchIndexBackend do
before do before do |example|
configure_elasticsearch next if !example.metadata[:searchindex]
rebuild_searchindex
configure_elasticsearch(required: true, rebuild: true)
end end
describe '.build_query' do describe '.build_query' do
@ -19,7 +20,7 @@ RSpec.describe SearchIndexBackend, searchindex: true do
end end
end end
describe '.search' do describe '.search', searchindex: true do
context 'query finds results' do context 'query finds results' do
@ -200,7 +201,8 @@ RSpec.describe SearchIndexBackend, searchindex: true do
end end
end end
describe '.remove' do describe '.remove', searchindex: true do
context 'record gets deleted' do context 'record gets deleted' do
let(:record_type) { 'Ticket'.freeze } let(:record_type) { 'Ticket'.freeze }
@ -239,7 +241,7 @@ RSpec.describe SearchIndexBackend, searchindex: true do
end end
end end
describe '.selectors' do describe '.selectors', searchindex: true do
let(:group1) { create :group } let(:group1) { create :group }
let(:organization1) { create :organization, note: 'hihi' } let(:organization1) { create :organization, note: 'hihi' }
@ -845,4 +847,145 @@ RSpec.describe SearchIndexBackend, searchindex: true do
end end
end end
end end
describe '.verify_date_range' do
let(:range_1) { { range: { created_at: { from: '2020-01-01T00:00:00.000Z', to: '2021-12-31T23:59:59Z' } } } }
let(:range_2) { { range: { created_at: { from: '2020-03-01T00:00:00.000Z', to: '2020-03-31T23:59:59Z' } } } }
let(:range_3) { { range: { created_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } }
let(:range_4) { { range: { updated_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } }
def build_payload(*ranges)
{
query: {
bool: {
must: ranges
}
}
}
end
it 'verifies single range' do
result = described_class.verify_date_range 'url', build_payload(range_1)
expect(result).to be_truthy
end
it 'verifies multiple intersecting ranges' do
result = described_class.verify_date_range 'url', build_payload(range_1, range_2)
expect(result).to be_truthy
end
it 'verifies non-intersecting ranges on different keys' do
result = described_class.verify_date_range 'url', build_payload(range_1, range_4)
expect(result).to be_truthy
end
it 'verifies payload without any ranges' do
result = described_class.verify_date_range 'url', build_payload
expect(result).to be_truthy
end
it 'verifies payload without payload' do
result = described_class.verify_date_range 'url', {}
expect(result).to be_truthy
end
it 'raises an error with multiple non-intersecting range' do
expect { described_class.verify_date_range 'url', build_payload(range_1, range_3) }
.to raise_error(%r{Conflicting date ranges})
end
context 'with a stubbed range' do
before do
allow(described_class).to receive(:convert_es_date_range).and_return(mock_range)
end
let(:mock_range) { instance_double('Range', overlaps?: true) }
it 'checks overlap once for 2 ranges' do
described_class.verify_date_range 'url', build_payload(range_1, range_2)
expect(mock_range).to have_received(:overlaps?).exactly(1).times
end
it 'checks overlap 3 times for 3 ranges' do
described_class.verify_date_range 'url', build_payload(range_1, range_2, range_3)
expect(mock_range).to have_received(:overlaps?).exactly(3).times
end
end
end
describe '.verify_single_key_range' do
let(:range_1) { DateTime.new(2020, 1, 1)..DateTime.new(2021, 12, 31) }
let(:range_2) { DateTime.new(2020, 3, 1)..DateTime.new(2020, 3, 31) }
let(:range_3) { DateTime.new(2018, 3, 1)..DateTime.new(2018, 3, 31) }
it 'returns true with a single range' do
result = described_class.verify_single_key_range [range_1]
expect(result).to be_truthy
end
it 'returns true with overlapping ranges' do
result = described_class.verify_single_key_range [range_1, range_2]
expect(result).to be_truthy
end
it 'returns false with non-overlapping ranges' do
result = described_class.verify_single_key_range [range_1, range_3]
expect(result).to be_falsey
end
end
describe '.convert_es_date_range' do
let(:from) { DateTime.new 2018, 1, 1, 17 }
let(:from_placeholder) { DateTime.new(-9999, 1, 1) }
let(:to) { DateTime.new 2020, 10, 1, 23 }
let(:to_placeholder) { DateTime.new 9999, 1, 1 }
it 'converts range' do
result = described_class.convert_es_date_range(
{
range: {
created_at: {
from: '2018-01-01T17:00:00.000Z',
to: '2020-10-01T23:00:00Z'
}
}
}
)
expect(result).to eq from..to
end
it 'converts less than' do
result = described_class.convert_es_date_range(
{
range: {
created_at: {
lt: '2020-10-01T23:00:00Z'
}
}
}
)
expect(result).to eq from_placeholder..to
end
it 'converts greater than' do
result = described_class.convert_es_date_range(
{
range: {
created_at: {
gt: '2018-01-01T17:00:00.000Z',
}
}
}
)
expect(result).to eq from..to_placeholder
end
end
end end

View file

@ -33,4 +33,42 @@ RSpec.describe 'Report', type: :system, searchindex: true do
end end
end end
end end
context 'report profiles are displayed' do
let!(:report_profile_active) { create(:report_profile) }
let!(:report_profile_inactive) { create(:report_profile, active: false) }
it 'shows report profiles' do
visit 'report'
expect(page)
.to have_css('ul.checkbox-list .label-text', text: report_profile_active.name)
.and have_no_css('ul.checkbox-list .label-text', text: report_profile_inactive.name)
end
end
context 'with report profiles with date-based conditions' do
let(:report_profile) { create(:report_profile, :condition_created_at, ticket_created_at: 1.year.ago) }
before do
freeze_time
report_profile
visit 'report'
end
it 'shows previous year for a profile with matching conditions' do
click '.js-timePickerYear', text: Time.zone.now.year - 1
click '.label-text', text: report_profile.name
expect(page).to have_no_css('.modal')
end
it 'throws error for a profile when showing a different year than described in the profile' do
click '.label-text', text: report_profile.name
in_modal disappears: false do
expect(page).to have_text 'Conflicting date ranges'
end
end
end
end end

File diff suppressed because it is too large Load diff