From 68c67fe51d42d39247289c3ac46b59b145a539bf Mon Sep 17 00:00:00 2001 From: nepalez Date: Sat, 21 Sep 2019 14:53:03 +0300 Subject: [PATCH 1/2] Fix Gemfile --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index e902bca..887485a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source "https://rubygems.org" -# Specify your gem's dependencies in sms_aero.gemspec gemspec group :development, :test do From 0e4ad0f5f80905a3958611abd6aa1685eb1377be Mon Sep 17 00:00:00 2001 From: nepalez Date: Sat, 21 Sep 2019 20:33:55 +0300 Subject: [PATCH 2/2] Prevent calling a stabbed method within a transaction 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 forbid for some other arguments at the same time. --- CHANGELOG.md | 38 +++++++++++++++++++++++++- README.md | 2 ++ fixturama.gemspec | 1 + lib/fixturama/stubs/chain.rb | 4 +-- lib/fixturama/stubs/chain/arguments.rb | 21 +++++++++++--- spec/fixturama/stub_fixture/_spec.rb | 22 +++++++++++++++ spec/fixturama/stub_fixture/stub.yml | 1 + 7 files changed, 82 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de15ee..1ebc0d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 # + end + ``` + + We use the [isolator][isolator] gem under the hood + ## [0.0.7] - [2019-07-01] ### Added @@ -166,7 +200,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.0.1] - [2018-03-01] -This is a first public release with features extracted from production app. +This is the first public release with features extracted from production app. [0.0.1]: https://github.com/nepalez/fixturama/releases/tag/v0.0.1 [0.0.2]: https://github.com/nepalez/fixturama/compare/v0.0.1...v0.0.2 @@ -175,3 +209,5 @@ This is a first public release with features extracted from production app. [0.0.5]: https://github.com/nepalez/fixturama/compare/v0.0.4...v0.0.5 [0.0.6]: https://github.com/nepalez/fixturama/compare/v0.0.5...v0.0.6 [0.0.7]: https://github.com/nepalez/fixturama/compare/v0.0.6...v0.0.7 + +[isolator]: https://github.com/palkan/isolator diff --git a/README.md b/README.md index c84ac68..b0464c3 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 %> diff --git a/fixturama.gemspec b/fixturama.gemspec index 9ae5976..3e8ca51 100644 --- a/fixturama.gemspec +++ b/fixturama.gemspec @@ -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" diff --git a/lib/fixturama/stubs/chain.rb b/lib/fixturama/stubs/chain.rb index 2cbd902..9dbcaf9 100644 --- a/lib/fixturama/stubs/chain.rb +++ b/lib/fixturama/stubs/chain.rb @@ -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) diff --git a/lib/fixturama/stubs/chain/arguments.rb b/lib/fixturama/stubs/chain/arguments.rb index a3c0d21..0117464 100644 --- a/lib/fixturama/stubs/chain/arguments.rb +++ b/lib/fixturama/stubs/chain/arguments.rb @@ -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 @@ -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 # @@ -70,8 +74,9 @@ def to_s # @param [Fixturama::Stubs::Chain] chain Back reference # @param [Array] 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 @@ -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 diff --git a/spec/fixturama/stub_fixture/_spec.rb b/spec/fixturama/stub_fixture/_spec.rb index ee917db..4cd11ae 100644 --- a/spec/fixturama/stub_fixture/_spec.rb +++ b/spec/fixturama/stub_fixture/_spec.rb @@ -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 } diff --git a/spec/fixturama/stub_fixture/stub.yml b/spec/fixturama/stub_fixture/stub.yml index 979dd71..295ae67 100644 --- a/spec/fixturama/stub_fixture/stub.yml +++ b/spec/fixturama/stub_fixture/stub.yml @@ -19,6 +19,7 @@ chain: - new - pay + within_transaction: false arguments: - 1 actions: