radius-spec 0.3.0 → 0.7.0

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.
@@ -1,3 +1,5 @@
1
+ require: rubocop-rails
2
+
1
3
  inherit_mode:
2
4
  merge:
3
5
  - Exclude
@@ -5,10 +7,6 @@ inherit_mode:
5
7
 
6
8
  inherit_from: common_rubocop.yml
7
9
 
8
- # Enable additional Rails cops
9
- Rails:
10
- Enabled: true
11
-
12
10
  AllCops:
13
11
  Exclude:
14
12
  - 'bin/puma'
@@ -27,6 +25,9 @@ Documentation:
27
25
 
28
26
  Metrics/BlockLength:
29
27
  Exclude:
28
+ - 'bin/setup'
29
+ - 'bin/update'
30
+ - 'config/routes.rb'
30
31
  - 'spec/rails_helper.rb'
31
32
 
32
33
  # Rails foreign keys and indexes can get long. We want to ignore our annotation
@@ -39,6 +40,52 @@ Metrics/LineLength:
39
40
  - '\A# fk_rails_'
40
41
  - '\A# index_'
41
42
 
43
+ # For our Rails apps several of them use the `respond_to` with `format` blocks
44
+ # to handle various mime types (mostly HTML and JSON). Given our `do` / `end`
45
+ # block style for non-functional blocks (which includes both `respond_to` and
46
+ # `format`) the general method limit of is too small. This also applies to the
47
+ # helper methods which define the allowed parameters for the action; especially
48
+ # for larger forms.
49
+ #
50
+ # Here is an example of a minimal controller `update` method which uses the
51
+ # `respond_to` style to support just the HTML and JSON formats:
52
+ #
53
+ # ```ruby
54
+ # def update
55
+ # respond_to do |format|
56
+ # if @resource.update(resource_params)
57
+ # format.html do
58
+ # redirect_to resources_url, notice: 'Resource was successfully updated.'
59
+ # end
60
+ # format.json do
61
+ # render :show, status: :ok, location: @resource
62
+ # end
63
+ # else
64
+ # format.html do
65
+ # render :edit
66
+ # end
67
+ # format.json do
68
+ # render json: @resource.errors, status: :unprocessable_entity
69
+ # end
70
+ # end
71
+ # end
72
+ # end
73
+ # ```
74
+ #
75
+ # We do believe that the default size of 10, which is what we explicitly
76
+ # configure below so there's no confusion, is a good general limit to help
77
+ # encourage a balance between terseness and procedural code. Thus we do not
78
+ # want to raise the limit, instead we just want to exclude these controllers.
79
+ #
80
+ # At this time there is no way for us to exclude just the common controller
81
+ # actions / *_params methods so we exclude the entire file.
82
+ #
83
+ # Configuration parameters: CountComments.
84
+ Metrics/MethodLength:
85
+ Max: 10
86
+ Exclude:
87
+ - 'app/controllers/**/*_controller.rb'
88
+
42
89
  # Ignore subclass parent for one off benchmarks
43
90
  #
44
91
  # Benchmarks are generally meant to be small and targeted. They often have
@@ -59,14 +106,58 @@ Rails/ApplicationRecord:
59
106
  Rails/CreateTableWithTimestamps:
60
107
  Enabled: false
61
108
 
62
- # The ActiveSupport monkey patches for `present?` are nearly all defiend as:
109
+ # Usage of `find_by` is more expressive of intent than `where.first`. We should
110
+ # check all app code, not just the models to improve intent expression.
111
+ #
112
+ # Since rake tasks often live in `lib` we also check all of lib as well.
113
+ #
114
+ # Configuration parameters: Include.
115
+ # Include: app/models/**/*.rb
116
+ Rails/FindBy:
117
+ Enabled: true
118
+ Include:
119
+ - 'app/**/*.rb'
120
+ - 'lib/**/*.rb'
121
+
122
+ # Usage of `each` for large datasets can be a performance issue; specially a
123
+ # drain on system memory. When possible it's better to use `find_each` so that
124
+ # chunks of data are evaluated at a time.
125
+ #
126
+ # We should check all app code, not just the models to help prevent this. Since
127
+ # rake tasks often live in `lib` we also check all of lib as well.
128
+ #
129
+ # Configuration parameters: Include.
130
+ # Include: app/models/**/*.rb
131
+ Rails/FindEach:
132
+ Enabled: true
133
+ Include:
134
+ - 'app/**/*.rb'
135
+ - 'lib/**/*.rb'
136
+
137
+ # We understand the trade-offs for using the through model versus a lookup
138
+ # table. As such this cop is just noise as it flags only those cases we really
139
+ # do want a lookup table.
140
+ #
141
+ # Configuration parameters: Include.
142
+ # Include: app/models/**/*.rb
143
+ Rails/HasAndBelongsToMany:
144
+ Enabled: false
145
+
146
+ # We find the combo `:only` and `:if` readable. While the `:except` and `:if`
147
+ # combo is easier to read as a combined proc. As a team we are fine with
148
+ # handling this in PR reviews, until such time which Rubocop provides an option
149
+ # for us to configure this.
150
+ Rails/IgnoredSkipActionFilterOption:
151
+ Enabled: false
152
+
153
+ # The ActiveSupport monkey patches for `present?` are nearly all defined as:
63
154
  #
