lenjador 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +8 -0
- data/README.md +121 -0
- data/Rakefile +7 -0
- data/benchmark/whitelisting.rb +53 -0
- data/lenjador.gemspec +36 -0
- data/lib/lenjador/adapters/stdout_adapter.rb +29 -0
- data/lib/lenjador/adapters/stdout_json_adapter.rb +60 -0
- data/lib/lenjador/adapters.rb +22 -0
- data/lib/lenjador/null_logger.rb +18 -0
- data/lib/lenjador/preprocessors/blacklist.rb +61 -0
- data/lib/lenjador/preprocessors/json_pointer_trie.rb +40 -0
- data/lib/lenjador/preprocessors/strategies/mask.rb +43 -0
- data/lib/lenjador/preprocessors/strategies/prune.rb +44 -0
- data/lib/lenjador/preprocessors/whitelist.rb +56 -0
- data/lib/lenjador/preprocessors.rb +18 -0
- data/lib/lenjador/utils.rb +91 -0
- data/lib/lenjador.rb +74 -0
- data/spec/adapters/stdout_adapter_spec.rb +48 -0
- data/spec/adapters/stdout_json_adapter_spec.rb +62 -0
- data/spec/lenjador_spec.rb +142 -0
- data/spec/preprocessors/blacklist_spec.rb +131 -0
- data/spec/preprocessors/json_pointer_trie.rb +36 -0
- data/spec/preprocessors/whitelist_spec.rb +335 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/implement_interface.rb +11 -0
- data/spec/utils_spec.rb +84 -0
- metadata +168 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class Lenjador
|
4
|
+
module Utils
|
5
|
+
DECIMAL_FRACTION_OF_SECOND = 3
|
6
|
+
|
7
|
+
# Build logstash json compatible event
|
8
|
+
#
|
9
|
+
# @param [Hash] metadata
|
10
|
+
# @param [#to_s] level
|
11
|
+
# @param [String] service_name
|
12
|
+
#
|
13
|
+
# @return [Hash]
|
14
|
+
def self.build_event(metadata, level, application_name)
|
15
|
+
overwritable_params
|
16
|
+
.merge(metadata)
|
17
|
+
.merge(
|
18
|
+
application: application_name,
|
19
|
+
level: level
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return application name
|
24
|
+
#
|
25
|
+
# Returns lower snake case application name. This allows the
|
26
|
+
# application value to be used in the elasticsearch index name.
|
27
|
+
#
|
28
|
+
# @param [String] service_name
|
29
|
+
#
|
30
|
+
# @return [String]
|
31
|
+
def self.application_name(service_name)
|
32
|
+
underscore(service_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.overwritable_params
|
36
|
+
{
|
37
|
+
:@timestamp => Time.now.utc.iso8601(DECIMAL_FRACTION_OF_SECOND)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.serialize_time_objects!(object)
|
42
|
+
if object.is_a?(Hash)
|
43
|
+
object.each do |key, value|
|
44
|
+
object[key] = serialize_time_objects!(value)
|
45
|
+
end
|
46
|
+
elsif object.is_a?(Array)
|
47
|
+
object.each_index do |index|
|
48
|
+
object[index] = serialize_time_objects!(object[index])
|
49
|
+
end
|
50
|
+
elsif object.is_a?(Time) || object.is_a?(Date)
|
51
|
+
object.iso8601
|
52
|
+
else
|
53
|
+
object
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if RUBY_PLATFORM =~ /java/
|
58
|
+
require 'jrjackson'
|
59
|
+
|
60
|
+
DUMP_OPTIONS = {
|
61
|
+
timezone: 'utc',
|
62
|
+
date_format: "YYYY-MM-dd'T'HH:mm:ss.SSSX"
|
63
|
+
}.freeze
|
64
|
+
|
65
|
+
def self.generate_json(obj)
|
66
|
+
JrJackson::Json.dump(obj, DUMP_OPTIONS)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
require 'oj'
|
70
|
+
DUMP_OPTIONS = { mode: :compat, time_format: :ruby }.freeze
|
71
|
+
|
72
|
+
def self.generate_json(obj)
|
73
|
+
serialize_time_objects!(obj)
|
74
|
+
|
75
|
+
Oj.dump(obj, DUMP_OPTIONS)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.underscore(input)
|
80
|
+
word = input.to_s.dup
|
81
|
+
word.gsub!(/::/, '/')
|
82
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
83
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
84
|
+
word.tr!("-", "_")
|
85
|
+
word.downcase!
|
86
|
+
word
|
87
|
+
end
|
88
|
+
|
89
|
+
private_class_method :overwritable_params
|
90
|
+
end
|
91
|
+
end
|
data/lib/lenjador.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require_relative 'lenjador/adapters'
|
3
|
+
require_relative 'lenjador/utils'
|
4
|
+
require_relative 'lenjador/null_logger'
|
5
|
+
require_relative 'lenjador/preprocessors'
|
6
|
+
|
7
|
+
LOG_LEVEL_QUERY_METHODS = [:debug?, :info?, :warn?, :error?, :fatal?]
|
8
|
+
|
9
|
+
class Lenjador
|
10
|
+
def self.build(service_name, loggers_config, preprocessors_config = {})
|
11
|
+
loggers_config ||= {stdout: nil}
|
12
|
+
preprocessors = preprocessors_config.map do |type, arguments|
|
13
|
+
Preprocessors.get(type.to_s, arguments || {})
|
14
|
+
end
|
15
|
+
adapters = loggers_config.map do |type, arguments|
|
16
|
+
Adapters.get(type.to_s, service_name, arguments || {})
|
17
|
+
end
|
18
|
+
new(adapters, preprocessors)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(adapters, preprocessors)
|
22
|
+
@adapters = adapters
|
23
|
+
@preprocessors = preprocessors
|
24
|
+
end
|
25
|
+
|
26
|
+
def debug(*args, &block)
|
27
|
+
log :debug, *args, &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def info(*args, &block)
|
31
|
+
log :info, *args, &block
|
32
|
+
end
|
33
|
+
|
34
|
+
def warn(*args, &block)
|
35
|
+
log :warn, *args, &block
|
36
|
+
end
|
37
|
+
|
38
|
+
def error(*args, &block)
|
39
|
+
log :error, *args, &block
|
40
|
+
end
|
41
|
+
|
42
|
+
def fatal(*args, &block)
|
43
|
+
log :fatal, *args, &block
|
44
|
+
end
|
45
|
+
|
46
|
+
LOG_LEVEL_QUERY_METHODS.each do |method|
|
47
|
+
define_method(method) do
|
48
|
+
@adapters.any? {|adapter| adapter.public_send(method) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def log(level, *args, &block)
|
55
|
+
data = parse_log_data(*args, &block)
|
56
|
+
processed_data = preprocess(data)
|
57
|
+
|
58
|
+
@adapters.each do |adapter|
|
59
|
+
adapter.log(level, processed_data)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def preprocess(data)
|
64
|
+
@preprocessors.inject(data) do |data_to_process, preprocessor|
|
65
|
+
preprocessor.process(data_to_process)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_log_data(message = nil, metadata = {}, &block)
|
70
|
+
return message if message.is_a?(Hash)
|
71
|
+
|
72
|
+
(metadata || {}).merge(message: block ? block.call : message)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../lib/lenjador/adapters/stdout_adapter'
|
3
|
+
|
4
|
+
describe Lenjador::Adapters::StdoutAdapter do
|
5
|
+
it 'creates a stdout logger' do
|
6
|
+
io_logger = described_class.new(0)
|
7
|
+
|
8
|
+
logger = io_logger.instance_variable_get(:@logger)
|
9
|
+
expect(logger).to be_a Logger
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#log' do
|
13
|
+
let(:adapter) { described_class.new(0) }
|
14
|
+
let(:logger) { adapter.logger }
|
15
|
+
|
16
|
+
context 'with only a message' do
|
17
|
+
it 'stringifies it correctly' do
|
18
|
+
expect(logger).to receive(:info).with('test')
|
19
|
+
|
20
|
+
adapter.log :info, message: 'test'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with an empty message' do
|
25
|
+
it 'stringifies it correctly' do
|
26
|
+
expect(logger).to receive(:info).with(' {"a":"b"}')
|
27
|
+
|
28
|
+
adapter.log :info, message: '', a: 'b'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'with no message' do
|
33
|
+
it 'stringifies it correctly' do
|
34
|
+
expect(logger).to receive(:info).with('{"a":"b"}')
|
35
|
+
|
36
|
+
adapter.log :info, a: 'b'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with a message and metadata' do
|
41
|
+
it 'stringifies it correctly' do
|
42
|
+
expect(logger).to receive(:info).with('test {"a":"b"}')
|
43
|
+
|
44
|
+
adapter.log :info, message: 'test', a: 'b'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
require_relative '../../lib/lenjador/adapters/stdout_json_adapter'
|
4
|
+
|
5
|
+
describe Lenjador::Adapters::StdoutJsonAdapter do
|
6
|
+
let(:debug_level_code) { 0 }
|
7
|
+
let(:debug_level) { Lenjador::Adapters::LOG_LEVELS[debug_level_code] }
|
8
|
+
let(:info_level_code) { 1 }
|
9
|
+
let(:info_level) { Lenjador::Adapters::LOG_LEVELS[info_level_code] }
|
10
|
+
|
11
|
+
let(:stdout) { StringIO.new }
|
12
|
+
|
13
|
+
around do |example|
|
14
|
+
old_stdout = $stdout
|
15
|
+
$stdout = stdout
|
16
|
+
|
17
|
+
begin
|
18
|
+
example.call
|
19
|
+
ensure
|
20
|
+
$stdout = old_stdout
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#log' do
|
25
|
+
context 'when below threshold' do
|
26
|
+
let(:adapter) { described_class.new(debug_level_code, service_name) }
|
27
|
+
let(:metadata) { {x: 'y'} }
|
28
|
+
let(:event) { {a: 'b', x: 'y'} }
|
29
|
+
let(:serialized_event) { JSON.dump(event) }
|
30
|
+
let(:service_name) { 'my-service' }
|
31
|
+
let(:application_name) { 'my_service' }
|
32
|
+
|
33
|
+
before do
|
34
|
+
allow(Lenjador::Utils).to receive(:build_event)
|
35
|
+
.with(metadata, info_level, application_name)
|
36
|
+
.and_return(event)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'sends serialized event to $stdout' do
|
40
|
+
adapter.log(info_level, metadata)
|
41
|
+
expect(output).to eq serialized_event + "\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when above threshold' do
|
46
|
+
let(:adapter) { described_class.new(info_level_code, service_name) }
|
47
|
+
let(:metadata) { {x: 'y'} }
|
48
|
+
let(:service_name) { 'my-service' }
|
49
|
+
|
50
|
+
it 'does not log the event' do
|
51
|
+
adapter.log(debug_level, metadata)
|
52
|
+
expect(output).to be_empty
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def output
|
60
|
+
stdout.string
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lenjador do
|
4
|
+
describe '.build' do
|
5
|
+
it 'creates stdout logger' do
|
6
|
+
expect(described_class).to receive(:new) do |adapters|
|
7
|
+
expect(adapters.count).to be(1)
|
8
|
+
expect(adapters.first).to be_a(described_class::Adapters::StdoutAdapter)
|
9
|
+
end
|
10
|
+
|
11
|
+
described_class.build('test_service', stdout: nil)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'creates stdout json logger' do
|
15
|
+
expect(described_class).to receive(:new) do |adapters|
|
16
|
+
expect(adapters.count).to be(1)
|
17
|
+
expect(adapters.first).to be_a(described_class::Adapters::StdoutJsonAdapter)
|
18
|
+
end
|
19
|
+
|
20
|
+
described_class.build('test_service', stdout: {json: true})
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'creates stdout logger when no loggers are specified' do
|
24
|
+
expect(described_class).to receive(:new) do |adapters|
|
25
|
+
expect(adapters.count).to be(1)
|
26
|
+
expect(adapters.first).to be_a(described_class::Adapters::StdoutAdapter)
|
27
|
+
end
|
28
|
+
|
29
|
+
described_class.build('test_service', nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'creates preprocessor when preprocessor defined' do
|
33
|
+
expect(described_class).to receive(:new) do |adapters, preprocessors|
|
34
|
+
expect(preprocessors.count).to be(1)
|
35
|
+
expect(preprocessors.first).to be_a(described_class::Preprocessors::Blacklist)
|
36
|
+
end
|
37
|
+
|
38
|
+
preprocessors = {blacklist: {fields: []}}
|
39
|
+
described_class.build('test_service', nil, preprocessors)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when preprocessor defined' do
|
44
|
+
let(:lenjador) { described_class.new([adapter], [preprocessor]) }
|
45
|
+
let(:adapter) { double }
|
46
|
+
let(:preprocessor) { double }
|
47
|
+
let(:data) { {data: 'data'} }
|
48
|
+
|
49
|
+
it 'preprocesses data before logging' do
|
50
|
+
expect(preprocessor).to receive(:process).with(data).and_return(data.merge(processed: true)).ordered
|
51
|
+
expect(adapter).to receive(:log).with(:info, data.merge(processed: true)).ordered
|
52
|
+
|
53
|
+
lenjador.info(data)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when parsing log data' do
|
58
|
+
let(:lenjador) { described_class.new([adapter], preprocessors) }
|
59
|
+
let(:adapter) { double }
|
60
|
+
let(:preprocessors) { [] }
|
61
|
+
|
62
|
+
it 'parses empty string with nil metadata' do
|
63
|
+
expect(adapter).to receive(:log).with(:info, message: '')
|
64
|
+
|
65
|
+
lenjador.info('', nil)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'parses nil as metadata' do
|
69
|
+
expect(adapter).to receive(:log).with(:info, message: nil)
|
70
|
+
|
71
|
+
lenjador.info(nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'parses only message' do
|
75
|
+
expect(adapter).to receive(:log).with(:info, message: 'test message')
|
76
|
+
|
77
|
+
lenjador.info 'test message'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'parses only metadata' do
|
81
|
+
expect(adapter).to receive(:log).with(:info, test: 'data')
|
82
|
+
|
83
|
+
lenjador.info test: 'data'
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'parses message and metadata' do
|
87
|
+
expect(adapter).to receive(:log).with(:info, message: 'test message', test: 'data')
|
88
|
+
|
89
|
+
lenjador.info 'test message', test: 'data'
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'parses block as a message' do
|
93
|
+
message = 'test message'
|
94
|
+
expect(adapter).to receive(:log).with(:info, message: message)
|
95
|
+
|
96
|
+
lenjador.info { message }
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'ignores progname on block syntax' do
|
100
|
+
message = 'test message'
|
101
|
+
expect(adapter).to receive(:log).with(:info, message: message)
|
102
|
+
|
103
|
+
lenjador.info('progname') { message }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'log level queries' do
|
108
|
+
context 'when adapter has debug level' do
|
109
|
+
let(:logger) do
|
110
|
+
described_class.build('test_service', stdout: {level: 'debug'})
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'responds true to debug? and higher levels' do
|
114
|
+
expect(logger.debug?).to be(true)
|
115
|
+
expect(logger.info?).to be(true)
|
116
|
+
expect(logger.warn?).to be(true)
|
117
|
+
expect(logger.error?).to be(true)
|
118
|
+
expect(logger.fatal?).to be(true)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when adapter has info level' do
|
123
|
+
let(:logger) do
|
124
|
+
described_class.build('test_service', stdout: {level: 'info'})
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'responds true to info? and higher levels' do
|
128
|
+
expect(logger.debug?).to be(false)
|
129
|
+
expect(logger.info?).to be(true)
|
130
|
+
expect(logger.warn?).to be(true)
|
131
|
+
expect(logger.error?).to be(true)
|
132
|
+
expect(logger.fatal?).to be(true)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'has the same interface as Ruby logger' do
|
138
|
+
skip "https://salemove.atlassian.net/browse/INF-464"
|
139
|
+
logger = described_class.build('test_service', stdout: {level: 'debug'})
|
140
|
+
expect(logger).to implement_interface(Logger)
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../lib/lenjador/preprocessors/blacklist'
|
3
|
+
|
4
|
+
describe Lenjador::Preprocessors::Blacklist do
|
5
|
+
subject(:processed_data) { described_class.new(config).process(data) }
|
6
|
+
|
7
|
+
let(:config) {{
|
8
|
+
fields: [{ key: 'field', action: action }]
|
9
|
+
}}
|
10
|
+
|
11
|
+
let(:action) {}
|
12
|
+
let(:data) {{
|
13
|
+
field: value,
|
14
|
+
data: {
|
15
|
+
field: 'secret'
|
16
|
+
},
|
17
|
+
array: [{field: 'secret'}]
|
18
|
+
}}
|
19
|
+
|
20
|
+
let(:value) { 'secret' }
|
21
|
+
|
22
|
+
context 'when action is unsupported' do
|
23
|
+
let(:action) { 'reverse' }
|
24
|
+
|
25
|
+
it 'throws exception' do
|
26
|
+
expect { processed_data }.to raise_exception(Lenjador::Preprocessors::Blacklist::UnsupportedActionException)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when action is "exclude"' do
|
31
|
+
let(:action) { 'exclude' }
|
32
|
+
|
33
|
+
it 'removes the field' do
|
34
|
+
expect(processed_data).not_to include(:field)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'removes nested field' do
|
38
|
+
expect(processed_data).not_to include_at_depth({field: 'secret'}, 1)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'removes nested in array field' do
|
42
|
+
expect(processed_data[:array]).not_to include({field: 'secret'})
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when field is deeply nested' do
|
46
|
+
let(:depth) { 10 }
|
47
|
+
let(:data) { data_with_nested_field({field: 'secret'}, depth) }
|
48
|
+
|
49
|
+
it 'removes deeply nested field' do
|
50
|
+
expect(processed_data).not_to include_at_depth({field: 'secret'}, depth)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when action is "mask"' do
|
56
|
+
let(:action) { 'mask' }
|
57
|
+
|
58
|
+
it 'masks nested field' do
|
59
|
+
expect(processed_data).to include_at_depth({field: '*****'}, 1)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'masks nested in array field' do
|
63
|
+
expect(processed_data[:array]).to include({field: '*****'})
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when field is string' do
|
67
|
+
let(:value) { 'secret' }
|
68
|
+
|
69
|
+
it 'masks value with asterisks' do
|
70
|
+
expect(processed_data).to include(field: '*****')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when field is number' do
|
75
|
+
let(:value) { 42 }
|
76
|
+
|
77
|
+
it 'masks number value' do
|
78
|
+
expect(processed_data).to include(field: '*****')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when field is boolean' do
|
83
|
+
let(:value) { true }
|
84
|
+
|
85
|
+
it 'masks value with asterisks' do
|
86
|
+
expect(processed_data).to include(field: '*****')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'when field is array' do
|
91
|
+
let(:value) { [1,2,3,4] }
|
92
|
+
|
93
|
+
it 'masks value with asterisks' do
|
94
|
+
expect(processed_data).to include(field: '*****')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when field is hash' do
|
99
|
+
let(:value) { {data: {}} }
|
100
|
+
|
101
|
+
it 'masks value with asterisks' do
|
102
|
+
expect(processed_data).to include(field: '*****')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when field is deeply nested' do
|
107
|
+
let(:depth) { 10 }
|
108
|
+
let(:data) { data_with_nested_field({field: 'secret'}, depth) }
|
109
|
+
|
110
|
+
it 'masks deeply nested field' do
|
111
|
+
expect(processed_data).to include_at_depth({field: '*****'}, depth)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def data_with_nested_field(field, depth)
|
117
|
+
depth.times.inject(field) do |mem|
|
118
|
+
{}.merge(data: mem)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
RSpec::Matchers.define :include_at_depth do |expected_hash, depth|
|
123
|
+
match do |actual|
|
124
|
+
nested_data = depth.times.inject(actual) do |mem|
|
125
|
+
mem[:data]
|
126
|
+
end
|
127
|
+
|
128
|
+
expect(nested_data).to include(expected_hash)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lenjador/preprocessors/json_pointer_trie'
|
3
|
+
|
4
|
+
RSpec.describe Lenjador::Preprocessors::JSONPointerTrie do
|
5
|
+
let(:trie) { described_class.new }
|
6
|
+
|
7
|
+
describe '#includes?' do
|
8
|
+
it 'returns true for empty prefix' do
|
9
|
+
expect(trie).to include('')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns true if trie contains requested prefix or value itself' do
|
13
|
+
trie.insert('/data/nested/key')
|
14
|
+
|
15
|
+
expect(trie).to include('/data')
|
16
|
+
expect(trie).to include('/data/nested')
|
17
|
+
expect(trie).to include('/data/nested/key')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns false if trie does not contain requested prefix or value' do
|
21
|
+
trie.insert('/data/nested/key')
|
22
|
+
|
23
|
+
expect(trie).to_not include('/bad_data')
|
24
|
+
expect(trie).to_not include('/data/bad_nested')
|
25
|
+
expect(trie).to_not include('/data/nested/bad_key')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns true if trie contains requested prefix under wildcard' do
|
29
|
+
trie.insert('/data/~/key')
|
30
|
+
|
31
|
+
expect(trie).to include('/data/arbitrary_key/key')
|
32
|
+
expect(trie).to include('/data/another_key/key')
|
33
|
+
expect(trie).to_not include('/data/arbitrary_key/bad_nested_key')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|