radius-spec 0.3.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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