64
155
  # !blank?
65
156
  #
66
- # For most of use `unless blank?` reads just as easily as `if present?`.
157
+ # For most of us `unless blank?` reads just as easily as `if present?`.
67
158
  # Sometimes contextually, it can read better depending on the branch logic and
68
159
  # surrounding context. As `if present?` requires an additional negation and
69
- # method call it is technically slower. In the general case the perf different
160
+ # method call it is technically slower. In the general case the perf difference
70
161
  # isn't much but in some cases it matters. Thus, we are not enforcing changing
71
162
  # `unless blank?` to `if present?` and are leaving it up to the context to
72
163
  # decide which is a better fit.
@@ -79,7 +170,7 @@ Rails/Present:
79
170
  # We prefer you use the attribute readers and writes. For those special cases
80
171
  # where the intent is really to interact with the raw underlying attribute we
81
172
  # prefer `read_attribute` and `write_attribute`; as this makes the intent
82
- # explict. Ideally we'd never use the hash like accessor `[:attr]`.
173
+ # explicit. Ideally we'd never use the hash like accessor `[:attr]`.
83
174
  #
84
175
  # We disable this cop because it is not configurable.
85
176
  #
@@ -88,6 +179,37 @@ Rails/Present:
88
179
  Rails/ReadWriteAttribute:
89
180
  Enabled: false
90
181
 
182
+ # This ensures we do not ignore potential validation issues in the code. Doing
183
+ # so can lead to strange and surprising bugs where records are expected to
184
+ # be created, or be modified, but are not.
185
+ #
186
+ # # If author is a new record the book may not be created since the FK is
187
+ # # invalid. Perhaps omitting other fields, maybe new required fields, is
188
+ # # an oversight in the book creation as well.
189
+ # author.save
190
+ # Book.create(author: author)
191
+ #
192
+ # Or side effects are expected to occur but they do not:
193
+ #
194
+ # # This is a condensed default Rails scaffold controller for `destroy`.
195
+ # #
196
+ # # Once a `has_many` or `has_one` associations is added which specifies
197
+ # # `dependent: :restrict_with_error` this no longer behaves as expected.
198
+ # # Given such associations are often added much later in time errors in
199
+ # # this action are an all to common oversight in Rails.
200
+ # def destroy
201
+ # @book.destroy
202
+ # respond_to do |format|
203
+ # format.html do
204
+ # redirect_to books_url, notice: 'Book was successfully destroyed.'
205
+ # end
206
+ # end
207
+ # end
208
+ #
209
+ # Configuration parameters: AllowImplicitReturn, AllowedReceivers.
210
+ Rails/SaveBang:
211
+ Enabled: true
212
+
91
213
  # According to the Rails docs while the following methods skip validations they
92
214
  # only update the specified (single) attribute reducing risks. We'd rather not
93
215
  # warn for those cases:
@@ -105,17 +227,13 @@ Rails/ReadWriteAttribute:
105
227
  # - http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html
106
228
  # - http://api.rubyonrails.org/classes/ActiveRecord/Relation.html
107
229
  #
230
+ # Configuration parameters: Blacklist, Whitelist.
108
231
  # Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
109
232
  Rails/SkipsModelValidations:
110
- Blacklist:
111
- - 'decrement_counter'
112
- - 'increment_counter'
113
- - 'toggle!'
114
- - 'update_all'
115
- - 'update_attribute'
116
- - 'update_column'
117
- - 'update_columns'
118
- - 'update_counters'
233
+ Whitelist:
234
+ - 'decrement!'
235
+ - 'increment!'
236
+ - 'touch'
119
237
 
120
238
  # Rails uses compact style by default so we're disabling this with a :hammer:
