attr_masker 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a6c5ae88c368437043840351487b67c3d5a4e9a6
4
- data.tar.gz: fca5c1719634945ba02cf3bbc5ba02ee3418816f
3
+ metadata.gz: 43cbbd6cf1150e2228d2b5fc1323bbdbae852510
4
+ data.tar.gz: 614372bc5a85ac8b23112062b437a8d468064160
5
5
  SHA512:
6
- metadata.gz: 659120184ebe3048eda7bfbf459f394a167bdc4902eb1462f26d42918979a0a5b7174807145fd42a1bb0b3a32d51a68c5cd860e27b3fe4ede6fc8e7497fa36a1
7
- data.tar.gz: 1c8f330415a6347dc6a7c0f719666f8dceca8d64cb139a2aef01c899fb9ae7602029a7f0e70677835cacf3117852ecba4be693170bb40c0cab375628542d1074
6
+ metadata.gz: cb815eda36c7a99d2cfebf18df5a1b901a5e597df340b6f2041bd9bdb6ee722b4fac5b21dde71457ad63794de5cc3e1aec2659c02d32c2aea6e0bce7dfff2b8c
7
+ data.tar.gz: 6e97866032f1d8c1323e11bb160f62d9798f001b2736b98be03907f9269f39ff462be45633ac3c59c04856621196fc8979556c1438cac3b6be5eff68f1f6c238
data/CHANGELOG.adoc ADDED
@@ -0,0 +1,15 @@
1
+ == 0.1.1
2
+
3
+ * Mask records disregarding default scope
4
+ (https://github.com/riboseinc/attr_masker/pull/41[#41])
5
+ * Major refactoring (extracting `Attribute` and `Model` classes)
6
+ * Code style improvements (nearly all violations fixed)
7
+
8
+ == 0.1
9
+
10
+ * First useful version
11
+ * Rails 4 & 5 compatibility
12
+ * Callable objects as maskers
13
+ * Nice progress bar
14
+ * Built-in maskers: `AttrMasker::Maskers::Replacing`
15
+ and `AttrMasker::Maskers::SIMPLE`
data/README.adoc CHANGED
@@ -3,6 +3,7 @@
3
3
  :pygments-style: native
4
4
  :pygments-linenums-mode: inline
5
5
 
6
+ image:https://img.shields.io/gem/v/attr_masker.svg["Gem Version", link="https://rubygems.org/gems/attr_masker"]
6
7
  image:https://img.shields.io/travis/riboseinc/attr_masker/master.svg["Build Status", link="https://travis-ci.org/riboseinc/attr_masker"]
7
8
 
8
9
  Mask ActiveRecord data with ease!
@@ -68,6 +69,11 @@ attr_masker :email :unless => ->(record) { ! record.tester_user? }
68
69
  attr_masker :first_name, :if => :tester_user?
69
70
  ----
70
71
 
72
+ The ActiveRecord's `::default_scope` method has no effect on masking. All
73
+ table records are updated, provided that :if and :unless filters allow that.
74
+ For example, if you're using a Paranoia[https://github.com/rubysherpas/paranoia]
75
+ gem to soft-delete your data, records marked as deleted will be masked as well.
76
+
71
77
  === Using custom maskers
72
78
 
73
79
  By default, data is maksed with `AttrMasker::Maskers::SIMPLE` masker which
data/Rakefile CHANGED
@@ -1,32 +1,5 @@
1
1
  # (c) 2017 Ribose Inc.
2
2
  #
3
3
 
4
- # require 'rake'
5
- # require 'rake/testtask'
6
- # require 'rake/rdoctask'
7
-
8
4
  require "bundler/gem_tasks"
9
5
  load "tasks/db.rake"
10
-
11
- # desc 'Generate documentation for the attr_masker gem.'
12
- # Rake::RDocTask.new(:rdoc) do |rdoc|
13
- # rdoc.rdoc_dir = 'rdoc'
14
- # rdoc.title = 'attr_masker'
15
- # rdoc.options << '--line-numbers' << '--inline-source'
16
- # rdoc.rdoc_files.include('README*')
17
- # rdoc.rdoc_files.include('lib/**/*.rb')
18
- # end
19
- #
20
- # if RUBY_VERSION < '1.9.3'
21
- # require 'rcov/rcovtask'
22
- #
23
- # task :rcov do
24
- # system "rcov -o coverage/rcov --exclude '^(?!lib)' " + FileList[ 'test/**/*_test.rb' ].join(' ')
25
- # end
26
- #
27
- # desc 'Default: run unit tests under rcov.'
28
- # task :default => :rcov
29
- # else
30
- # desc 'Default: run unit tests.'
31
- # task :default => :test
32
- # end
data/attr_masker.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |gem|
17
17
  "of certain models by modifying the database."
18
18
 
19
19
  gem.files = `git ls-files`.split($/)
20
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
21
21
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
22
  gem.require_paths = ["lib"]
23
23
 
data/lib/attr_masker.rb CHANGED
@@ -4,6 +4,8 @@
4
4
  # Adds attr_accessors that mask an object's attributes
5
5
  module AttrMasker
6
6
  autoload :Version, "attr_masker/version"
7
+ autoload :Attribute, "attr_masker/attribute"
8
+ autoload :Model, "attr_masker/model"
7
9
 
8
10
  autoload :Error, "attr_masker/error"
9
11
  autoload :Performer, "attr_masker/performer"
@@ -14,214 +16,6 @@ module AttrMasker
14
16
  end
15
17
 
16
18
  require "attr_masker/railtie" if defined?(Rails)
17
- def self.extended(base) # :nodoc:
18
- base.class_eval do
19
-
20
- # Only include the dangerous instance methods during the Rake task!
21
- include InstanceMethods
22
- attr_writer :attr_masker_options
23
- @attr_masker_options, @masker_attributes = {}, {}
24
- end
25
- end
26
-
27
- # Generates attr_accessors that mask attributes transparently
28
- #
29
- # Options (any other options you specify are passed to the masker's mask
30
- # methods)
31
- #
32
- # :marshal => If set to true, attributes will be marshaled as well as masker. This is useful if you're planning
33
- # on masking something other than a string. Defaults to false unless you're using it with ActiveRecord
34
- # or DataMapper.
35
- #
36
- # :marshaler => The object to use for marshaling. Defaults to Marshal.
37
- #
38
- # :dump_method => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.
39
- #
40
- # :load_method => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.
41
- #
42
- # :masker => The object to use for masking. It must respond to +#mask+. Defaults to AttrMasker::Maskers::Simple.
43
- #
44
- # :if => Attributes are only masker if this option evaluates to true. If you pass a symbol representing an instance
45
- # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
46
- # Defaults to true.
47
- #
48
- # :unless => Attributes are only masker if this option evaluates to false. If you pass a symbol representing an instance
49
- # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
50
- # Defaults to false.
51
- #
52
- # You can specify your own default options
53
- #
54
- # class User
55
- # # now all attributes will be encoded and marshaled by default
56
- # attr_masker_options.merge!(:marshal => true, :some_other_option => true)
57
- # attr_masker :configuration
58
- # end
59
- #
60
- #
61
- # Example
62
- #
63
- # class User
64
- # attr_masker :email, :credit_card
65
- # attr_masker :configuration, :marshal => true
66
- # end
67
- #
68
- # @user = User.new
69
- # @user.masker_email # nil
70
- # @user.email? # false
71
- # @user.email = 'test@example.com'
72
- # @user.email? # true
73
- # @user.masker_email # returns the masker version of 'test@example.com'
74
- #
75
- # @user.configuration = { :time_zone => 'UTC' }
76
- # @user.masker_configuration # returns the masker version of configuration
77
- #
78
- # See README for more examples
79
- def attr_masker(*attributes)
80
- options = {
81
- :if => true,
82
- :unless => false,
83
- :column_name => nil,
84
- :marshal => false,
85
- :marshaler => Marshal,
86
- :dump_method => "dump",
87
- :load_method => "load",
88
- :masker => AttrMasker::Maskers::SIMPLE,
89
- }.merge!(attr_masker_options).merge!(attributes.last.is_a?(Hash) ? attributes.pop : {})
90
-
91
- attributes.each do |attribute|
92
- masker_attributes[attribute.to_sym] = options.merge(attribute: attribute.to_sym)
93
- end
94
- end
95
-
96
- # Default options to use with calls to <tt>attr_masker</tt>
97
- # XXX:Keep
98
- #
99
- # It will inherit existing options from its superclass
100
- def attr_masker_options
101
- @attr_masker_options ||= superclass.attr_masker_options.dup
102
- end
103
-
104
- # Checks if an attribute is configured with <tt>attr_masker</tt>
105
- # XXX:Keep
106
- #
107
- # Example
108
- #
109
- # class User
110
- # attr_accessor :name
111
- # attr_masker :email
112
- # end
113
- #
114
- # User.attr_masker?(:name) # false
115
- # User.attr_masker?(:email) # true
116
- def attr_masker?(attribute)
117
- masker_attributes.has_key?(attribute.to_sym)
118
- end
119
-
120
- # masks a value for the attribute specified
121
- # XXX:modify
122
- #
123
- # Example
124
- #
125
- # class User
126
- # attr_masker :email
127
- # end
128
- #
129
- # masker_email = User.mask(:email, 'test@example.com')
130
- def mask(attribute, value, options = {})
131
- options = masker_attributes[attribute.to_sym].merge(options)
132
- # if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
133
- if options[:if] && !options[:unless]
134
- value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
135
- masker_value = options[:masker].call(options.merge!(value: value))
136
- masker_value
137
- else
138
- value
139
- end
140
- end
141
-
142
- # Contains a hash of masker attributes with virtual attribute names as keys
143
- # and their corresponding options as values
144
- # XXX:Keep
145
- #
146
- # Example
147
- #
148
- # class User
149
- # attr_masker :email
150
- # end
151
- #
152
- # User.masker_attributes # { :email => { :attribute => 'masker_email' } }
153
- def masker_attributes
154
- @masker_attributes ||= superclass.masker_attributes.dup
155
- end
156
-
157
- # Forwards calls to :mask_#{attribute} to the corresponding mask method
158
- # if attribute was configured with attr_masker
159
- #
160
- # Example
161
- #
162
- # class User
163
- # attr_masker :email
164
- # end
165
- #
166
- # User.mask_email('SOME_masker_EMAIL_STRING')
167
- def method_missing(method, *arguments, &block)
168
- if method.to_s =~ /^mask_(.+)$/ && attr_masker?($1)
169
- send(:mask, $1, *arguments)
170
- else
171
- super
172
- end
173
- end
174
-
175
- module InstanceMethods
176
-
177
- # masks a value for the attribute specified using options evaluated in the current object's scope
178
- #
179
- # Example
180
- #
181
- # class User
182
- # attr_accessor :secret_key
183
- # attr_masker :email
184
- #
185
- # def initialize(secret_key)
186
- # self.secret_key = secret_key
187
- # end
188
- # end
189
- #
190
- # @user = User.new('some-secret-key')
191
- # @user.mask(:email, 'test@example.com')
192
- def mask(attribute, value=nil)
193
- value = self.send(attribute) if value.nil?
194
- self.class.mask(attribute, value, evaluated_attr_masker_options_for(attribute))
195
- end
196
-
197
- protected
198
-
199
- # Returns attr_masker options evaluated in the current object's scope for the attribute specified
200
- # XXX:Keep
201
- def evaluated_attr_masker_options_for(attribute)
202
- self.class.masker_attributes[attribute.to_sym].inject({}) do |hash, (option, value)|
203
- if %i[if unless].include?(option)
204
- hash.merge!(option => evaluate_attr_masker_option(value))
205
- else
206
- hash.merge!(option => value)
207
- end
208
- end
209
- end
210
-
211
- # Evaluates symbol (method reference) or proc (responds to call) options
212
- # XXX:Keep
213
- #
214
- # If the option is not a symbol or proc then the original option is returned
215
- def evaluate_attr_masker_option(option)
216
- if option.is_a?(Symbol) && respond_to?(option)
217
- send(option)
218
- elsif option.respond_to?(:call)
219
- option.call(self)
220
- else
221
- option
222
- end
223
- end
224
- end
225
19
  end
226
20
 
227
- Object.extend AttrMasker
21
+ Object.extend AttrMasker::Model
@@ -0,0 +1,70 @@
1
+ # (c) 2017 Ribose Inc.
2
+ #
3
+
4
+ module AttrMasker
5
+ # Holds the definition of maskable attribute.
6
+ class Attribute
7
+ attr_reader :name, :model, :options
8
+
9
+ def initialize(name, model, options)
10
+ @name = name.to_sym
11
+ @model = model
12
+ @options = options
13
+ end
14
+
15
+ # Evaluates the +:if+ and +:unless+ attribute options on given instance.
16
+ # Returns +true+ or +fasle+, depending on whether the attribute should be
17
+ # masked for this object or not.
18
+ def should_mask?(model_instance)
19
+ not (
20
+ options.key?(:if) && !evaluate_option(:if, model_instance) ||
21
+ options.key?(:unless) && evaluate_option(:unless, model_instance)
22
+ )
23
+ end
24
+
25
+ # Mask the attribute on given model. Masking will be performed regardless
26
+ # of +:if+ and +:unless+ options. A +should_mask?+ method should be called
27
+ # separately to ensure that given object is eligible for masking.
28
+ #
29
+ # The method returns the masked value but does not modify the object's
30
+ # attribute.
31
+ #
32
+ # If +marshal+ attribute's option is +true+, the attribute value will be
33
+ # loaded before masking, and dumped to proper storage format prior
34
+ # returning.
35
+ def mask(model_instance)
36
+ value = unmarshal_data(model_instance.send(name))
37
+ masker_value = options[:masker].call(options.merge!(value: value))
38
+ marshal_data(masker_value)
39
+ end
40
+
41
+ # Evaluates option (typically +:if+ or +:unless+) on given model instance.
42
+ # That option can be either a proc (a model is passed as an only argument),
43
+ # or a symbol (a method of that name is called on model instance).
44
+ def evaluate_option(option_name, model_instance)
45
+ option = options[option_name]
46
+
47
+ if option.is_a?(Symbol)
48
+ model_instance.send(option)
49
+ elsif option.respond_to?(:call)
50
+ option.call(model_instance)
51
+ else
52
+ option
53
+ end
54
+ end
55
+
56
+ def marshal_data(data)
57
+ return data unless options[:marshal]
58
+ options[:marshaler].send(options[:dump_method], data)
59
+ end
60
+
61
+ def unmarshal_data(data)
62
+ return data unless options[:marshal]
63
+ options[:marshaler].send(options[:load_method], data)
64
+ end
65
+
66
+ def column_name
67
+ options[:column_name] || name
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,137 @@
1
+ module AttrMasker
2
+ module Model
3
+ def self.extended(base) # :nodoc:
4
+ base.class_eval do
5
+ attr_writer :attr_masker_options
6
+ @attr_masker_options = {}
7
+ @masker_attributes = {}
8
+ end
9
+ end
10
+
11
+ # Generates attr_accessors that mask attributes transparently
12
+ #
13
+ # Options (any other options you specify are passed to the masker's mask
14
+ # methods)
15
+ #
16
+ # [:masker]
17
+ # The object to use for masking. It must respond to +#mask+. Defaults to
18
+ # AttrMasker::Maskers::Simple.
19
+ #
20
+ # [:if]
21
+ # Attributes are only masker if this option evaluates to true. If you
22
+ # pass a symbol representing an instance method then the result of
23
+ # the method will be evaluated. Any objects that respond to
24
+ # <tt>:call</tt> are evaluated as well. Defaults to true.
25
+ #
26
+ # [:unless]
27
+ # Attributes are only masker if this option evaluates to false. If you
28
+ # pass a symbol representing an instance method then the result of
29
+ # the method will be evaluated. Any objects that respond to
30
+ # <tt>:call</tt> are evaluated as well. Defaults to false.
31
+ #
32
+ # [:marshal]
33
+ # If set to true, attributes will be marshaled as well as masker. This
34
+ # is useful if you're planning on masking something other than a string.
35
+ # Defaults to false unless you're using it with ActiveRecord or
36
+ # DataMapper.
37
+ #
38
+ # [:marshaler]
39
+ # The object to use for marshaling. Defaults to Marshal.
40
+ #
41
+ # [:dump_method]
42
+ # The dump method name to call on the <tt>:marshaler</tt> object to.
43
+ # Defaults to 'dump'.
44
+ #
45
+ # [:load_method]
46
+ # The load method name to call on the <tt>:marshaler</tt> object.
47
+ # Defaults to 'load'.
48
+ #
49
+ # You can specify your own default options
50
+ #
51
+ # class User
52
+ # # now all attributes will be encoded and marshaled by default
53
+ # attr_masker_options.merge!(:marshal => true, :another_option => true)
54
+ # attr_masker :configuration
55
+ # end
56
+ #
57
+ #
58
+ # Example
59
+ #
60
+ # class User
61
+ # attr_masker :email, :credit_card
62
+ # attr_masker :configuration, :marshal => true
63
+ # end
64
+ #
65
+ # @user = User.new
66
+ # @user.masker_email # nil
67
+ # @user.email? # false
68
+ # @user.email = 'test@example.com'
69
+ # @user.email? # true
70
+ # @user.masker_email # returns the masker version of 'test@example.com'
71
+ #
72
+ # @user.configuration = { :time_zone => 'UTC' }
73
+ # @user.masker_configuration # returns the masker version of configuration
74
+ #
75
+ # See README for more examples
76
+ def attr_masker(*args)
77
+ default_options = {
78
+ if: true,
79
+ unless: false,
80
+ column_name: nil,
81
+ marshal: false,
82
+ marshaler: Marshal,
83
+ dump_method: "dump",
84
+ load_method: "load",
85
+ masker: AttrMasker::Maskers::SIMPLE,
86
+ }
87
+
88
+ options = args.extract_options!.
89
+ reverse_merge(attr_masker_options).
90
+ reverse_merge(default_options)
91
+
92
+ args.each do |attribute_name|
93
+ attribute = Attribute.new(attribute_name, self, options)
94
+ masker_attributes[attribute.name] = attribute
95
+ end
96
+ end
97
+
98
+ # Default options to use with calls to <tt>attr_masker</tt>
99
+ # XXX:Keep
100
+ #
101
+ # It will inherit existing options from its superclass
102
+ def attr_masker_options
103
+ @attr_masker_options ||= superclass.attr_masker_options.dup
104
+ end
105
+
106
+ # Checks if an attribute is configured with <tt>attr_masker</tt>
107
+ # XXX:Keep
108
+ #
109
+ # Example
110
+ #
111
+ # class User
112
+ # attr_accessor :name
113
+ # attr_masker :email
114
+ # end
115
+ #
116
+ # User.attr_masker?(:name) # false
117
+ # User.attr_masker?(:email) # true
118
+ def attr_masker?(attribute)
119
+ masker_attributes.has_key?(attribute.to_sym)
120
+ end
121
+
122
+ # Contains a hash of masker attributes with virtual attribute names as keys
123
+ # and their corresponding options as values
124
+ # XXX:Keep
125
+ #
126
+ # Example
127
+ #
128
+ # class User
129
+ # attr_masker :email
130
+ # end
131
+ #
132
+ # User.masker_attributes # { :email => { :attribute => 'masker_email' } }
133
+ def masker_attributes
134
+ @masker_attributes ||= superclass.masker_attributes.dup
135
+ end
136
+ end
137
+ end
@@ -24,7 +24,7 @@ module AttrMasker
24
24
 
25
25
  def mask_class(klass)
26
26
  progressbar_for_model(klass) do |bar|
27
- klass.all.each do |model|
27
+ klass.all.unscoped.each do |model|
28
28
  mask_object model
29
29
  bar.increment
30
30
  end
@@ -36,20 +36,21 @@ module AttrMasker
36
36
  def mask_object(instance)
37
37
  klass = instance.class
38
38
 
39
- updates = klass.masker_attributes.reduce({}) do |acc, masker_attr|
40
- attr_name = masker_attr[0]
41
- column_name = masker_attr[1][:column_name] || attr_name
42
- masker_value = instance.mask(attr_name)
39
+ updates = klass.masker_attributes.values.reduce({}) do |acc, attribute|
40
+ next acc unless attribute.should_mask?(instance)
41
+
42
+ column_name = attribute.column_name
43
+ masker_value = attribute.mask(instance)
43
44
  acc.merge!(column_name => masker_value)
44
45
  end
45
46
 
46
- klass.all.update(instance.id, updates)
47
+ klass.all.unscoped.update(instance.id, updates)
47
48
  end
48
49
 
49
50
  def progressbar_for_model(klass)
50
51
  bar = ProgressBar.create(
51
52
  title: klass.name,
52
- total: klass.count,
53
+ total: klass.unscoped.count,
53
54
  throttle_rate: 0.1,
54
55
  format: %q[%t %c/%C (%j%%) %B %E],
55
56
  )
@@ -6,7 +6,7 @@ module AttrMasker
6
6
  module Version
7
7
  MAJOR = 0
8
8
  MINOR = 1
9
- PATCH = 0
9
+ PATCH = 1
10
10
 
11
11
  # Returns a version string by joining <tt>MAJOR</tt>, <tt>MINOR</tt>, and
12
12
  # <tt>PATCH</tt> with <tt>'.'</tt>
@@ -3,6 +3,7 @@ ActiveRecord::Schema.define do
3
3
  t.string :first_name
4
4
  t.string :last_name
5
5
  t.string :email
6
+ t.text :avatar
6
7
  t.timestamps null: false
7
8
  end
8
9
  end
@@ -28,6 +28,7 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
28
28
  first_name: "Han",
29
29
  last_name: "Solo",
30
30
  email: "han@example.test",
31
+ avatar: Marshal.dump("Millenium Falcon photo"),
31
32
  )
