From 5c94b624d16c4b089a67c78e457b83a94557ffc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20Bo=CC=88ttger?= Date: Wed, 1 Jul 2015 23:41:44 +1200 Subject: [PATCH] first set of ActiveRecord tests --- PLANNED_CHANGES.md | 1 + lib/aasm/base.rb | 2 +- .../persistence/active_record_persistence.rb | 12 +- spec/database.rb | 18 +- spec/models/active_record/derivate_new_dsl.rb | 4 + spec/models/active_record/false_state.rb | 18 + spec/models/active_record/gate.rb | 20 + .../active_record/no_direct_assignment.rb | 11 + spec/models/active_record/no_scope.rb | 11 + spec/models/active_record/simple_new_dsl.rb | 9 + spec/models/active_record/thief.rb | 15 + spec/models/active_record/with_enum.rb | 20 + spec/models/active_record/with_false_enum.rb | 16 + spec/models/active_record/with_true_enum.rb | 20 + spec/models/invalid_persistor.rb | 15 + spec/models/transactor.rb | 27 + spec/models/validator.rb | 39 ++ ...active_record_persistence_multiple_spec.rb | 512 ++++++++++++++++++ .../active_record_persistence_spec.rb | 12 +- 19 files changed, 766 insertions(+), 16 deletions(-) create mode 100644 spec/unit/persistence/active_record_persistence_multiple_spec.rb diff --git a/PLANNED_CHANGES.md b/PLANNED_CHANGES.md index 4939dce..beed22d 100644 --- a/PLANNED_CHANGES.md +++ b/PLANNED_CHANGES.md @@ -4,6 +4,7 @@ * add support for multiple state machines per class * check all tests + * _ActiveRecord_ when :right state machine is defined as well * what happen's if someone accesses `aasm`, but has defined a state machine for `aasm(:my_name)`? * persistence diff --git a/lib/aasm/base.rb b/lib/aasm/base.rb index 3e0bc3a..a9c30b3 100644 --- a/lib/aasm/base.rb +++ b/lib/aasm/base.rb @@ -33,7 +33,7 @@ module AASM # and attribute is directly assigned though @klass.class_eval %Q( def #{@state_machine.config.column}=(state_name) - if self.class.aasm.state_machine.config.no_direct_assignment + if self.class.aasm(:#{@name}).state_machine.config.no_direct_assignment raise AASM::NoDirectAssignmentError.new( 'direct assignment of AASM column has been disabled (see AASM configuration for this class)' ) diff --git a/lib/aasm/persistence/active_record_persistence.rb b/lib/aasm/persistence/active_record_persistence.rb index 4c80710..c122f69 100644 --- a/lib/aasm/persistence/active_record_persistence.rb +++ b/lib/aasm/persistence/active_record_persistence.rb @@ -118,18 +118,18 @@ module AASM def aasm_enum(name=:default) case AASM::StateMachine[self.class][name].config.enum when false then nil - when true then aasm_guess_enum_method - when nil then aasm_guess_enum_method if aasm_column_looks_like_enum + when true then aasm_guess_enum_method(name) + when nil then aasm_guess_enum_method(name) if aasm_column_looks_like_enum(name) else AASM::StateMachine[self.class][name].config.enum end end - def aasm_column_looks_like_enum - self.class.columns_hash[self.class.aasm.attribute_name.to_s].type == :integer + def aasm_column_looks_like_enum(name=:default) + self.class.columns_hash[self.class.aasm(name).attribute_name.to_s].type == :integer end - def aasm_guess_enum_method - self.class.aasm.attribute_name.to_s.pluralize.to_sym + def aasm_guess_enum_method(name=:default) + self.class.aasm(name).attribute_name.to_s.pluralize.to_sym end def aasm_skipping_validations(state_machine_name) diff --git a/spec/database.rb b/spec/database.rb index 4e7ccbd..52594aa 100644 --- a/spec/database.rb +++ b/spec/database.rb @@ -1,5 +1,5 @@ ActiveRecord::Migration.suppress_messages do - %w{gates readers writers transients simples no_scopes no_direct_assignments thieves localizer_test_models persisted_states provided_and_persisted_states with_enums with_true_enums with_false_enums false_states}.each do |table_name| + %w{gates multiple_gates readers writers transients simples no_scopes multiple_no_scopes no_direct_assignments multiple_no_direct_assignments thieves multiple_thieves localizer_test_models persisted_states provided_and_persisted_states with_enums with_true_enums with_false_enums false_states multiple_with_enums multiple_with_true_enums multiple_with_false_enums multiple_false_states}.each do |table_name| ActiveRecord::Migration.create_table table_name, :force => true do |t| t.string "aasm_state" end @@ -8,17 +8,29 @@ ActiveRecord::Migration.suppress_messages do ActiveRecord::Migration.create_table "simple_new_dsls", :force => true do |t| t.string "status" end + ActiveRecord::Migration.create_table "multiple_simple_new_dsls", :force => true do |t| + t.string "status" + end ActiveRecord::Migration.create_table "validators", :force => true do |t| t.string "name" t.string "status" end + ActiveRecord::Migration.create_table "multiple_validators", :force => true do |t| + t.string "name" + t.string "status" + end ActiveRecord::Migration.create_table "transactors", :force => true do |t| t.string "name" t.string "status" t.integer "worker_id" end + ActiveRecord::Migration.create_table "multiple_transactors", :force => true do |t| + t.string "name" + t.string "status" + t.integer "worker_id" + end ActiveRecord::Migration.create_table "workers", :force => true do |t| t.string "name" @@ -29,6 +41,10 @@ ActiveRecord::Migration.suppress_messages do t.string "name" t.string "status" end + ActiveRecord::Migration.create_table "multiple_invalid_persistors", :force => true do |t| + t.string "name" + t.string "status" + end ActiveRecord::Migration.create_table "fathers", :force => true do |t| t.string "aasm_state" diff --git a/spec/models/active_record/derivate_new_dsl.rb b/spec/models/active_record/derivate_new_dsl.rb index 532d0b3..2aa6cbd 100644 --- a/spec/models/active_record/derivate_new_dsl.rb +++ b/spec/models/active_record/derivate_new_dsl.rb @@ -1,3 +1,7 @@ require_relative 'simple_new_dsl' + class DerivateNewDsl < SimpleNewDsl end + +class MultipleDerivateNewDsl < MultipleSimpleNewDsl +end diff --git a/spec/models/active_record/false_state.rb b/spec/models/active_record/false_state.rb index 3c19aed..70e0797 100644 --- a/spec/models/active_record/false_state.rb +++ b/spec/models/active_record/false_state.rb @@ -15,3 +15,21 @@ class FalseState < ActiveRecord::Base end end end + +class MultipleFalseState < ActiveRecord::Base + include AASM + + def initialize(*args) + super + self.aasm_state = false + end + + aasm :left do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/gate.rb b/spec/models/active_record/gate.rb index b89daa2..7fcc70a 100644 --- a/spec/models/active_record/gate.rb +++ b/spec/models/active_record/gate.rb @@ -17,3 +17,23 @@ class Gate < ActiveRecord::Base end end end + +class MultipleGate < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + # attr_accessor :aasm_state + + def value + 'value' + end + + aasm :left do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/no_direct_assignment.rb b/spec/models/active_record/no_direct_assignment.rb index a3fea8e..fc942c1 100644 --- a/spec/models/active_record/no_direct_assignment.rb +++ b/spec/models/active_record/no_direct_assignment.rb @@ -8,3 +8,14 @@ class NoDirectAssignment < ActiveRecord::Base end end end + +class MultipleNoDirectAssignment < ActiveRecord::Base + include AASM + aasm :left, :no_direct_assignment => true do + state :pending, :initial => true + state :running + event :run do + transitions :from => :pending, :to => :running + end + end +end diff --git a/spec/models/active_record/no_scope.rb b/spec/models/active_record/no_scope.rb index dfa5380..13d7169 100644 --- a/spec/models/active_record/no_scope.rb +++ b/spec/models/active_record/no_scope.rb @@ -8,3 +8,14 @@ class NoScope < ActiveRecord::Base end end end + +class MultipleNoScope < ActiveRecord::Base + include AASM + aasm :left, :create_scopes => false do + state :pending, :initial => true + state :running + event :run do + transitions :from => :pending, :to => :running + end + end +end diff --git a/spec/models/active_record/simple_new_dsl.rb b/spec/models/active_record/simple_new_dsl.rb index 61a7f2a..7e63f57 100644 --- a/spec/models/active_record/simple_new_dsl.rb +++ b/spec/models/active_record/simple_new_dsl.rb @@ -6,3 +6,12 @@ class SimpleNewDsl < ActiveRecord::Base state :new end end + +class MultipleSimpleNewDsl < ActiveRecord::Base + include AASM + aasm :left, :column => :status + aasm :left do + state :unknown_scope + state :new + end +end diff --git a/spec/models/active_record/thief.rb b/spec/models/active_record/thief.rb index 7d84be6..9dd90d9 100644 --- a/spec/models/active_record/thief.rb +++ b/spec/models/active_record/thief.rb @@ -12,3 +12,18 @@ class Thief < ActiveRecord::Base end attr_accessor :skilled, :aasm_state end + +class MultipleThief < ActiveRecord::Base + if ActiveRecord::VERSION::MAJOR >= 3 + self.table_name = 'multiple_thieves' + else + set_table_name "multiple_thieves" + end + include AASM + aasm :left do + state :rich + state :jailed + initial_state Proc.new {|thief| thief.skilled ? :rich : :jailed } + end + attr_accessor :skilled, :aasm_state +end diff --git a/spec/models/active_record/with_enum.rb b/spec/models/active_record/with_enum.rb index 70ce1aa..83500e7 100644 --- a/spec/models/active_record/with_enum.rb +++ b/spec/models/active_record/with_enum.rb @@ -17,3 +17,23 @@ class WithEnum < ActiveRecord::Base end end end + +class MultipleWithEnum < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + attr_accessor :aasm_state + + def self.test + {} + end + + aasm :left, :enum => :test do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/with_false_enum.rb b/spec/models/active_record/with_false_enum.rb index 074589b..428276b 100644 --- a/spec/models/active_record/with_false_enum.rb +++ b/spec/models/active_record/with_false_enum.rb @@ -13,3 +13,19 @@ class WithFalseEnum < ActiveRecord::Base end end end + +class MultipleWithFalseEnum < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + attr_accessor :aasm_state + + aasm :left, :enum => false do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/active_record/with_true_enum.rb b/spec/models/active_record/with_true_enum.rb index 56a3be3..fea1687 100644 --- a/spec/models/active_record/with_true_enum.rb +++ b/spec/models/active_record/with_true_enum.rb @@ -17,3 +17,23 @@ class WithTrueEnum < ActiveRecord::Base end end end + +class MultipleWithTrueEnum < ActiveRecord::Base + include AASM + + # Fake this column for testing purposes + attr_accessor :aasm_state + + def value + 'value' + end + + aasm :left, :enum => true do + state :opened + state :closed + + event :view do + transitions :to => :read, :from => [:needs_attention] + end + end +end diff --git a/spec/models/invalid_persistor.rb b/spec/models/invalid_persistor.rb index 2888ac1..aebd071 100644 --- a/spec/models/invalid_persistor.rb +++ b/spec/models/invalid_persistor.rb @@ -14,3 +14,18 @@ class InvalidPersistor < ActiveRecord::Base end validates_presence_of :name end + +class MultipleInvalidPersistor < ActiveRecord::Base + include AASM + aasm :left, :column => :status, :skip_validation_on_save => true do + state :sleeping, :initial => true + state :running + event :run do + transitions :to => :running, :from => :sleeping + end + event :sleep do + transitions :to => :sleeping, :from => :running + end + end + validates_presence_of :name +end diff --git a/spec/models/transactor.rb b/spec/models/transactor.rb index 0ffda5c..bca819e 100644 --- a/spec/models/transactor.rb +++ b/spec/models/transactor.rb @@ -1,4 +1,5 @@ require 'active_record' + class Transactor < ActiveRecord::Base belongs_to :worker @@ -24,3 +25,29 @@ private end end + +class MultipleTransactor < ActiveRecord::Base + + belongs_to :worker + + include AASM + aasm :left, :column => :status do + state :sleeping, :initial => true + state :running, :before_enter => :start_worker, :after_enter => :fail + + event :run do + transitions :to => :running, :from => :sleeping + end + end + +private + + def start_worker + worker.update_attribute(:status, 'running') + end + + def fail + raise StandardError.new('failed on purpose') + end + +end diff --git a/spec/models/validator.rb b/spec/models/validator.rb index c23bd52..133d8e9 100644 --- a/spec/models/validator.rb +++ b/spec/models/validator.rb @@ -38,3 +38,42 @@ class Validator < ActiveRecord::Base raise StandardError.new('failed on purpose') end end + +class MultipleValidator < ActiveRecord::Base + + include AASM + aasm :left, :column => :status do + state :sleeping, :initial => true + state :running + state :failed, :after_enter => :fail + + event :run, :after_commit => :change_name! do + transitions :to => :running, :from => :sleeping + end + event :sleep do + after_commit do |name| + change_name_on_sleep name + end + transitions :to => :sleeping, :from => :running + end + event :fail do + transitions :to => :failed, :from => [:sleeping, :running] + end + end + + validates_presence_of :name + + def change_name! + self.name = "name changed" + save! + end + + def change_name_on_sleep name + self.name = name + save! + end + + def fail + raise StandardError.new('failed on purpose') + end +end diff --git a/spec/unit/persistence/active_record_persistence_multiple_spec.rb b/spec/unit/persistence/active_record_persistence_multiple_spec.rb new file mode 100644 index 0000000..54762de --- /dev/null +++ b/spec/unit/persistence/active_record_persistence_multiple_spec.rb @@ -0,0 +1,512 @@ +require 'active_record' +require 'spec_helper' +Dir[File.dirname(__FILE__) + "/../../models/active_record/*.rb"].sort.each do |f| + require File.expand_path(f) +end + +load_schema + +# if you want to see the statements while running the spec enable the following line +# require 'logger' +# ActiveRecord::Base.logger = Logger.new(STDERR) + +describe "instance methods" do + let(:gate) {MultipleGate.new} + + it "should respond to aasm persistence methods" do + expect(gate).to respond_to(:aasm_read_state) + expect(gate).to respond_to(:aasm_write_state) + expect(gate).to respond_to(:aasm_write_state_without_persistence) + end + + describe "aasm_column_looks_like_enum" do + subject { lambda{ gate.send(:aasm_column_looks_like_enum, :left) } } + + let(:column_name) { "value" } + let(:columns_hash) { Hash[column_name, column] } + + before :each do + allow(gate.class.aasm(:left)).to receive(:attribute_name).and_return(column_name.to_sym) + allow(gate.class).to receive(:columns_hash).and_return(columns_hash) + end + + context "when AASM column has integer type" do + let(:column) { double(Object, type: :integer) } + + it "returns true" do + expect(subject.call).to be_truthy + end + end + + context "when AASM column has string type" do + let(:column) { double(Object, type: :string) } + + it "returns false" do + expect(subject.call).to be_falsey + end + end + end + + describe "aasm_guess_enum_method" do + subject { lambda{ gate.send(:aasm_guess_enum_method, :left) } } + + before :each do + allow(gate.class.aasm(:left)).to receive(:attribute_name).and_return(:value) + end + + it "pluralizes AASM column name" do + expect(subject.call).to eq :values + end + end + + describe "aasm_enum" do + context "when AASM enum setting contains an explicit enum method name" do + let(:with_enum) { MultipleWithEnum.new } + + it "returns whatever value was set in AASM config" do + expect(with_enum.send(:aasm_enum, :left)).to eq :test + end + end + + context "when AASM enum setting is simply set to true" do + let(:with_true_enum) { MultipleWithTrueEnum.new } + before :each do + allow(MultipleWithTrueEnum.aasm(:left)).to receive(:attribute_name).and_return(:value) + end + + it "infers enum method name from pluralized column name" do + expect(with_true_enum.send(:aasm_enum, :left)).to eq :values + end + end + + context "when AASM enum setting is explicitly disabled" do + let(:with_false_enum) { MultipleWithFalseEnum.new } + + it "returns nil" do + expect(with_false_enum.send(:aasm_enum, :left)).to be_nil + end + end + + context "when AASM enum setting is not enabled" do + before :each do + allow(MultipleGate.aasm(:left)).to receive(:attribute_name).and_return(:value) + end + + context "when AASM column looks like enum" do + before :each do + allow(gate).to receive(:aasm_column_looks_like_enum).with(:left).and_return(true) + end + + it "infers enum method name from pluralized column name" do + expect(gate.send(:aasm_enum, :left)).to eq :values + end + end + + context "when AASM column doesn't look like enum'" do + before :each do + allow(gate).to receive(:aasm_column_looks_like_enum) + .and_return(false) + end + + it "returns nil, as we're not using enum" do + expect(gate.send(:aasm_enum, :left)).to be_nil + end + end + end + end + + context "when AASM is configured to use enum" do + let(:state_sym) { :running } + let(:state_code) { 2 } + let(:enum_name) { :states } + let(:enum) { Hash[state_sym, state_code] } + + before :each do + allow(gate).to receive(:aasm_enum).and_return(enum_name) + allow(gate).to receive(:aasm_write_attribute) + allow(gate).to receive(:write_attribute) + + allow(MultipleGate).to receive(enum_name).and_return(enum) + end + + describe "aasm_write_state" do + context "when AASM is configured to skip validations on save" do + before :each do + allow(gate).to receive(:aasm_skipping_validations).and_return(true) + end + + it "passes state code instead of state symbol to update_all" do + # stub_chain does not allow us to give expectations on call + # parameters in the middle of the chain, so we need to use + # intermediate object instead. + obj = double(Object, update_all: 1) + allow(MultipleGate).to receive(:where).and_return(obj) + + gate.aasm_write_state state_sym, :left + + expect(obj).to have_received(:update_all) + .with(Hash[gate.class.aasm(:left).attribute_name, state_code]) + end + end + + context "when AASM is not skipping validations" do + it "delegates state update to the helper method" do + # Let's pretend that validation is passed + allow(gate).to receive(:save).and_return(true) + + gate.aasm_write_state state_sym, :left + + expect(gate).to have_received(:aasm_write_attribute).with(state_sym, :left) + expect(gate).to_not have_received :write_attribute + end + end + end + + describe "aasm_write_state_without_persistence" do + it "delegates state update to the helper method" do + gate.aasm_write_state_without_persistence state_sym, :left + + expect(gate).to have_received(:aasm_write_attribute).with(state_sym, :left) + expect(gate).to_not have_received :write_attribute + end + end + + describe "aasm_raw_attribute_value" do + it "converts state symbol to state code" do + expect(gate.send(:aasm_raw_attribute_value, state_sym)) + .to eq state_code + end + end + end + + context "when AASM is configured to use string field" do + let(:state_sym) { :running } + + before :each do + allow(gate).to receive(:aasm_enum).and_return(nil) + end + + describe "aasm_raw_attribute_value" do + it "converts state symbol to string" do + expect(gate.send(:aasm_raw_attribute_value, state_sym)) + .to eq state_sym.to_s + end + end + end + + describe "aasm_write_attribute helper method" do + let(:sym) { :sym } + let(:value) { 42 } + + before :each do + allow(gate).to receive(:write_attribute) + allow(gate).to receive(:aasm_raw_attribute_value).and_return(value) + + gate.send(:aasm_write_attribute, sym, :left) + end + + it "generates attribute value using a helper method" do + expect(gate).to have_received(:aasm_raw_attribute_value).with(sym, :left) + end + + it "writes attribute to the model" do + expect(gate).to have_received(:write_attribute).with(:aasm_state, value) + end + end + + it "should return the initial state when new and the aasm field is nil" do + expect(gate.aasm(:left).current_state).to eq(:opened) + end + + it "should return the aasm column when new and the aasm field is not nil" do + gate.aasm_state = "closed" + expect(gate.aasm(:left).current_state).to eq(:closed) + end + + it "should return the aasm column when not new and the aasm.attribute_name is not nil" do + allow(gate).to receive(:new_record?).and_return(false) + gate.aasm_state = "state" + expect(gate.aasm(:left).current_state).to eq(:state) + end + + it "should allow a nil state" do + allow(gate).to receive(:new_record?).and_return(false) + gate.aasm_state = nil + expect(gate.aasm(:left).current_state).to be_nil + end + + context 'on initialization' do + it "should initialize the aasm state" do + expect(MultipleGate.new.aasm_state).to eql 'opened' + expect(MultipleGate.new.aasm(:left).current_state).to eql :opened + end + + it "should not initialize the aasm state if it has not been loaded" do + # we have to create a gate in the database, for which we only want to + # load the id, and not the state + gate = MultipleGate.create! + + # then we just load the gate ids + MultipleGate.select(:id).where(id: gate.id).first + end + end + +end + +if ActiveRecord::VERSION::MAJOR < 4 && ActiveRecord::VERSION::MINOR < 2 # won't work with Rails >= 4.2 +describe "direct state column access" do + it "accepts false states" do + f = MultipleFalseState.create! + expect(f.aasm_state).to eql false + expect { + f.aasm(:left).events.map(&:name) + }.to_not raise_error + end +end +end + +describe 'subclasses' do + it "should have the same states as its parent class" do + expect(MultipleDerivateNewDsl.aasm(:left).states).to eq(MultipleSimpleNewDsl.aasm(:left).states) + end + + it "should have the same events as its parent class" do + expect(MultipleDerivateNewDsl.aasm(:left).events).to eq(MultipleSimpleNewDsl.aasm(:left).events) + end + + it "should have the same column as its parent even for the new dsl" do + expect(MultipleSimpleNewDsl.aasm(:left).attribute_name).to eq(:status) + expect(MultipleDerivateNewDsl.aasm(:left).attribute_name).to eq(:status) + end +end + +describe "named scopes with the new DSL" do + context "Does not already respond_to? the scope name" do + it "should add a scope" do + expect(MultipleSimpleNewDsl).to respond_to(:unknown_scope) + expect(MultipleSimpleNewDsl.unknown_scope.is_a?(ActiveRecord::Relation)).to be_truthy + end + end + + context "Already respond_to? the scope name" do + it "should not add a scope" do + expect(MultipleSimpleNewDsl).to respond_to(:new) + expect(MultipleSimpleNewDsl.new.class).to eq(MultipleSimpleNewDsl) + end + end + + it "does not create scopes if requested" do + expect(MultipleNoScope).not_to respond_to(:pending) + end +end # scopes + +describe "direct assignment" do + it "is allowed by default" do + obj = MultipleNoScope.create + expect(obj.aasm_state.to_sym).to eql :pending + + obj.aasm_state = :running + expect(obj.aasm_state.to_sym).to eql :running + end + + it "is forbidden if configured" do + obj = MultipleNoDirectAssignment.create + expect(obj.aasm_state.to_sym).to eql :pending + + expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError) + expect(obj.aasm_state.to_sym).to eql :pending + end + + it 'can be turned off and on again' do + obj = MultipleNoDirectAssignment.create + expect(obj.aasm_state.to_sym).to eql :pending + + expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError) + expect(obj.aasm_state.to_sym).to eql :pending + + # allow it temporarily + MultipleNoDirectAssignment.aasm(:left).state_machine.config.no_direct_assignment = false + obj.aasm_state = :pending + expect(obj.aasm_state.to_sym).to eql :pending + + # and forbid it again + MultipleNoDirectAssignment.aasm(:left).state_machine.config.no_direct_assignment = true + expect {obj.aasm_state = :running}.to raise_error(AASM::NoDirectAssignmentError) + expect(obj.aasm_state.to_sym).to eql :pending + end +end # direct assignment + +describe 'initial states' do + it 'should support conditions' do + expect(MultipleThief.new(:skilled => true).aasm(:left).current_state).to eq(:rich) + expect(MultipleThief.new(:skilled => false).aasm(:left).current_state).to eq(:jailed) + end +end + +describe 'transitions with persistence' do + + it "should work for valid models" do + valid_object = MultipleValidator.create(:name => 'name') + expect(valid_object).to be_sleeping + valid_object.status = :running + expect(valid_object).to be_running + end + + it 'should not store states for invalid models' do + validator = MultipleValidator.create(:name => 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect(validator.run!).to be_falsey + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should store states for invalid models if configured' do + persistor = MultipleInvalidPersistor.create(:name => 'name') + expect(persistor).to be_valid + expect(persistor).to be_sleeping + + persistor.name = nil + expect(persistor).not_to be_valid + expect(persistor.run!).to be_truthy + expect(persistor).to be_running + + persistor = MultipleInvalidPersistor.find(persistor.id) + persistor.valid? + expect(persistor).to be_valid + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + + persistor.reload + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + end + + describe 'transactions' do + let(:worker) { Worker.create!(:name => 'worker', :status => 'sleeping') } + let(:transactor) { MultipleTransactor.create!(:name => 'transactor', :worker => worker) } + + it 'should rollback all changes' do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + expect {transactor.run!}.to raise_error(StandardError, 'failed on purpose') + expect(transactor).to be_running + expect(worker.reload.status).to eq('sleeping') + end + + context "nested transactions" do + it "should rollback all changes in nested transaction" do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + Worker.transaction do + expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose') + end + + expect(transactor).to be_running + expect(worker.reload.status).to eq('sleeping') + end + + it "should only rollback changes in the main transaction not the nested one" do + # change configuration to not require new transaction + AASM::StateMachine[MultipleTransactor][:left].config.requires_new_transaction = false + + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + Worker.transaction do + expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose') + end + + expect(transactor).to be_running + expect(worker.reload.status).to eq('running') + end + end + + describe "after_commit callback" do + it "should fire :after_commit if transaction was successful" do + validator = MultipleValidator.create(:name => 'name') + expect(validator).to be_sleeping + + validator.run! + expect(validator).to be_running + expect(validator.name).to eq("name changed") + + validator.sleep!("sleeper") + expect(validator).to be_sleeping + expect(validator.name).to eq("sleeper") + end + + it "should not fire :after_commit if transaction failed" do + validator = MultipleValidator.create(:name => 'name') + expect { validator.fail! }.to raise_error(StandardError, 'failed on purpose') + expect(validator.name).to eq("name") + end + + it "should not fire if not saving" do + validator = MultipleValidator.create(:name => 'name') + expect(validator).to be_sleeping + validator.run + expect(validator).to be_running + expect(validator.name).to eq("name") + end + + end + + context "when not persisting" do + it 'should not rollback all changes' do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + # Notice here we're calling "run" and not "run!" with a bang. + expect {transactor.run}.to raise_error(StandardError, 'failed on purpose') + expect(transactor).to be_running + expect(worker.reload.status).to eq('running') + end + + it 'should not create a database transaction' do + expect(transactor.class).not_to receive(:transaction) + expect {transactor.run}.to raise_error(StandardError, 'failed on purpose') + end + end + end +end + +describe "invalid states with persistence" do + + it "should not store states" do + validator = MultipleValidator.create(:name => 'name') + validator.status = 'invalid_state' + expect(validator.save).to be_falsey + expect {validator.save!}.to raise_error(ActiveRecord::RecordInvalid) + + validator.reload + expect(validator).to be_sleeping + end + + it "should store invalid states if configured" do + persistor = MultipleInvalidPersistor.create(:name => 'name') + persistor.status = 'invalid_state' + expect(persistor.save).to be_truthy + + persistor.reload + expect(persistor.status).to eq('invalid_state') + end + +end diff --git a/spec/unit/persistence/active_record_persistence_spec.rb b/spec/unit/persistence/active_record_persistence_spec.rb index 057e337..0939b3a 100644 --- a/spec/unit/persistence/active_record_persistence_spec.rb +++ b/spec/unit/persistence/active_record_persistence_spec.rb @@ -1,19 +1,15 @@ require 'active_record' require 'spec_helper' -Dir[File.dirname(__FILE__) + "/../../models/active_record/*.rb"].sort.each { |f| require File.expand_path(f) } +Dir[File.dirname(__FILE__) + "/../../models/active_record/*.rb"].sort.each do |f| + require File.expand_path(f) +end + load_schema # if you want to see the statements while running the spec enable the following line # require 'logger' # ActiveRecord::Base.logger = Logger.new(STDERR) -shared_examples_for "aasm model" do - it "should include persistence mixins" do - expect(klass.included_modules).to be_include(AASM::Persistence::ActiveRecordPersistence) - expect(klass.included_modules).to be_include(AASM::Persistence::ActiveRecordPersistence::InstanceMethods) - end -end - describe "instance methods" do let(:gate) {Gate.new}