attr_masker 0.1.0 → 0.1.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 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