Skip to content

Commit

Permalink
Prevent calling a stabbed method within a transaction
Browse files Browse the repository at this point in the history
With a new setting `within_transaction: false` you can prevent
calling a stabbed method within a database transaction.

```yaml
---
- class: Foo
  chain:
    - bar
    - baz
  within_transaction: false
  actions:
    - return: 0
```

If you call the method inside a transaction, the call will raise
an exception `::Isolator::UnsafeOperationError` pointing to
the method `Foo.bar.baz(*)`.

Notice that this setting is argument-specific. You can allow it
to be called inside a transaction with some arguments, and
<explicitly> forbid for some other arguments at the same time.
  • Loading branch information
nepalez committed Sep 21, 2019
1 parent 68c67fe commit b7ebf81
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 6 deletions.
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## 0.0.8 - WIP

### Added

- Protection of stubbed methods from being called within a db transaction (nepalez)

```yaml
---
- class: GoogleTranslateDiff
- chain: translate
- within_transaction: false
- arguments:
- Color
- :from: en
:to: de
- actions:
- return: Farbe
```
This method will raise an exception if the method is executed in a transaction:
```ruby
GoogleTranslateDiff.translate "Color", from: "en", to: "de"
# => "Farbe"

# but not within a transaction
ActiveRecord::Base.transaction do
GoogleTranslateDiff.translate "Color", from: "en", to: "de"
# => raise #<Isolator::UnsafeOperationError ...>
end
```

We use the [isolator][isolator] gem under the hood

## [0.0.7] - [2019-07-01]

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ For message chains:
- `class` for stubbed class
- `chain` for messages chain
- `arguments` (optional) for specific arguments
- `within_transaction` (default to `true`) if the method can be called within a database transaction
- `actions` for an array of actions for consecutive invocations of the chain

For constants:
Expand Down Expand Up @@ -161,6 +162,7 @@ Every action either `return` some value, or `raise` some exception
- class: Notifier
chain:
- create
within_transaction: false
arguments:
- :profileDeleted
- <%= profile_id %>
Expand Down
1 change: 1 addition & 0 deletions fixturama.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency "factory_bot", "~> 4.0"
gem.add_runtime_dependency "rspec", "~> 3.0"
gem.add_runtime_dependency "hashie", "~> 3.6"
gem.add_runtime_dependency "isolator", "~> 0.6.1"

gem.add_development_dependency "rake", "~> 10"
gem.add_development_dependency "rspec-its", "~> 1.2"
Expand Down
4 changes: 2 additions & 2 deletions lib/fixturama/stubs/chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ def to_s
# @option (see Fixturama::Stubs::Arguments#add_action)
# @return [self]
#
def update!(actions:, arguments: nil, **)
def update!(actions:, arguments: nil, within_transaction: true, **)
Utils.array(arguments).tap do |args|
stub = find_by(args)
unless stub
stub = Stubs::Chain::Arguments.new(self, args)
stub = Stubs::Chain::Arguments.new(self, within_transaction, args)
stubs << stub
end
stub.add!(*actions)
Expand Down
21 changes: 17 additions & 4 deletions lib/fixturama/stubs/chain/arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Fixturama
# Collection of arguments for a stub with a list of actions to be called
#
class Stubs::Chain::Arguments
attr_reader :chain, :arguments
attr_reader :chain, :arguments, :within_transaction

#
# Register new action for these set of arguments
Expand Down Expand Up @@ -54,7 +54,11 @@ def reset!
# @raise [StandardError]
#
def call_next!
list.fetch(counter) { list.last }.call.tap { @counter += 1 }
action = list.fetch(counter) { list.last }
isolate! unless within_transaction
action.call
ensure
@counter += 1
end

#
Expand All @@ -70,8 +74,9 @@ def to_s

# @param [Fixturama::Stubs::Chain] chain Back reference
# @param [Array<Object>] list Definition of arguments
def initialize(chain, list)
@chain = chain
def initialize(chain, within_transaction, list)
@chain = chain
@within_transaction = within_transaction || (require("isolator") || false)
@arguments = Utils.array(list)
end

Expand All @@ -82,5 +87,13 @@ def counter
def list
@list ||= []
end

def isolate!
return unless ::Isolator.within_transaction?

raise ::Isolator::UnsafeOperationError, <<~MESSAGE
You're trying to call #{self} inside db transaction
MESSAGE
end
end
end
22 changes: 22 additions & 0 deletions spec/fixturama/stub_fixture/_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ def pay(_)
end
end

context "when called within a transaction" do
before do
allow(Isolator).to receive(:within_transaction?).and_return(true)
end

context "without the :within_transaction option" do
let(:arguments) { [2] }

it "raises an exception" do
expect { subject }.not_to raise_error
end
end

context "when the option :within_transaction was set to false" do
let(:arguments) { [1] }

it "raises an exception" do
expect { subject }.to raise_error(Isolator::UnsafeOperationError)
end
end
end

context "with several actions" do
let(:arguments) { [2] * 4 }

Expand Down
1 change: 1 addition & 0 deletions spec/fixturama/stub_fixture/stub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
chain:
- new
- pay
within_transaction: false
arguments:
- 1
actions:
Expand Down

0 comments on commit b7ebf81

Please # to comment.