32
33
  end
33
34
 
@@ -36,6 +37,7 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
36
37
  first_name: "Luke",
37
38
  last_name: "Skywalker",
38
39
  email: "luke@jedi.example.test",
40
+ avatar: Marshal.dump("photo with a light saber"),
39
41
  )
40
42
  end
41
43
 
@@ -180,6 +182,76 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
180
182
  )
181
183
  end
182
184
 
185
+ example "Masking a marshalled attribute" do
186
+ User.class_eval do
187
+ attr_masker :avatar, marshal: true
188
+ end
189
+
190
+ expect { run_rake_task }.not_to(change { User.count })
191
+
192
+ expect { han.reload }.to(
193
+ preserve { han.first_name } &
194
+ preserve { han.last_name } &
195
+ preserve { han.email } &
196
+ change { han.avatar }
197
+ )
198
+
199
+ expect(han.avatar).to eq(Marshal.dump("(redacted)"))
200
+
201
+ expect { luke.reload }.to(
202
+ preserve { luke.first_name } &
203
+ preserve { luke.last_name } &
204
+ preserve { luke.email } &
205
+ change { luke.avatar }
206
+ )
207
+
208
+ expect(luke.avatar).to eq(Marshal.dump("(redacted)"))
209
+ end
210
+
211
+ example "Masking a marshalled attribute with a custom marshaller" do
212
+ module CustomMarshal
213
+ module_function
214
+
215
+ def load_marshalled(*args)
216
+ Marshal.load(*args) # rubocop:disable Security/MarshalLoad
217
+ end
218
+
219
+ def dump_json(*args)
220
+ JSON.dump(json: args)
221
+ end
222
+ end
223
+
224
+ User.class_eval do
225
+ attr_masker(
226
+ :avatar,
227
+ marshal: true,
228
+ marshaler: CustomMarshal,
229
+ load_method: :load_marshalled,
230
+ dump_method: :dump_json,
231
+ )
232
+ end
233
+
234
+ expect { run_rake_task }.not_to(change { User.count })
235
+
236
+ expect { han.reload }.to(
237
+ preserve { han.first_name } &
238
+ preserve { han.last_name } &
239
+ preserve { han.email } &
240
+ change { han.avatar }
241
+ )
242
+
243
+ expect(han.avatar).to eq({ json: ["(redacted)"] }.to_json)
244
+
245
+ expect { luke.reload }.to(
246
+ preserve { luke.first_name } &
247
+ preserve { luke.last_name } &
248
+ preserve { luke.email } &
249
+ change { luke.avatar }
250
+ )
251
+
252
+ expect(luke.avatar).to eq({ json: ["(redacted)"] }.to_json)
253
+ end
254
+
183
255
  example "It is disabled in production environment" do