121
239
  # for things likely to be generated by Rails (i.e. most things in app).
@@ -268,6 +268,7 @@ module Radius
268
268
  def safe_transform(value)
269
269
  return value.call if value.is_a?(Proc)
270
270
  return value if value.frozen?
271
+
271
272
  value.dup
272
273
  end
273
274
 
@@ -426,44 +427,56 @@ module Radius
426
427
 
427
428
  # Convenience wrapper for building, and persisting, a model template.
428
429
  #
429
- # This is a thin wrapper around `build(name, attrs).tap(&:save!)`. The
430
- # persistence message `save!` will only be called on objects which
431
- # respond to it.
432
- #
433
- # ### Avoid for New Code
430
+ # This is a thin wrapper around:
434
431
  #
435
- # It is strongly suggested that you avoid using `create` for new code.
436
- # Instead be explicit about when and how objects are persisted. This
437
- # allows you to have fine grain control over how your data is setup.
432
+ # ```ruby
433
+ # build(name, attrs, &block).tap(&:save!)
434
+ # ```
438
435
  #
439
- # We suggest that you create instances which need to be persisted before
440
- # your specs using the following syntax:
436
+ # The persistence message `save!` will only be called on objects which
437
+ # respond to it.
441
438
  #
442
- # ```ruby
443
- # let(:an_instance) { build("AnyClass") }
439
+ # @note It is generally suggested that you avoid using `build!` for new
440
+ # code. Instead be explicit about when and how objects are persisted.
441
+ # This allows you to have fine grain control over how your data is
442
+ # setup.
444
443
  #
445
- # before do
446
- # an_instance.save!
447
- # end
448
- # ```
444
+ # We suggest that you create instances which need to be persisted
445
+ # before your specs using the following syntax:
449
446
  #
450
- # Alternatively if you really want for the instance be lazy instantiated,
451
- # and persisted, pass the appropriate persistence method as the block:
447
+ # ```ruby
448
+ # let(:an_instance) { build("AnyClass") }
452
449
  #
453
- # ```ruby
454
- # let(:an_instance) { build("AnyClass", &:save!) }
455
- # ```
450
+ # before do
451
+ # an_instance.save!
452
+ # end
453
+ # ```
456
454
  #
457
455
  # @param (see .build)
458
456
  # @return (see .build)
459
457
  # @raise (see .build)
460
458
  # @see .build
461
459
  # @see .define_factory
462
- def create(name, custom_attrs = {}, &block)
460
+ # @since 0.5.0
461
+ def build!(name, custom_attrs = {}, &block)
463
462
  instance = build(name, custom_attrs, &block)
464
463
  instance.save! if instance.respond_to?(:save!)
465
464
  instance
466
465
  end
466
+
467
+ # Legacy helper provided for backwards compatibility support.
468
+ #
469
+ # This provides the same behavior as {.build!} and will be removed in a
470
+ # future release.
471
+ #
472
+ # @param (see .build)
473
+ # @return (see .build)
474
+ # @raise (see .build)
475
+ # @see .build
476
+ # @see .define_factory
477
+ def create(name, custom_attrs = {}, &block)
478
+ build!(name, custom_attrs, &block)
479
+ end
467
480
  end
468
481
  end
469
482
  end
@@ -7,7 +7,7 @@ require 'rspec/rails'
7
7
 
8
8
  RSpec.configure do |config|
9
9
  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
10
- config.fixture_path = ::Rails.root.join("spec", "fixtures")
10
+ config.fixture_path = ::Rails.root.join("spec", "fixtures").to_path
11
11
 
12
12
  # If you're not using ActiveRecord, or you'd prefer not to run each of your
13
13
  # examples within a transaction, remove the following line or assign false
@@ -40,7 +40,7 @@ RSpec.configure do |config|
40
40
  # - http://guides.rubyonrails.org/v5.1.4/testing.html#implementing-a-system-test
41
41
  # - https://relishapp.com/rspec/rspec-core/v/3-7/docs/filtering/exclusion-filters
42
42
  # - http://rspec.info/documentation/3.7/rspec-core/RSpec/Core/Configuration.html#filter_run_excluding-instance_method
43
- config.filter_run_excluding type: "system"
43
+ config.filter_run_excluding type: "system" unless config.files_to_run.one?
44
44
 
45
45
  # Always clear the cache before specs to avoid state leak problems
46
46
  config.before do
@@ -109,6 +109,11 @@ RSpec.configure do |config|
109
109
  config.include Radius::Spec::ModelFactory, :model_factory, :model_factories
