forest_liana 5.1.3 → 5.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/forest_liana/actions_controller.rb +82 -0
- data/app/controllers/forest_liana/resources_controller.rb +24 -2
- data/app/helpers/forest_liana/is_same_data_structure_helper.rb +44 -0
- data/app/models/forest_liana/model/action.rb +2 -1
- data/app/services/forest_liana/apimap_sorter.rb +1 -0
- data/app/services/forest_liana/filters_parser.rb +7 -3
- data/app/services/forest_liana/permissions_checker.rb +13 -1
- data/app/services/forest_liana/resources_getter.rb +3 -3
- data/app/services/forest_liana/scope_validator.rb +97 -0
- data/config/routes.rb +2 -0
- data/lib/forest_liana/bootstrapper.rb +19 -1
- data/lib/forest_liana/schema_file_updater.rb +1 -0
- data/lib/forest_liana/version.rb +1 -1
- data/spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb +87 -0
- data/spec/requests/actions_controller_spec.rb +136 -0
- data/spec/services/forest_liana/apimap_sorter_spec.rb +6 -4
- data/spec/services/forest_liana/schema_adapter_spec.rb +1 -1
- data/test/services/forest_liana/scope_validator_test.rb +185 -0
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e035848d2e7346973187338d78579301dcf062b2b6192ba383cd1f50dcccf584
|
4
|
+
data.tar.gz: 5f8a3ecdea7a8f4643922c27626397bfa3d497c14302f77f377153a22f064b0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a30b760625155ae65acf9c8d6941e1e8ddd408b98cd377fdd6f6385c943ceef74e72cf3318d1373f966f8eda48dd9c35c4198bdaeecba2c9c6ffd52f2285e465
|
7
|
+
data.tar.gz: 52b468bfea30b0c115c318ab60eefd2caf382c94df171ea53ec0e203d6869d823f0deb23b20f66939f2560a8611d55b2c6b64f1dcb0b5ffb3d461de3471db8a5
|
@@ -1,7 +1,89 @@
|
|
1
1
|
module ForestLiana
|
2
2
|
class ActionsController < ForestLiana::BaseController
|
3
|
+
|
3
4
|
def values
|
4
5
|
render serializer: nil, json: {}, status: :ok
|
5
6
|
end
|
7
|
+
|
8
|
+
def get_collection(collection_name)
|
9
|
+
ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_action(collection_name)
|
13
|
+
collection = get_collection(collection_name)
|
14
|
+
begin
|
15
|
+
collection.actions.find {|action| ActiveSupport::Inflector.parameterize(action.name) == params[:action_name]}
|
16
|
+
rescue => error
|
17
|
+
FOREST_LOGGER.error "Smart Action get action retrieval error: #{error}"
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_record
|
23
|
+
model = ForestLiana::SchemaUtils.find_model_from_collection_name(params[:collectionName])
|
24
|
+
redord_getter = ForestLiana::ResourceGetter.new(model, {:id => params[:recordIds][0]})
|
25
|
+
redord_getter.perform
|
26
|
+
redord_getter.record
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_smart_action_load_ctx(fields)
|
30
|
+
fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.merge!(value: nil))}
|
31
|
+
{:record => get_record, :fields => fields}
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_smart_action_change_ctx(fields)
|
35
|
+
fields = fields.reduce({}) {|p, c| p.update(c[:field] => c.permit!.to_h)}
|
36
|
+
{:record => get_record, :fields => fields}
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_result(result, formatted_fields, action)
|
40
|
+
if result.nil? || !result.is_a?(Hash)
|
41
|
+
return render status: 500, json: { error: 'Error in smart action load hook: hook must return an object' }
|
42
|
+
end
|
43
|
+
is_same_data_structure = ForestLiana::IsSameDataStructureHelper::Analyser.new(formatted_fields, result, 1)
|
44
|
+
unless is_same_data_structure.perform
|
45
|
+
return render status: 500, json: { error: 'Error in smart action hook: fields must be unchanged (no addition nor deletion allowed)' }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Apply result on fields (transform the object back to an array), preserve order.
|
49
|
+
fields = action.fields.map { |field| result[field[:field]] }
|
50
|
+
|
51
|
+
render serializer: nil, json: { fields: fields}, status: :ok
|
52
|
+
end
|
53
|
+
|
54
|
+
def load
|
55
|
+
action = get_action(params[:collectionName])
|
56
|
+
|
57
|
+
if !action
|
58
|
+
render status: 500, json: {error: 'Error in smart action load hook: cannot retrieve action from collection'}
|
59
|
+
else
|
60
|
+
# Transform fields from array to an object to ease usage in hook, adds null value.
|
61
|
+
context = get_smart_action_load_ctx(action.fields)
|
62
|
+
formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
|
63
|
+
|
64
|
+
# Call the user-defined load hook.
|
65
|
+
result = action.hooks[:load].(context)
|
66
|
+
|
67
|
+
handle_result(result, formatted_fields, action)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def change
|
72
|
+
action = get_action(params[:collectionName])
|
73
|
+
|
74
|
+
if !action
|
75
|
+
render status: 500, json: {error: 'Error in smart action change hook: cannot retrieve action from collection'}
|
76
|
+
else
|
77
|
+
# Transform fields from array to an object to ease usage in hook.
|
78
|
+
context = get_smart_action_change_ctx(params[:fields])
|
79
|
+
formatted_fields = context[:fields].clone # clone for following test on is_same_data_structure
|
80
|
+
|
81
|
+
# Call the user-defined change hook.
|
82
|
+
field_name = params[:fields].select { |field| field[:value] != field[:previousValue] }[0][:field]
|
83
|
+
result = action.hooks[:change][field_name].(context)
|
84
|
+
|
85
|
+
handle_result(result, formatted_fields, action)
|
86
|
+
end
|
87
|
+
end
|
6
88
|
end
|
7
89
|
end
|
@@ -22,7 +22,13 @@ module ForestLiana
|
|
22
22
|
checker = ForestLiana::PermissionsChecker.new(@resource, 'searchToEdit', @rendering_id)
|
23
23
|
return head :forbidden unless checker.is_authorized?
|
24
24
|
else
|
25
|
-
checker = ForestLiana::PermissionsChecker.new(
|
25
|
+
checker = ForestLiana::PermissionsChecker.new(
|
26
|
+
@resource,
|
27
|
+
'list',
|
28
|
+
@rendering_id,
|
29
|
+
nil,
|
30
|
+
get_collection_list_permission_info(forest_user, request)
|
31
|
+
)
|
26
32
|
return head :forbidden unless checker.is_authorized?
|
27
33
|
end
|
28
34
|
|
@@ -51,7 +57,13 @@ module ForestLiana
|
|
51
57
|
|
52
58
|
def count
|
53
59
|
begin
|
54
|
-
checker = ForestLiana::PermissionsChecker.new(
|
60
|
+
checker = ForestLiana::PermissionsChecker.new(
|
61
|
+
@resource,
|
62
|
+
'list',
|
63
|
+
@rendering_id,
|
64
|
+
nil,
|
65
|
+
get_collection_list_permission_info(forest_user, request)
|
66
|
+
)
|
55
67
|
return head :forbidden unless checker.is_authorized?
|
56
68
|
|
57
69
|
getter = ForestLiana::ResourcesGetter.new(@resource, params)
|
@@ -232,5 +244,15 @@ module ForestLiana
|
|
232
244
|
collection_name = ForestLiana.name_for(@resource)
|
233
245
|
@collection ||= ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
|
234
246
|
end
|
247
|
+
|
248
|
+
# NOTICE: Return a formatted object containing the request condition filters and
|
249
|
+
# the user id used by the scope validator class to validate if scope is
|
250
|
+
# in request
|
251
|
+
def get_collection_list_permission_info(user, collection_list_request)
|
252
|
+
{
|
253
|
+
user_id: user['id'],
|
254
|
+
filters: collection_list_request[:filters],
|
255
|
+
}
|
256
|
+
end
|
235
257
|
end
|
236
258
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ForestLiana
|
4
|
+
module IsSameDataStructureHelper
|
5
|
+
class Analyser
|
6
|
+
def initialize(object, other, deep = 0)
|
7
|
+
@object = object
|
8
|
+
@other = other
|
9
|
+
@deep = deep
|
10
|
+
end
|
11
|
+
|
12
|
+
def are_objects(object, other)
|
13
|
+
object && other && object.is_a?(Hash) && other.is_a?(Hash)
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_keys(object, other, step = 0)
|
17
|
+
unless are_objects(object, other)
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
object_keys = object.keys
|
22
|
+
other_keys = other.keys
|
23
|
+
|
24
|
+
if object_keys.length != other_keys.length
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
|
28
|
+
object_keys_set = object_keys.to_set
|
29
|
+
other_keys.each { |key|
|
30
|
+
if !object_keys_set.member?(key) || (step + 1 <= @deep && !check_keys(object[key], other[key], step + 1))
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
}
|
34
|
+
|
35
|
+
return true
|
36
|
+
end
|
37
|
+
|
38
|
+
def perform
|
39
|
+
check_keys(@object, @other)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -5,7 +5,7 @@ class ForestLiana::Model::Action
|
|
5
5
|
extend ActiveModel::Naming
|
6
6
|
|
7
7
|
attr_accessor :id, :name, :base_url, :endpoint, :http_method, :fields, :redirect,
|
8
|
-
:type, :download
|
8
|
+
:type, :download, :hooks
|
9
9
|
|
10
10
|
def initialize(attributes = {})
|
11
11
|
if attributes.key?(:global)
|
@@ -66,6 +66,7 @@ class ForestLiana::Model::Action
|
|
66
66
|
@base_url ||= nil
|
67
67
|
@type ||= "bulk"
|
68
68
|
@download ||= false
|
69
|
+
@hooks = !@hooks.nil? ? @hooks.symbolize_keys : nil
|
69
70
|
end
|
70
71
|
|
71
72
|
def persisted?
|
@@ -81,11 +81,15 @@ module ForestLiana
|
|
81
81
|
parsed_field = parse_field_name(field)
|
82
82
|
parsed_operator = parse_operator(operator)
|
83
83
|
parsed_value = parse_value(operator, value)
|
84
|
+
field_and_operator = "#{parsed_field} #{parsed_operator}"
|
84
85
|
|
85
|
-
if Rails::VERSION::MAJOR
|
86
|
-
|
86
|
+
if Rails::VERSION::MAJOR < 5
|
87
|
+
"#{field_and_operator} #{ActiveRecord::Base.sanitize(parsed_value)}"
|
88
|
+
# NOTICE: sanitize method as been removed in Rails 5.1 and sanitize_sql introduced in Rails 5.2.
|
89
|
+
elsif Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR == 1
|
90
|
+
"#{field_and_operator} #{ActiveRecord::Base.connection.quote(parsed_value)}"
|
87
91
|
else
|
88
|
-
"#{
|
92
|
+
ActiveRecord::Base.sanitize_sql(["#{field_and_operator} ?", parsed_value])
|
89
93
|
end
|
90
94
|
end
|
91
95
|
|
@@ -3,11 +3,12 @@ module ForestLiana
|
|
3
3
|
@@permissions_per_rendering = Hash.new
|
4
4
|
@@expiration_in_seconds = (ENV['FOREST_PERMISSIONS_EXPIRATION_IN_SECONDS'] || 3600).to_i
|
5
5
|
|
6
|
-
def initialize(resource, permission_name, rendering_id, smart_action_parameters = nil)
|
6
|
+
def initialize(resource, permission_name, rendering_id, smart_action_parameters = nil, collection_list_parameters = nil)
|
7
7
|
@collection_name = ForestLiana.name_for(resource)
|
8
8
|
@permission_name = permission_name
|
9
9
|
@rendering_id = rendering_id
|
10
10
|
@smart_action_parameters = smart_action_parameters
|
11
|
+
@collection_list_parameters = collection_list_parameters
|
11
12
|
end
|
12
13
|
|
13
14
|
def is_authorized?
|
@@ -46,12 +47,23 @@ module ForestLiana
|
|
46
47
|
return @allowed && (@users.nil?|| @users.include?(@user_id.to_i));
|
47
48
|
end
|
48
49
|
|
50
|
+
def collection_list_allowed?(scope_permissions)
|
51
|
+
return ForestLiana::ScopeValidator.new(
|
52
|
+
scope_permissions['filter'],
|
53
|
+
scope_permissions['dynamicScopesValues']['users']
|
54
|
+
).is_scope_in_request?(@collection_list_parameters)
|
55
|
+
end
|
56
|
+
|
49
57
|
def is_allowed?
|
50
58
|
permissions = get_permissions
|
51
59
|
if permissions && permissions[@collection_name] &&
|
52
60
|
permissions[@collection_name]['collection']
|
53
61
|
if @permission_name === 'actions'
|
54
62
|
return smart_action_allowed?(permissions[@collection_name]['actions'])
|
63
|
+
# NOTICE: Permissions[@collection_name]['scope'] will either contains conditions filter and
|
64
|
+
# dynamic user values definition, or null for collection that does not use scopes
|
65
|
+
elsif @permission_name === 'list' and permissions[@collection_name]['scope']
|
66
|
+
return collection_list_allowed?(permissions[@collection_name]['scope'])
|
55
67
|
else
|
56
68
|
return permissions[@collection_name]['collection'][@permission_name]
|
57
69
|
end
|
@@ -12,11 +12,11 @@ module ForestLiana
|
|
12
12
|
@collection = get_collection(@collection_name)
|
13
13
|
@fields_to_serialize = get_fields_to_serialize
|
14
14
|
@field_names_requested = field_names_requested
|
15
|
-
get_segment
|
16
|
-
compute_includes
|
15
|
+
get_segment
|
16
|
+
compute_includes
|
17
17
|
@search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection)
|
18
18
|
|
19
|
-
prepare_query
|
19
|
+
prepare_query
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.get_ids_from_request(params)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module ForestLiana
|
2
|
+
class ScopeValidator
|
3
|
+
def initialize(scope_permissions, users_variable_values)
|
4
|
+
@scope_filters = scope_permissions
|
5
|
+
@users_variable_values = users_variable_values
|
6
|
+
end
|
7
|
+
|
8
|
+
def is_scope_in_request?(scope_request)
|
9
|
+
begin
|
10
|
+
filters = JSON.parse(scope_request[:filters])
|
11
|
+
rescue JSON::ParserError
|
12
|
+
raise ForestLiana::Errors::HTTP422Error.new('Invalid filters JSON format')
|
13
|
+
end
|
14
|
+
@computed_scope = compute_condition_filters_from_scope(scope_request[:user_id])
|
15
|
+
|
16
|
+
# NOTICE: Perfom a travel in the request condition filters tree to find the scope
|
17
|
+
tagged_scope_filters = get_scope_found_in_request(filters)
|
18
|
+
|
19
|
+
# NOTICE: Permission system always send an aggregator even if there is only one condition
|
20
|
+
# In that case, if the condition is valid, then request was not edited
|
21
|
+
return !tagged_scope_filters.nil? if @scope_filters['conditions'].length == 1
|
22
|
+
|
23
|
+
# NOTICE: If there is more than one condition, do a final validation on the condition filters
|
24
|
+
return tagged_scope_filters != nil &&
|
25
|
+
tagged_scope_filters[:aggregator] == @scope_filters['aggregator'] &&
|
26
|
+
tagged_scope_filters[:conditions] &&
|
27
|
+
tagged_scope_filters[:conditions].length == @scope_filters['conditions'].length
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def compute_condition_filters_from_scope(user_id)
|
33
|
+
computed_condition_filters = @scope_filters.clone
|
34
|
+
computed_condition_filters['conditions'].each do |condition|
|
35
|
+
if condition.include?('value') &&
|
36
|
+
!condition['value'].nil? &&
|
37
|
+
condition['value'].start_with?('$') &&
|
38
|
+
@users_variable_values.include?(user_id)
|
39
|
+
condition['value'] = @users_variable_values[user_id][condition['value']]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
return computed_condition_filters
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_scope_found_in_request(filters)
|
46
|
+
return nil unless filters
|
47
|
+
return search_scope_aggregation(filters)
|
48
|
+
end
|
49
|
+
|
50
|
+
def search_scope_aggregation(node)
|
51
|
+
ensure_valid_aggregation(node)
|
52
|
+
|
53
|
+
return is_scope_condition?(node) unless node['aggregator']
|
54
|
+
|
55
|
+
# NOTICE: Remove conditions that are not from the scope
|
56
|
+
filtered_conditions = node['conditions'].map { |condition|
|
57
|
+
search_scope_aggregation(condition)
|
58
|
+
}.select { |condition|
|
59
|
+
condition
|
60
|
+
}
|
61
|
+
|
62
|
+
# NOTICE: If there is only one condition filter left and its current aggregator is
|
63
|
+
# an "and", this condition filter is the searched scope
|
64
|
+
if (filtered_conditions.length == 1 &&
|
65
|
+
filtered_conditions.first.is_a?(Hash) &&
|
66
|
+
filtered_conditions.first.include?(:aggregator) &&
|
67
|
+
node['aggregator'] == 'and')
|
68
|
+
return filtered_conditions.first
|
69
|
+
end
|
70
|
+
|
71
|
+
# NOTICE: Otherwise, validate if the current node is the scope and return nil
|
72
|
+
# if it's not
|
73
|
+
return (filtered_conditions.length == @scope_filters['conditions'].length &&
|
74
|
+
node['aggregator'] == @scope_filters['aggregator']) ?
|
75
|
+
{ aggregator: node['aggregator'], conditions: filtered_conditions } :
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def is_scope_condition?(condition)
|
80
|
+
ensure_valid_condition(condition)
|
81
|
+
return @computed_scope['conditions'].include?(condition)
|
82
|
+
end
|
83
|
+
|
84
|
+
def ensure_valid_aggregation(node)
|
85
|
+
raise ForestLiana::Errors::HTTP422Error.new('Filters cannot be a raw value') unless node.is_a?(Hash)
|
86
|
+
raise_empty_condition_in_filter_error if node.empty?
|
87
|
+
end
|
88
|
+
|
89
|
+
def ensure_valid_condition(condition)
|
90
|
+
raise_empty_condition_in_filter_error if condition.empty?
|
91
|
+
raise ForestLiana::Errors::HTTP422Error.new('Condition cannot be a raw value') unless condition.is_a?(Hash)
|
92
|
+
unless condition['field'].is_a?(String) and condition['operator'].is_a?(String)
|
93
|
+
raise ForestLiana::Errors::HTTP422Error.new('Invalid condition format')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/config/routes.rb
CHANGED
@@ -38,6 +38,14 @@ module ForestLiana
|
|
38
38
|
|
39
39
|
private
|
40
40
|
|
41
|
+
def get_collection(collection_name)
|
42
|
+
ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_action(collection, action_name)
|
46
|
+
collection.actions.find {|action| action.name == action_name}
|
47
|
+
end
|
48
|
+
|
41
49
|
def generate_apimap
|
42
50
|
create_apimap
|
43
51
|
require_lib_forest_liana
|
@@ -45,6 +53,17 @@ module ForestLiana
|
|
45
53
|
|
46
54
|
if Rails.env.development?
|
47
55
|
@collections_sent = ForestLiana.apimap.as_json
|
56
|
+
|
57
|
+
@collections_sent.each do |collection|
|
58
|
+
collection['actions'].each do |action|
|
59
|
+
c = get_collection(collection['name'])
|
60
|
+
a = get_action(c, action['name'])
|
61
|
+
load = !a.hooks.nil? && a.hooks.key?(:load) && a.hooks[:load].is_a?(Proc)
|
62
|
+
change = !a.hooks.nil? && a.hooks.key?(:change) && a.hooks[:change].is_a?(Hash) ? a.hooks[:change].keys : []
|
63
|
+
action['hooks'] = {:load => load, :change => change}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
48
67
|
@meta_sent = ForestLiana.meta
|
49
68
|
SchemaFileUpdater.new(SCHEMA_FILENAME, @collections_sent, @meta_sent).perform()
|
50
69
|
else
|
@@ -175,7 +194,6 @@ module ForestLiana
|
|
175
194
|
def setup_forest_liana_meta
|
176
195
|
ForestLiana.meta = {
|
177
196
|
database_type: database_type,
|
178
|
-
framework_version: Gem.loaded_specs["rails"].version.version,
|
179
197
|
liana: 'forest-rails',
|
180
198
|
liana_version: ForestLiana::VERSION,
|
181
199
|
orm_version: Gem.loaded_specs["activerecord"].version.version
|
data/lib/forest_liana/version.rb
CHANGED
@@ -0,0 +1,87 @@
|
|
1
|
+
module ForestLiana
|
2
|
+
context 'IsSameDataStructure class' do
|
3
|
+
it 'should: be valid with simple data' do
|
4
|
+
object = {:a => 'a', :b => 'b'}
|
5
|
+
other = {:a => 'a', :b => 'b'}
|
6
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other).perform
|
7
|
+
expect(result).to be true
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should: be invalid with simple data' do
|
11
|
+
object = {:a => 'a', :b => 'b'}
|
12
|
+
other = {:a => 'a', :c => 'c'}
|
13
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other).perform
|
14
|
+
expect(result).to be false
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should: be invalid with not same hash' do
|
18
|
+
object = {:a => 'a', :b => 'b'}
|
19
|
+
other = {:a => 'a', :b => 'b', :c => 'c'}
|
20
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other).perform
|
21
|
+
expect(result).to be false
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should: be invalid with nil' do
|
25
|
+
object = nil
|
26
|
+
other = {:a => 'a', :b => 'b', :c => 'c'}
|
27
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other).perform
|
28
|
+
expect(result).to be false
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should: be invalid with not hash' do
|
32
|
+
object = nil
|
33
|
+
other = {:a => 'a', :b => 'b', :c => 'c'}
|
34
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other).perform
|
35
|
+
expect(result).to be false
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should: be invalid with integer' do
|
39
|
+
object = 1
|
40
|
+
other = {:a => 'a', :b => 'b', :c => 'c'}
|
41
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other).perform
|
42
|
+
expect(result).to be false
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should: be invalid with string' do
|
46
|
+
object = 'a'
|
47
|
+
other = {:a => 'a', :b => 'b', :c => 'c'}
|
48
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other).perform
|
49
|
+
expect(result).to be false
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should: be valid with depth 1' do
|
53
|
+
object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
|
54
|
+
other = {:a => {:c => 'c'}, :b => {:d => 'd'}}
|
55
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
|
56
|
+
expect(result).to be true
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should: be invalid with depth 1' do
|
60
|
+
object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
|
61
|
+
other = {:a => {:c => 'c'}, :b => {:e => 'e'}}
|
62
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
|
63
|
+
expect(result).to be false
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should: be invalid with depth 1 and nil' do
|
67
|
+
object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
|
68
|
+
other = {:a => {:c => 'c'}, :b => nil}
|
69
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
|
70
|
+
expect(result).to be false
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should: be invalid with depth 1 and integer' do
|
74
|
+
object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
|
75
|
+
other = {:a => {:c => 'c'}, :b => 1}
|
76
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
|
77
|
+
expect(result).to be false
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should: be invalid with depth 1 and string' do
|
81
|
+
object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
|
82
|
+
other = {:a => {:c => 'c'}, :b => 'b'}
|
83
|
+
result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
|
84
|
+
expect(result).to be false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
describe 'Requesting Actions routes', :type => :request do
|
4
|
+
before(:each) do
|
5
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
|
6
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
|
7
|
+
Island.create(name: 'Corsica')
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
Island.destroy_all
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'call /values' do
|
15
|
+
it 'should respond 200' do
|
16
|
+
post '/forest/actions/foo/values', {}
|
17
|
+
expect(response.status).to eq(200)
|
18
|
+
expect(response.body).to be {}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'hooks' do
|
23
|
+
foo = {
|
24
|
+
field: 'foo',
|
25
|
+
type: 'String',
|
26
|
+
default_value: nil,
|
27
|
+
enums: nil,
|
28
|
+
is_required: false,
|
29
|
+
reference: nil,
|
30
|
+
description: nil,
|
31
|
+
widget: nil,
|
32
|
+
}
|
33
|
+
action_definition = {
|
34
|
+
name: 'my_action',
|
35
|
+
fields: [foo],
|
36
|
+
hooks: {
|
37
|
+
:load => -> (context) {
|
38
|
+
context[:fields]
|
39
|
+
},
|
40
|
+
:change => {
|
41
|
+
'foo' => -> (context) {
|
42
|
+
fields = context[:fields]
|
43
|
+
fields['foo'][:value] = 'baz'
|
44
|
+
return fields
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
fail_action_definition = {
|
50
|
+
name: 'fail_action',
|
51
|
+
fields: [foo],
|
52
|
+
hooks: {
|
53
|
+
:load => -> (context) {
|
54
|
+
1
|
55
|
+
},
|
56
|
+
:change => {
|
57
|
+
'foo' => -> (context) {
|
58
|
+
1
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
cheat_action_definition = {
|
64
|
+
name: 'cheat_action',
|
65
|
+
fields: [foo],
|
66
|
+
hooks: {
|
67
|
+
:load => -> (context) {
|
68
|
+
context[:fields]['baz'] = foo.clone.update({field: 'baz'})
|
69
|
+
context[:fields]
|
70
|
+
},
|
71
|
+
:change => {
|
72
|
+
'foo' => -> (context) {
|
73
|
+
context[:fields]['baz'] = foo.clone.update({field: 'baz'})
|
74
|
+
context[:fields]
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}
|
79
|
+
action = ForestLiana::Model::Action.new(action_definition)
|
80
|
+
fail_action = ForestLiana::Model::Action.new(fail_action_definition)
|
81
|
+
cheat_action = ForestLiana::Model::Action.new(cheat_action_definition)
|
82
|
+
island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
|
83
|
+
island.actions = [action, fail_action, cheat_action]
|
84
|
+
|
85
|
+
describe 'call /load' do
|
86
|
+
params = {recordIds: [1], collectionName: 'Island'}
|
87
|
+
|
88
|
+
it 'should respond 200' do
|
89
|
+
post '/forest/actions/my_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
|
90
|
+
expect(response.status).to eq(200)
|
91
|
+
expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).stringify_keys]})
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should respond 500 with bad params' do
|
95
|
+
post '/forest/actions/my_action/hooks/load', {}
|
96
|
+
expect(response.status).to eq(500)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should respond 500 with bad hook result type' do
|
100
|
+
post '/forest/actions/fail_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
|
101
|
+
expect(response.status).to eq(500)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should respond 500 with bad hook result data structure' do
|
105
|
+
post '/forest/actions/cheat_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
|
106
|
+
expect(response.status).to eq(500)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'call /change' do
|
111
|
+
updated_foo = foo.clone.merge({:previousValue => nil, :value => 'bar'})
|
112
|
+
params = {recordIds: [1], fields: [updated_foo], collectionName: 'Island'}
|
113
|
+
|
114
|
+
it 'should respond 200' do
|
115
|
+
post '/forest/actions/my_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
|
116
|
+
expect(response.status).to eq(200)
|
117
|
+
expect(JSON.parse(response.body)).to eq({'fields' => [updated_foo.merge({:value => 'baz'}).stringify_keys]})
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should respond 500 with bad params' do
|
121
|
+
post '/forest/actions/my_action/hooks/change', {}
|
122
|
+
expect(response.status).to eq(500)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'should respond 500 with bad hook result type' do
|
126
|
+
post '/forest/actions/fail_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
|
127
|
+
expect(response.status).to eq(500)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should respond 500 with bad hook result data structure' do
|
131
|
+
post '/forest/actions/cheat_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
|
132
|
+
expect(response.status).to eq(500)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -75,7 +75,8 @@ module ForestLiana
|
|
75
75
|
type: 'File',
|
76
76
|
field: 'File'
|
77
77
|
}],
|
78
|
-
http_method: nil
|
78
|
+
http_method: nil,
|
79
|
+
hooks: nil,
|
79
80
|
}
|
80
81
|
}, {
|
81
82
|
attributes: {
|
@@ -95,7 +96,8 @@ module ForestLiana
|
|
95
96
|
download: nil,
|
96
97
|
endpoint: nil,
|
97
98
|
redirect: nil,
|
98
|
-
'http_method': nil
|
99
|
+
'http_method': nil,
|
100
|
+
hooks: nil,
|
99
101
|
}
|
100
102
|
}]
|
101
103
|
}
|
@@ -148,8 +150,8 @@ module ForestLiana
|
|
148
150
|
end
|
149
151
|
|
150
152
|
it 'should sort the included actions and segments objects attributes values' do
|
151
|
-
expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download'])
|
152
|
-
expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields'])
|
153
|
+
expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download', 'hooks'])
|
154
|
+
expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields', 'hooks'])
|
153
155
|
expect(apimap_sorted['included'][2]['attributes'].keys).to eq(['name'])
|
154
156
|
expect(apimap_sorted['included'][3]['attributes'].keys).to eq(['name'])
|
155
157
|
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module ForestLiana
|
2
|
+
class ScopeValidatorTest < ActiveSupport::TestCase
|
3
|
+
test 'Request with aggregated condition filters should be allowed if it matches the scope exactly' do
|
4
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
5
|
+
'aggregator' => 'and',
|
6
|
+
'conditions' => [
|
7
|
+
{ 'field' => 'name', 'value' => 'john', 'operator' => 'equal' },
|
8
|
+
{ 'field' => 'price', 'value' => '2500', 'operator' => 'equal' }
|
9
|
+
]
|
10
|
+
}, [])
|
11
|
+
|
12
|
+
allowed = scope_validator.is_scope_in_request?({
|
13
|
+
user_id: '1',
|
14
|
+
filters: JSON.generate({
|
15
|
+
aggregator: 'and',
|
16
|
+
conditions: [
|
17
|
+
{ field: 'name', value: 'john', operator: 'equal' },
|
18
|
+
{ field: 'price', value: '2500', operator: 'equal' }
|
19
|
+
]
|
20
|
+
})
|
21
|
+
})
|
22
|
+
assert allowed == true
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'Request with simple condition filter should be allowed if it matches the scope exactly' do
|
26
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
27
|
+
'aggregator' => 'and',
|
28
|
+
'conditions' => [
|
29
|
+
{ 'field' => 'field', 'value' => 'value', 'operator' => 'equal' }
|
30
|
+
]
|
31
|
+
}, [])
|
32
|
+
allowed = scope_validator.is_scope_in_request?({
|
33
|
+
user_id: '1',
|
34
|
+
filters: JSON.generate({
|
35
|
+
field: 'field', value: 'value', operator: 'equal'
|
36
|
+
})
|
37
|
+
})
|
38
|
+
assert allowed == true
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'Request with multiples condition filters should be allowed if it contains the scope ' do
|
42
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
43
|
+
'aggregator' => 'and',
|
44
|
+
'conditions' => [
|
45
|
+
{ 'field' => 'name', 'value' => 'doe', 'operator' => 'equal' }
|
46
|
+
]
|
47
|
+
}, []
|
48
|
+
)
|
49
|
+
|
50
|
+
allowed = scope_validator.is_scope_in_request?({
|
51
|
+
user_id: '1',
|
52
|
+
filters: JSON.generate({
|
53
|
+
aggregator: 'and',
|
54
|
+
conditions: [
|
55
|
+
{ field: 'name', value: 'doe', operator: 'equal' },
|
56
|
+
{ field: 'field2', value: 'value2', operator: 'equal' }
|
57
|
+
]
|
58
|
+
})
|
59
|
+
})
|
60
|
+
assert allowed == true
|
61
|
+
end
|
62
|
+
|
63
|
+
test 'Request with dynamic user values should be allowed if it matches the scope exactly' do
|
64
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
65
|
+
'aggregator' => 'and',
|
66
|
+
'conditions' => [
|
67
|
+
{ 'field' => 'name', 'value' => '$currentUser.lastname', 'operator' => 'equal' }
|
68
|
+
],
|
69
|
+
}, {
|
70
|
+
'1' => { '$currentUser.lastname' => 'john' }
|
71
|
+
})
|
72
|
+
|
73
|
+
allowed = scope_validator.is_scope_in_request?({
|
74
|
+
user_id: '1',
|
75
|
+
filters: JSON.generate({
|
76
|
+
'field' => 'name', 'value' => 'john', 'operator' => 'equal'
|
77
|
+
})
|
78
|
+
})
|
79
|
+
assert allowed == true
|
80
|
+
end
|
81
|
+
|
82
|
+
test 'Request with multiples aggregation and dynamic values should be allowed if it contains the scope' do
|
83
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
84
|
+
'aggregator' => 'or',
|
85
|
+
'conditions' => [
|
86
|
+
{ 'field' => 'price', 'value' => '2500', 'operator' => 'equal' },
|
87
|
+
{ 'field' => 'name', 'value' => '$currentUser.lastname', 'operator' => 'equal' }
|
88
|
+
]
|
89
|
+
}, {
|
90
|
+
'1' => { '$currentUser.lastname' => 'john' }
|
91
|
+
})
|
92
|
+
|
93
|
+
allowed = scope_validator.is_scope_in_request?({
|
94
|
+
user_id: '1',
|
95
|
+
filters: JSON.generate({
|
96
|
+
aggregator: 'and',
|
97
|
+
conditions: [
|
98
|
+
{ field: 'field', value: 'value', operator: 'equal' },
|
99
|
+
{
|
100
|
+
aggregator: 'or',
|
101
|
+
conditions: [
|
102
|
+
{ field: 'price', value: '2500', operator: 'equal' },
|
103
|
+
{ field: 'name', value: 'john', operator: 'equal' }
|
104
|
+
]
|
105
|
+
}
|
106
|
+
]
|
107
|
+
})
|
108
|
+
})
|
109
|
+
assert allowed == true
|
110
|
+
end
|
111
|
+
|
112
|
+
test 'Request that does not match the expect scope should not be allowed' do
|
113
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
114
|
+
'aggregator' => 'and',
|
115
|
+
'conditions' => [
|
116
|
+
{ 'field' => 'name', 'value' => 'john', 'operator' => 'equal' },
|
117
|
+
{ 'field' => 'price', 'value' => '2500', 'operator' => 'equal' }
|
118
|
+
]
|
119
|
+
}, [])
|
120
|
+
|
121
|
+
allowed = scope_validator.is_scope_in_request?({
|
122
|
+
user_id: '1',
|
123
|
+
filters: JSON.generate({
|
124
|
+
aggregator: 'and',
|
125
|
+
conditions: [
|
126
|
+
{ field: 'name', value: 'definitely_not_john', operator: 'equal' },
|
127
|
+
{ field: 'price', value: '0', operator: 'equal' }
|
128
|
+
]
|
129
|
+
})
|
130
|
+
})
|
131
|
+
assert allowed == false
|
132
|
+
end
|
133
|
+
|
134
|
+
test 'Request that are missing part of the scope should not be allowed' do
|
135
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
136
|
+
'aggregator' => 'and',
|
137
|
+
'conditions' => [
|
138
|
+
{ 'field' => 'name', 'value' => 'john', 'operator' => 'equal' },
|
139
|
+
{ 'field' => 'price', 'value' => '2500', 'operator' => 'equal' }
|
140
|
+
]
|
141
|
+
}, [])
|
142
|
+
|
143
|
+
allowed = scope_validator.is_scope_in_request?({
|
144
|
+
user_id: '1',
|
145
|
+
filters: JSON.generate({
|
146
|
+
aggregator: 'and',
|
147
|
+
conditions: [
|
148
|
+
{ field: 'name', value: 'john', operator: 'equal' },
|
149
|
+
]
|
150
|
+
})
|
151
|
+
})
|
152
|
+
assert allowed == false
|
153
|
+
end
|
154
|
+
|
155
|
+
test 'Request that does not have a top aggregator being "and" should not be allowed' do
|
156
|
+
scope_validator = ForestLiana::ScopeValidator.new({
|
157
|
+
'aggregator' => 'and',
|
158
|
+
'conditions' => [
|
159
|
+
{ 'field' => 'price', 'value' => '2500', 'operator' => 'equal' },
|
160
|
+
{ 'field' => 'name', 'value' => '$currentUser.lastname', 'operator' => 'equal' }
|
161
|
+
]
|
162
|
+
}, {
|
163
|
+
'1' => { '$currentUser.lastname' => 'john' }
|
164
|
+
})
|
165
|
+
|
166
|
+
allowed = scope_validator.is_scope_in_request?({
|
167
|
+
user_id: '1',
|
168
|
+
filters: JSON.generate({
|
169
|
+
aggregator: 'or',
|
170
|
+
conditions: [
|
171
|
+
{ field: 'field', value: 'value', operator: 'equal' },
|
172
|
+
{
|
173
|
+
aggregator: 'and',
|
174
|
+
conditions: [
|
175
|
+
{ field: 'price', value: '2500', operator: 'equal' },
|
176
|
+
{ field: 'name', value: 'john', operator: 'equal' }
|
177
|
+
]
|
178
|
+
}
|
179
|
+
]
|
180
|
+
})
|
181
|
+
})
|
182
|
+
assert allowed == false
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forest_liana
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sandro Munda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -211,6 +211,7 @@ files:
|
|
211
211
|
- app/helpers/forest_liana/adapter_helper.rb
|
212
212
|
- app/helpers/forest_liana/application_helper.rb
|
213
213
|
- app/helpers/forest_liana/decoration_helper.rb
|
214
|
+
- app/helpers/forest_liana/is_same_data_structure_helper.rb
|
214
215
|
- app/helpers/forest_liana/query_helper.rb
|
215
216
|
- app/helpers/forest_liana/schema_helper.rb
|
216
217
|
- app/models/forest_liana/model/action.rb
|
@@ -262,6 +263,7 @@ files:
|
|
262
263
|
- app/services/forest_liana/resources_getter.rb
|
263
264
|
- app/services/forest_liana/schema_adapter.rb
|
264
265
|
- app/services/forest_liana/schema_utils.rb
|
266
|
+
- app/services/forest_liana/scope_validator.rb
|
265
267
|
- app/services/forest_liana/search_query_builder.rb
|
266
268
|
- app/services/forest_liana/stat_getter.rb
|
267
269
|
- app/services/forest_liana/stripe_base_getter.rb
|
@@ -335,9 +337,11 @@ files:
|
|
335
337
|
- spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb
|
336
338
|
- spec/dummy/db/migrate/20190716135241_add_type_to_user.rb
|
337
339
|
- spec/dummy/db/schema.rb
|
340
|
+
- spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
|
338
341
|
- spec/helpers/forest_liana/query_helper_spec.rb
|
339
342
|
- spec/helpers/forest_liana/schema_helper_spec.rb
|
340
343
|
- spec/rails_helper.rb
|
344
|
+
- spec/requests/actions_controller_spec.rb
|
341
345
|
- spec/requests/resources_spec.rb
|
342
346
|
- spec/services/forest_liana/apimap_sorter_spec.rb
|
343
347
|
- spec/services/forest_liana/filters_parser_spec.rb
|
@@ -432,6 +436,7 @@ files:
|
|
432
436
|
- test/services/forest_liana/resource_updater_test.rb
|
433
437
|
- test/services/forest_liana/resources_getter_test.rb
|
434
438
|
- test/services/forest_liana/schema_adapter_test.rb
|
439
|
+
- test/services/forest_liana/scope_validator_test.rb
|
435
440
|
- test/services/forest_liana/value_stat_getter_test.rb
|
436
441
|
- test/test_helper.rb
|
437
442
|
homepage: https://github.com/ForestAdmin/forest-rails
|
@@ -453,8 +458,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
453
458
|
- !ruby/object:Gem::Version
|
454
459
|
version: '0'
|
455
460
|
requirements: []
|
456
|
-
|
457
|
-
rubygems_version: 2.7.7
|
461
|
+
rubygems_version: 3.0.8
|
458
462
|
signing_key:
|
459
463
|
specification_version: 4
|
460
464
|
summary: Official Rails Liana for Forest
|
@@ -469,6 +473,7 @@ test_files:
|
|
469
473
|
- test/fixtures/has_many_field.yml
|
470
474
|
- test/fixtures/string_field.yml
|
471
475
|
- test/services/forest_liana/resources_getter_test.rb
|
476
|
+
- test/services/forest_liana/scope_validator_test.rb
|
472
477
|
- test/services/forest_liana/schema_adapter_test.rb
|
473
478
|
- test/services/forest_liana/has_many_getter_test.rb
|
474
479
|
- test/services/forest_liana/value_stat_getter_test.rb
|
@@ -554,6 +559,7 @@ test_files:
|
|
554
559
|
- spec/services/forest_liana/apimap_sorter_spec.rb
|
555
560
|
- spec/services/forest_liana/filters_parser_spec.rb
|
556
561
|
- spec/spec_helper.rb
|
562
|
+
- spec/requests/actions_controller_spec.rb
|
557
563
|
- spec/requests/resources_spec.rb
|
558
564
|
- spec/dummy/README.rdoc
|
559
565
|
- spec/dummy/app/views/layouts/application.html.erb
|
@@ -596,5 +602,6 @@ test_files:
|
|
596
602
|
- spec/dummy/config/initializers/backtrace_silencers.rb
|
597
603
|
- spec/dummy/config/database.yml
|
598
604
|
- spec/helpers/forest_liana/schema_helper_spec.rb
|
605
|
+
- spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
|
599
606
|
- spec/helpers/forest_liana/query_helper_spec.rb
|
600
607
|
- spec/rails_helper.rb
|