184
256
  allow(Rails).to receive(:env) { "production".inquiry }
185
257
 
@@ -197,6 +269,22 @@ RSpec.describe "Attr Masker gem", :suppress_progressbar do
197
269
  end
198
270
  end
199
271
 
272
+ example "It masks records disregarding default scope" do
273
+ User.class_eval do
274
+ attr_masker :last_name
275
+
276
+ default_scope ->() { where(last_name: "Solo") }
277
+ end
278
+
279
+ expect { run_rake_task }.not_to(change { User.unscoped.count })
280
+
281
+ [han, luke].each do |record|
282
+ expect { record.reload }.to(
283
+ change { record.last_name }.to("(redacted)")
284
+ )
285
+ end
286
+ end
287
+
200
288
  def run_rake_task
201
289
  Rake::Task["db:mask"].execute
202
290
  end
@@ -0,0 +1,128 @@
1
+ # (c) 2017 Ribose Inc.
2
+ #
3
+
4
+ require "spec_helper"
5
+
6
+ RSpec.describe AttrMasker::Attribute do
7
+ describe "::new" do
8
+ subject { described_class.method :new }
9
+
10
+ it "instantiates a new attribute definition" do
11
+ opts = { arbitrary: :options }
12
+ retval = subject.call(:some_attr, :some_model, opts)
13
+ expect(retval.name).to eq(:some_attr)
14
+ expect(retval.model).to eq(:some_model)
15
+ expect(retval.options).to eq(opts)
16
+ end
17
+ end
18
+
19
+ describe "#column_name" do
20
+ subject { receiver.method :column_name }
21
+ let(:receiver) { described_class.new :some_attr, :some_model, options }
22
+ let(:options) { {} }
23
+
24
+ it "defaults to attribute name" do
25
+ expect(subject.call).to eq(:some_attr)
26
+ end
27
+
28
+ it "can be overriden with :column_name option" do
29
+ options[:column_name] = :some_column
30
+ expect(subject.call).to eq(:some_column)
31
+ end
32
+ end
33
+
34
+ describe "#should_mask?" do
35
+ subject { described_class.instance_method :should_mask? }
36
+
37
+ let(:model_instance) { double }
38
+ let(:truthy) { double call: true }
39
+ let(:falsey) { double call: false }
40
+
41
+ example { expect(retval_for_opts({})).to be(true) }
42
+ example { expect(retval_for_opts(if: truthy)).to be(true) }
43
+ example { expect(retval_for_opts(if: falsey)).to be(false) }
44
+ example { expect(retval_for_opts(unless: truthy)).to be(false) }
45
+ example { expect(retval_for_opts(unless: falsey)).to be(true) }
46
+ example { expect(retval_for_opts(if: truthy, unless: truthy)).to be(false) }
47
+ example { expect(retval_for_opts(if: truthy, unless: falsey)).to be(true) }
48
+ example { expect(retval_for_opts(if: falsey, unless: truthy)).to be(false) }
49
+ example { expect(retval_for_opts(if: falsey, unless: falsey)).to be(false) }
50
+
51
+ def retval_for_opts(opts)
52
+ receiver = described_class.new(:some_attr, :some_model, opts)
53
+ callable = subject.bind(receiver)
54
+ callable.(model_instance)
55
+ end
56
+ end
57
+
58
+ describe "#evaluate_option" do
59
+ subject { receiver.method :evaluate_option }
60
+ let(:receiver) { described_class.new :some_attr, model_instance, options }
61
+ let(:options) { {} }
62
+ let(:model_instance) { double }
63
+ let(:retval) { subject.call(:option_name, model_instance) }
64
+
65
+ context "when that option value is a symbol" do
66
+ let(:options) { { option_name: :meth } }
67
+
68
+ before do
69
+ allow(model_instance).to receive(:meth).with(no_args).and_return(:rv)
70
+ end
71
+
72
+ it "evaluates an object's method pointed by that symbol" do
73
+ expect(retval).to be(:rv)
74
+ end
75
+ end
76
+
77
+ context "when that option_nameion value responds to #call" do
78
+ let(:options) { { option_name: callable } }
79
+ let(:callable) { double }
80
+
81
+ before do
82
+ allow(callable).to receive(:call).with(model_instance).and_return(:rv)
83
+ end
84
+
85
+ it "calls #call on it passing model instance as the only argument" do
86
+ expect(retval).to be(:rv)
87
+ end
88
+ end
89
+ end
90
+
91
+ describe "#marshal_data" do
92
+ subject { receiver.method :marshal_data }
93
+ let(:receiver) { described_class.new :some_attr, model_instance, options }
94
+ let(:options) { { marshaler: marshaller, dump_method: :dump_m } }
95
+ let(:marshaller) { double }
96
+ let(:model_instance) { double }
97
+
98
+ it "returns unmodified argument when marshal option is falsey" do
99
+ options[:marshal] = false
100
+ expect(subject.call(:data)).to be(:data)
101
+ end
102
+
103
+ it "returns unmodified argument when marshal option is falsey" do
104
+ options[:marshal] = true
105
+ expect(marshaller).to receive(:dump_m).with(:data).and_return(:retval)
106
+ expect(subject.call(:data)).to be(:retval)
107
+ end
108
+ end
109
+
110
+ describe "#unmarshal_data" do
111
+ subject { receiver.method :unmarshal_data }
112
+ let(:receiver) { described_class.new :some_attr, model_instance, options }
113
+ let(:options) { { marshaler: marshaller, load_method: :load_m } }
114
+ let(:marshaller) { double }
115
+ let(:model_instance) { double }
116
+
117
+ it "returns unmodified argument when marshal option is falsey" do
118
+ options[:marshal] = false
119
+ expect(subject.call(:data)).to be(:data)
120
+ end
121
+
122
+ it "returns unmodified argument when marshal option is falsey" do
123
+ options[:marshal] = true
124
+ expect(marshaller).to receive(:load_m).with(:data).and_return(:retval)
125
+ expect(subject.call(:data)).to be(:retval)
126
+ end
127
+ end
128
+ end
File without changes
File without changes
@@ -0,0 +1,12 @@
1
+ # (c) 2017 Ribose Inc.
2
+ #
3
+
4
+ require "spec_helper"
5
+
6
+ RSpec.describe AttrMasker::Model do
7
+ it "extends every class and provides class methods" do
8
+ c = Class.new
9
+ expect(c).to respond_to(:attr_masker)
10
+ expect(c.singleton_class.included_modules).to include(described_class)
11
+ end
12
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attr_masker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-02 00:00:00.000000000 Z
11
+ date: 2017-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -156,6 +156,7 @@ files:
156
156
  - ".rspec"
157
157
  - ".rubocop.yml"
158
158
  - ".travis.yml"
159
+ - CHANGELOG.adoc
159
160
  - Gemfile
160
161
  - LICENSE
161
162
  - README.adoc
@@ -169,9 +170,11 @@ files:
169
170
  - gemfiles/Rails-5.1.gemfile
170
171
  - gemfiles/Rails-head.gemfile
171
172
  - lib/attr_masker.rb
173
+ - lib/attr_masker/attribute.rb
172
174
  - lib/attr_masker/error.rb
173
175
  - lib/attr_masker/maskers/replacing.rb
174
176
  - lib/attr_masker/maskers/simple.rb
177
+ - lib/attr_masker/model.rb
175
178
  - lib/attr_masker/performer.rb
176
179
  - lib/attr_masker/railtie.rb
177
180
  - lib/attr_masker/version.rb
@@ -181,14 +184,16 @@ files:
181
184
  - spec/dummy/db/schema.rb
182
185
  - spec/dummy/public/favicon.ico
183
186
  - spec/features_spec.rb
184
- - spec/maskers/replacing_spec.rb
185
- - spec/maskers/simple_spec.rb
186
187
  - spec/spec_helper.rb
187
188
  - spec/support/0_combustion.rb
188
189
  - spec/support/db_cleaner.rb