110
110
  end
111
111
 
112
+ config.when_first_matching_example_defined(:tempfile, :tmpfile) do
113
+ require 'radius/spec/tempfile'
114
+ config.include Radius::Spec::Tempfile, :tempfile, :tmpfile
115
+ end
116
+
112
117
  config.when_first_matching_example_defined(type: :controller) do
113
118
  require 'radius/spec/model_factory'
114
119
  config.include Radius::Spec::ModelFactory, type: :controller
@@ -124,6 +129,11 @@ RSpec.configure do |config|
124
129
  config.include Radius::Spec::ModelFactory, type: :job
125
130
  end
126
131
 
132
+ config.when_first_matching_example_defined(type: :mailer) do
133
+ require 'radius/spec/model_factory'
134
+ config.include Radius::Spec::ModelFactory, type: :mailer
135
+ end
136
+
127
137
  config.when_first_matching_example_defined(type: :model) do
128
138
  require 'radius/spec/model_factory'
129
139
  config.include Radius::Spec::ModelFactory, type: :model
@@ -138,4 +148,14 @@ RSpec.configure do |config|
138
148
  require 'radius/spec/model_factory'
139
149
  config.include Radius::Spec::ModelFactory, type: :system
140
150
  end
151
+
152
+ config.when_first_matching_example_defined(:webmock) do
153
+ require 'webmock/rspec'
154
+ end
155
+
156
+ config.when_first_matching_example_defined(:vcr, :vcr_record, :vcr_record_new) do
157
+ require 'radius/spec/vcr'
158
+ end
141
159
  end
160
+
161
+ require 'radius/spec/rspec/negated_matchers'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Define negated matchers for use with composable matchers and compound
4
+ # expectations.
5
+ #
6
+ # @see https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/composing-matchers
7
+ # @see https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/compound-expectations
8
+ RSpec::Matchers.define_negated_matcher :exclude, :include
9
+ RSpec::Matchers.define_negated_matcher :excluding, :including
10
+ RSpec::Matchers.define_negated_matcher :not_eq, :eq
11
+
12
+ # Allows us to check that a block doesn't raise an exception while also
13
+ # checking for other changes using compound expectations; since you can't chain
14
+ # a negative (`not_to`) with any other matchers
15
+ #
16
+ # @see https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/compound-expectations
17
+ RSpec::Matchers.define_negated_matcher :not_change, :change
18
+ RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error
19
+ RSpec::Matchers.define_negated_matcher :not_raise_exception, :raise_exception
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'tempfile'
5
+
6
+ module Radius
7
+ module Spec
8
+ # Temporary file helpers
9
+ #
10
+ # These helpers are meant to ease the creation of temporary files to either
11
+ # stub the data out or provide a location for data to be saved then
12
+ # verified.
13
+ #
14
+ # In the case of file stubs, using these helpers allows you to co-locate
15
+ # the file data with the specs. This makes it easy for someone to read the
16
+ # spec and understand the test case; instead of having to find a fixture
17
+ # file and look at its data. This also makes it easy to change the data
18
+ # between specs, allowing them to focus on just what they need.
19
+ #
20
+ # To make these helpers available require them after the gem:
21
+ #
22
+ # ```ruby
23
+ # require 'radius/spec'
24
+ # require 'radius/spec/tempfile'
25
+ # ```
26
+ #
27
+ # ### Including Helpers in Specs
28
+ #
29
+ # There are multiple ways you can use these helpers. Which method you
30
+ # choose depends on how much perceived magic/syntactic sugar you want:
31
+ #
32
+ # - call the helpers directly on the module
33
+ # - manually include the helper methods in the specs
34
+ # - use metadata to auto load this feature and include it in the specs
35
+ #
36
+ # When using the metadata option you do not need to explicitly require the
37
+ # module. This gem registers metadata with the RSpec configuration when it
38
+ # loads and `RSpec` is defined. When the matching metadata is first used it
39
+ # will automatically require and include the helpers.
40
+ #
41
+ # Any of following metadata will include the factory helpers:
42
+ #
43
+ # - `:tempfile`
44
+ # - `:tmpfile`
45
+ #
46
+ # @example use a helper directly in specs
47
+ # require 'radius/spec/tempfile'
48
+ #
49
+ # RSpec.describe AnyClass do
50
+ # it "includes the file helpers" do
51
+ # Radius::Spec::Tempfile.using_tempfile do |pathname|
52
+ # code_under_test pathname
53
+ # expect(pathname.read).to eq "Any written data"
54
+ # end
55
+ # end
56
+ # end
57
+ # @example manually include the helpers
58
+ # require 'radius/spec/tempfile'
59
+ #
60
+ # RSpec.describe AnyClass do
61
+ # include Radius::Spec::Tempfile
62
+ # it "includes the file helpers" do
63
+ # using_tempfile do |pathname|
64
+ # code_under_test pathname
65
+ # expect(pathname.read).to eq "Any written data"
66
+ # end
67
+ # end
68
+ # end
69
+ # @example use metadata to auto include the helpers
70
+ # RSpec.describe AnyClass do
71
+ # it "includes the file helpers", :tempfile do
72
+ # using_tempfile do |pathname|
73
+ # code_under_test pathname
74
+ # expect(pathname.read).to eq "Any written data"
75
+ # end
76
+ # end
77
+ # end
78
+ # @since 0.5.0
79
+ module Tempfile
80
+ module_function
81
+
82
+ # Convenience wrapper for managaing temporary files.
83
+ #
84
+ # This creates a temporary file and yields its path to the provided
85
+ # block. When the block returns the temporary file is deleted.
86
+ #
87
+ # ### Optional Parameters
88
+ #
89
+ # The block is required. All other parameters are optional. All
90
+ # parameters except `data` are Ruby version dependent and will be
91
+ # forwarded directly to the stdlib's
92
+ # {https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html#method-c-create
93
+ # `Tempfile.create`}. The when the `data` parameter is provided it's
94
+ # contents will be written to the temporary file prior to yielding to the
95
+ # block.
96
+ #
97
+ # @example creating a tempfile to pass to code
98
+ # def write_hello_world(filepath)
99
+ # File.write filepath, "Hello World"
100
+ # end
101
+ #
102
+ # Radius::Spec::Tempfile.using_tempfile do |pathname|
103
+ # write_hello_world pathname
104
+ # end
105
+ # @example creating a file stub
106
+ # stub_data = "Any file stub data text."
107
+ # Radius::Spec::Tempfile.using_tempfile(data: stub_data) do |stubpath|
108
+ # # File.read(stubpath)
109
+ # # => "Any file stub data text."
110
+ # code_under_test stubpath
111
+ # end
112
+ # @example creating a file stub inline
113
+ # Radius::Spec::Tempfile.using_tempfile(data: <<~TEXT) do |stubpath|
114
+ # Any file stub data text.
115
+ # TEXT
116
+ # # File.read(stubpath)
117
+ # # => "Any file stub data text.\n"
118
+ # code_under_test stubpath
119
+ # end
120
+ # @example creating a file stub inline without trailing newline
121
+ # Radius::Spec::Tempfile.using_tempfile(data: <<~TEXT.chomp) do |stubpath|
122
+ # Any file stub data text.
123
+ # TEXT
124
+ # # File.read(stubpath)
125
+ # # => "Any file stub data text."
126
+ # code_under_test stubpath
127
+ # end
128
+ # @example writing binary data inline
129
+ # Radius::Spec::Tempfile.using_tempfile(encoding: Encoding::BINARY, data: <<~BIN.chomp) do |binpath|
130
+ # \xC8\x90\xC5\x9D\xE1\xB9\x95\xC4\x93\xC4\x89
131
+ # BIN
132
+ # # File.read(binpath)
133
+ # # => "Ȑŝṕēĉ"
134
+ # code_under_test binpath
135
+ # end
136
+ # @param args [Object] addition file creation options
137
+ #
138
+ # Passed directly to {https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html#method-c-create
139
+ # `Tempfile.create`}; see the stdlib docs for details on available
140
+ # options.
141
+ # @param data [String] stub data to write to the file before yielding
142
+ # @param kwargs [Hash{Symbol => Object}] addition file creation options
143
+ #
144
+ # Passed directly to {https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html#method-c-create
145
+ # `Tempfile.create`}; see the stdlib docs for details on available
146
+ # options.
147
+ # @yieldparam pathname [Pathname] {https://ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html path}
148
+ # of the created temporary file
149
+ # @note A block must be provided
150
+ # @see https://ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html Pathname
151
+ # @see https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html Tempfile
152
+ def using_tempfile(*args, data: nil, **kwargs)
153
+ args << 'tmpfile' if args.empty?
154
+ ::Tempfile.create(*args, **kwargs) do |f|
155
+ f.write(data)
156
+ f.close
157
+ yield Pathname(f.path)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end