189
190
  - spec/support/matchers.rb
190
191
  - spec/support/rake.rb
191
192
  - spec/support/silence_stdout.rb
193
+ - spec/unit/attribute_spec.rb
194
+ - spec/unit/maskers/replacing_spec.rb
195
+ - spec/unit/maskers/simple_spec.rb
196
+ - spec/unit/model_spec.rb
192
197
  homepage: https://github.com/riboseinc/attr_masker
193
198
  licenses:
194
199
  - MIT
@@ -219,11 +224,13 @@ test_files:
219
224
  - spec/dummy/db/schema.rb
220
225
  - spec/dummy/public/favicon.ico
221
226
  - spec/features_spec.rb
222
- - spec/maskers/replacing_spec.rb
223
- - spec/maskers/simple_spec.rb
224
227
  - spec/spec_helper.rb
225
228
  - spec/support/0_combustion.rb
226
229
  - spec/support/db_cleaner.rb
227
230
  - spec/support/matchers.rb
228
231
  - spec/support/rake.rb
229
232
  - spec/support/silence_stdout.rb
233
+ - spec/unit/attribute_spec.rb
234
+ - spec/unit/maskers/replacing_spec.rb
235
+ - spec/unit/maskers/simple_spec.rb
236
+ - spec/unit/model_spec.rb