Skip to content
Browse files

- bundler

  • Loading branch information...
1 parent 479456d commit ba8fdacbb0c62807a9cf816f2ef0e5db206745ec @martinciu committed Jun 27, 2010
Showing with 1,723 additions and 3 deletions.
  1. +1 −0 .gitignore
  2. +8 −0 Gemfile
  3. +2 −0 Rakefile
  4. +1 −0 lib/state_machine-mongoid.rb
  5. +295 −0 lib/state_machine/integrations/mongoid.rb
  6. +62 −0 state_machine-mongoid.gemspec
  7. +12 −0 test/helper.rb
  8. +1,342 −3 test/test_state_machine-mongoid.rb
View
1 .gitignore
@@ -17,5 +17,6 @@ tmtags
coverage
rdoc
pkg
+.bundle
## PROJECT::SPECIFIC
View
8 Gemfile
@@ -0,0 +1,8 @@
+source :rubygems
+gem "mongoid", ">=2.0.0.beta7"
+gem "state_machine", ">=0.9.2"
+gem "activemodel", ">=3.0.0.beta4", :require => "active_model"
+
+group :development do
+ gem "test-unit"
+end
View
2 Rakefile
@@ -1,5 +1,6 @@
require 'rubygems'
require 'rake'
+require 'bundler'
begin
require 'jeweler'
@@ -10,6 +11,7 @@ begin
gem.email = "marcin.ciunelis@gmail.com"
gem.homepage = "http://github.com/martinciu/state_machine-mongoid"
gem.authors = ["Marcin Ciunelis"]
+ gem.add_bundler_dependencies
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
View
1 lib/state_machine-mongoid.rb
@@ -0,0 +1 @@
+require 'state_machine/integrations/mongoid'
View
295 lib/state_machine/integrations/mongoid.rb
@@ -0,0 +1,295 @@
+module StateMachine
+ module Integrations #:nodoc:
+ # Adds support for integrating state machines with Mongoid models.
+ #
+ # == Examples
+ #
+ # Below is an example of a simple state machine defined within a
+ # Mongoid model:
+ #
+ # class Vehicle
+ # include Mongoid::Document
+ #
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ # end
+ #
+ # The examples in the sections below will use the above class as a
+ # reference.
+ #
+ # == Actions
+ #
+ # By default, the action that will be invoked when a state is transitioned
+ # is the +save+ action. This will cause the record to save the changes
+ # made to the state machine's attribute. *Note* that if any other changes
+ # were made to the record prior to transition, then those changes will
+ # be saved as well.
+ #
+ # For example,
+ #
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
+ # vehicle.name = 'Ford Explorer'
+ # vehicle.ignite # => true
+ # vehicle.reload # => #<Vehicle id: 1, name: "Ford Explorer", state: "idling">
+ #
+ # == Events
+ #
+ # As described in StateMachine::InstanceMethods#state_machine, event
+ # attributes are created for every machine that allow transitions to be
+ # performed automatically when the object's action (in this case, :save)
+ # is called.
+ #
+ # In Mongoid, these automated events are run in the following order:
+ # * before validation - Run before callbacks and persist new states, then validate
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
+ # * after save - Run after callbacks
+ #
+ # For example,
+ #
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
+ # vehicle.state_event # => nil
+ # vehicle.state_event = 'invalid'
+ # vehicle.valid? # => false
+ # vehicle.errors.full_messages # => ["State event is invalid"]
+ #
+ # vehicle.state_event = 'ignite'
+ # vehicle.valid? # => true
+ # vehicle.save # => true
+ # vehicle.state # => "idling"
+ # vehicle.state_event # => nil
+ #
+ # Note that this can also be done on a mass-assignment basis:
+ #
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
+ # vehicle.state # => "idling"
+ #
+ # This technique is always used for transitioning states when the +save+
+ # action (which is the default) is configured for the machine.
+ #
+ # === Security implications
+ #
+ # Beware that public event attributes mean that events can be fired
+ # whenever mass-assignment is being used. If you want to prevent malicious
+ # users from tampering with events through URLs / forms, the attribute
+ # should be protected like so:
+ #
+ # class Vehicle
+ # include Mongoid::Document
+ #
+ # attr_protected :state_event
+ # # attr_accessible ... # Alternative technique
+ #
+ # state_machine do
+ # ...
+ # end
+ # end
+ #
+ # If you want to only have *some* events be able to fire via mass-assignment,
+ # you can build two state machines (one public and one protected) like so:
+ #
+ # class Vehicle
+ # include Mongoid::Document
+ #
+ # attr_protected :state_event # Prevent access to events in the first machine
+ #
+ # state_machine do
+ # # Define private events here
+ # end
+ #
+ # # Public machine targets the same state as the private machine
+ # state_machine :public_state, :attribute => :state do
+ # # Define public events here
+ # end
+ # end
+ #
+ # == Validation errors
+ #
+ # If an event fails to successfully fire because there are no matching
+ # transitions for the current record, a validation error is added to the
+ # record's state attribute to help in determining why it failed and for
+ # reporting via the UI.
+ #
+ # For example,
+ #
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling">
+ # vehicle.ignite # => false
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
+ #
+ # If an event fails to fire because of a validation error on the record and
+ # *not* because a matching transition was not available, no error messages
+ # will be added to the state attribute.
+ #
+ # == Scopes
+ #
+ # To assist in filtering models with specific states, a series of basic
+ # scopes are defined on the model for finding records with or without a
+ # particular set of states.
+ #
+ # These scopes are essentially the functional equivalent of the following
+ # definitions:
+ #
+ # class Vehicle
+ # include Mongoid::Document
+ #
+ # def self.with_states(*states)
+ # all(:conditions => {:state => {'$in' => states}})
+ # end
+ # # with_states also aliased to with_state
+ #
+ # def self.without_states(*states)
+ # all(:conditions => {:state => {'$nin' => states}})
+ # end
+ # # without_states also aliased to without_state
+ # end
+ #
+ # *Note*, however, that the states are converted to their stored values
+ # before being passed into the query.
+ #
+ # Because of the way named scopes work in Mongoid, they *cannot* be
+ # chained.
+ #
+ # == Callbacks
+ #
+ # All before/after transition callbacks defined for Mongoid models
+ # behave in the same way that other Mongoid callbacks behave. The
+ # object involved in the transition is passed in as an argument.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # include Mongoid::Document
+ #
+ # state_machine :initial => :parked do
+ # before_transition any => :idling do |vehicle|
+ # vehicle.put_on_seatbelt
+ # end
+ #
+ # before_transition do |vehicle, transition|
+ # # log message
+ # end
+ #
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ # end
+ #
+ # def put_on_seatbelt
+ # ...
+ # end
+ # end
+ #
+ # Note, also, that the transition can be accessed by simply defining
+ # additional arguments in the callback block.
+ module Mongoid
+ include ActiveModel
+
+ # The default options to use for state machines using this integration
+ @defaults = {:action => :save}
+
+ # Should this integration be used for state machines in the given class?
+ # Classes that include Mongoid::Document will automatically use the
+ # Mongoid integration.
+ def self.matches?(klass)
+ defined?(::Mongoid::Document) && klass <= ::Mongoid::Document
+ end
+
+ # Adds a validation error to the given object (no i18n support)
+ def invalidate(object, attribute, message, values = [])
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
+ end
+
+ protected
+ # Does not support observers
+ def supports_observers?
+ false
+ end
+
+ # Always adds validation support
+ def supports_validations?
+ true
+ end
+
+ # Only runs validations on the action if using <tt>:save</tt>
+ def runs_validations_on_action?
+ action == :save
+ end
+
+ # Always adds dirty tracking support
+ def supports_dirty_tracking?(object)
+ true
+ end
+
+ # Don't allow callback terminators
+ def callback_terminator
+ end
+
+ # Defines an initialization hook into the owner class for setting the
+ # initial state of the machine *before* any attributes are set on the
+ # object
+ def define_state_initializer
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
+ def initialize(attrs = {}, *args)
+ from_database = args.first
+
+ if !from_database && (!attrs || !attrs.stringify_keys.key?('_id'))
+ filtered = respond_to?(:filter_protected_attrs) ? filter_protected_attrs(attrs) : attrs
+ ignore = filtered ? filtered.keys : []
+
+ initialize_state_machines(:dynamic => false, :ignore => ignore)
+ super
+ initialize_state_machines(:dynamic => true, :ignore => ignore)
+ else
+ super
+ end
+ end
+ end_eval
+ end
+
+ # Skips defining reader/writer methods since this is done automatically
+ def define_state_accessor
+ owner_class.key(attribute, String) unless owner_class.keys.include?(attribute)
+
+ name = self.name
+ owner_class.validates_each(attribute, :logic => lambda {|*|
+ machine = self.class.state_machine(name)
+ machine.invalidate(self, :state, :invalid) unless machine.states.match(self)
+ })
+ end
+
+ # Adds support for defining the attribute predicate, while providing
+ # compatibility with the default predicate which determines whether
+ # *anything* is set for the attribute's value
+ def define_state_predicate
+ name = self.name
+
+ # Still use class_eval here instance of define_instance_method since
+ # we need to be able to call +super+
+ @instance_helper_module.class_eval do
+ define_method("#{name}?") do |*args|
+ args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
+ end
+ end
+ end
+
+ # Adds hooks into validation for automatically firing events
+ def define_action_helpers
+ super(action == :save ? :create_or_update : action)
+ end
+
+ # Creates a scope for finding records *with* a particular state or
+ # states for the attribute
+ def create_with_scope(name)
+ lambda {|model, values| model.all(:conditions => {attribute => {'$in' => values}})}
+ end
+
+ # Creates a scope for finding records *without* a particular state or
+ # states for the attribute
+ def create_without_scope(name)
+ lambda {|model, values| model.all(:conditions => {attribute => {'$nin' => values}})}
+ end
+ end
+ end
+end
View
62 state_machine-mongoid.gemspec
@@ -0,0 +1,62 @@
+# Generated by jeweler
+# DO NOT EDIT THIS FILE DIRECTLY
+# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{state_machine-mongoid}
+ s.version = "0.0.0"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Marcin Ciunelis"]
+ s.date = %q{2010-06-27}
+ s.description = %q{TODO: longer description of your gem}
+ s.email = %q{marcin.ciunelis@gmail.com}
+ s.extra_rdoc_files = [
+ "LICENSE",
+ "README.rdoc"
+ ]
+ s.files = [
+ ".document",
+ ".gitignore",
+ "LICENSE",
+ "README.rdoc",
+ "Rakefile",
+ "VERSION",
+ "lib/state_machine-mongoid.rb",
+ "test/helper.rb",
+ "test/test_state_machine-mongoid.rb"
+ ]
+ s.homepage = %q{http://github.com/martinciu/state_machine-mongoid}
+ s.rdoc_options = ["--charset=UTF-8"]
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.3.7}
+ s.summary = %q{state_machine mongoid integration}
+ s.test_files = [
+ "test/helper.rb",
+ "test/test_state_machine-mongoid.rb"
+ ]
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
+ s.add_runtime_dependency(%q<mongoid>, [">= 2.0.0.beta7"])
+ s.add_runtime_dependency(%q<state_machine>, [">= 0.9.2"])
+ s.add_runtime_dependency(%q<activemodel>, [">= 3.0.0.beta4"])
+ s.add_development_dependency(%q<test-unit>, [">= 0"])
+ else
+ s.add_dependency(%q<mongoid>, [">= 2.0.0.beta7"])
+ s.add_dependency(%q<state_machine>, [">= 0.9.2"])
+ s.add_dependency(%q<activemodel>, [">= 3.0.0.beta4"])
+ s.add_dependency(%q<test-unit>, [">= 0"])
+ end
+ else
+ s.add_dependency(%q<mongoid>, [">= 2.0.0.beta7"])
+ s.add_dependency(%q<state_machine>, [">= 0.9.2"])
+ s.add_dependency(%q<activemodel>, [">= 3.0.0.beta4"])
+ s.add_dependency(%q<test-unit>, [">= 0"])
+ end
+end
+
View
12 test/helper.rb
@@ -1,5 +1,17 @@
require 'rubygems'
require 'test/unit'
+require 'bundler'
+
+begin
+ Bundler.setup(:default, :development)
+rescue Bundler::BundlerError => e
+ $stderr.puts e.message
+ $stderr.puts "Run `bundle install` to install missing gems"
+ exit e.status_code
+end
+
+require 'active_model'
+require 'state_machine'
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
View
1,345 test/test_state_machine-mongoid.rb
@@ -1,7 +1,1346 @@
require 'helper'
-class TestStateMachineMongoid < Test::Unit::TestCase
- def test_something_for_real
- flunk "hey buddy, you should probably rename this file and start testing for real"
+# if ENV['VERSION'] && Gem::Version.new(ENV['VERSION']) <= Gem::Version.new('0.7.0') || !Gem.available?('>=0.7.0')
+# gem 'activesupport', '~>2.3'
+# require 'active_support'
+# end
+
+# gem 'mongoid', '>=2.0.0.beta7'
+# require 'mongoid'
+
+# Establish database connection
+MongoMapper.connection = Mongo::Connection.new('127.0.0.1', 27017, {:logger => Logger.new("#{File.dirname(__FILE__)}/../../mongo_mapper.log")})
+MongoMapper.database = 'test'
+
+module MongoMapperTest
+ class BaseTestCase < Test::Unit::TestCase
+ def default_test
+ end
+
+ protected
+ # Creates a new MongoMapper model (and the associated table)
+ def new_model(table_name = :foo, &block)
+
+ model = Class.new do
+ include MongoMapper::Document
+ set_collection_name(table_name)
+
+ def self.name; "MongoMapperTest::#{collection_name}"; end
+ def self.to_s; "MongoMapperTest::#{collection_name}"; end
+
+ key :state, String
+ end
+ model.class_eval(&block) if block_given?
+ model.collection.remove
+ model
+ end
+ end
+
+ class IntegrationTest < BaseTestCase
+ def test_should_match_if_class_includes_mongo_mapper
+ assert StateMachine::Integrations::MongoMapper.matches?(new_model)
+ end
+
+ def test_should_not_match_if_class_does_not_include_mongo_mapper
+ assert !StateMachine::Integrations::MongoMapper.matches?(Class.new)
+ end
+
+ def test_should_have_defaults
+ assert_equal e = {:action => :save}, StateMachine::Integrations::MongoMapper.defaults
+ end
+ end
+
+ class MachineWithoutDatabaseTest < BaseTestCase
+ def setup
+ @model = new_model do
+ # Simulate the database not being available entirely
+ def self.connection
+ raise Mongo::ConnectionFailure
+ end
+ end
+ end
+
+ def test_should_allow_machine_creation
+ assert_nothing_raised { StateMachine::Machine.new(@model) }
+ end
+ end
+
+ class MachineByDefaultTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ end
+
+ def test_should_use_save_as_action
+ assert_equal :save, @machine.action
+ end
+
+ def test_should_not_have_any_before_callbacks
+ assert_equal 0, @machine.callbacks[:before].size
+ end
+
+ def test_should_not_have_any_after_callbacks
+ assert_equal 0, @machine.callbacks[:after].size
+ end
+ end
+
+ class MachineWithStaticInitialStateTest < BaseTestCase
+ def setup
+ @model = new_model do
+ attr_accessor :value
+ end
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
+ end
+
+ def test_should_set_initial_state_on_created_object
+ record = @model.new
+ assert_equal 'parked', record.state
+ end
+
+ def test_should_set_initial_state_with_nil_attributes
+ record = @model.new(nil)
+ assert_equal 'parked', record.state
+ end
+
+ def test_should_still_set_attributes
+ record = @model.new(:value => 1)
+ assert_equal 1, record.value
+ end
+
+ def test_should_not_allow_initialize_blocks
+ block_args = nil
+ record = @model.new do |*args|
+ block_args = args
+ end
+
+ assert_nil block_args
+ end
+
+ def test_should_set_initial_state_before_setting_attributes
+ @model.class_eval do
+ attr_accessor :state_during_setter
+
+ define_method(:value=) do |value|
+ self.state_during_setter = state
+ end
+ end
+
+ record = @model.new(:value => 1)
+ assert_equal 'parked', record.state_during_setter
+ end
+
+ def test_should_not_set_initial_state_after_already_initialized
+ record = @model.new(:value => 1)
+ assert_equal 'parked', record.state
+
+ record.state = 'idling'
+ record.attributes = {}
+ assert_equal 'idling', record.state
+ end
+
+ def test_should_use_stored_values_when_loading_from_database
+ @machine.state :idling
+
+ record = @model.find(@model.create(:state => 'idling').id)
+ assert_equal 'idling', record.state
+ end
+
+ def test_should_use_stored_values_when_loading_from_database_with_nil_state
+ @machine.state nil
+
+ record = @model.find(@model.create(:state => nil).id)
+ assert_nil record.state
+ end
+ end
+
+ class MachineWithDynamicInitialStateTest < BaseTestCase
+ def setup
+ @model = new_model do
+ attr_accessor :value
+ end
+ @machine = StateMachine::Machine.new(@model, :initial => lambda {|object| :parked})
+ @machine.state :parked
+ end
+
+ def test_should_set_initial_state_on_created_object
+ record = @model.new
+ assert_equal 'parked', record.state
+ end
+
+ def test_should_still_set_attributes
+ record = @model.new(:value => 1)
+ assert_equal 1, record.value
+ end
+
+ def test_should_not_allow_initialize_blocks
+ block_args = nil
+ record = @model.new do |*args|
+ block_args = args
+ end
+
+ assert_nil block_args
+ end
+
+ def test_should_set_initial_state_after_setting_attributes
+ @model.class_eval do
+ attr_accessor :state_during_setter
+
+ define_method(:value=) do |value|
+ self.state_during_setter = state || 'nil'
+ end
+ end
+
+ record = @model.new(:value => 1)
+ assert_equal 'nil', record.state_during_setter
+ end
+
+ def test_should_not_set_initial_state_after_already_initialized
+ record = @model.new(:value => 1)
+ assert_equal 'parked', record.state
+
+ record.state = 'idling'
+ record.attributes = {}
+ assert_equal 'idling', record.state
+ end
+
+ def test_should_use_stored_values_when_loading_from_database
+ @machine.state :idling
+
+ record = @model.find(@model.create(:state => 'idling').id)
+ assert_equal 'idling', record.state
+ end
+
+ def test_should_use_stored_values_when_loading_from_database_with_nil_state
+ @machine.state nil
+
+ record = @model.find(@model.create(:state => nil).id)
+ assert_nil record.state
+ end
+ end
+
+ class MachineWithColumnDefaultTest < BaseTestCase
+ def setup
+ @model = new_model do
+ key :status, String, :default => 'idling'
+ end
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
+ @record = @model.new
+ end
+
+ def test_should_use_machine_default
+ assert_equal 'parked', @record.status
+ end
+ end
+
+ class MachineWithConflictingPredicateTest < BaseTestCase
+ def setup
+ @model = new_model do
+ def state?(*args)
+ true
+ end
+ end
+
+ @machine = StateMachine::Machine.new(@model)
+ @record = @model.new
+ end
+
+ def test_should_not_define_attribute_predicate
+ assert @record.state?
+ end
+ end
+
+ class MachineWithColumnStateAttributeTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
+ @machine.other_states(:idling)
+
+ @record = @model.new
+ end
+
+ def test_should_not_override_the_column_reader
+ @record[:state] = 'parked'
+ assert_equal 'parked', @record.state
+ end
+
+ def test_should_not_override_the_column_writer
+ @record.state = 'parked'
+ assert_equal 'parked', @record[:state]
+ end
+
+ def test_should_have_an_attribute_predicate
+ assert @record.respond_to?(:state?)
+ end
+
+ def test_should_test_for_existence_on_predicate_without_parameters
+ assert @record.state?
+
+ @record.state = nil
+ assert !@record.state?
+ end
+
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
+ assert !@record.state?(:idling)
+ end
+
+ def test_should_return_true_for_predicate_if_matches_current_value
+ assert @record.state?(:parked)
+ end
+
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
+ assert_raise(IndexError) { @record.state?(:invalid) }
+ end
+ end
+
+ class MachineWithNonColumnStateAttributeUndefinedTest < BaseTestCase
+ def setup
+ @model = new_model do
+ def initialize
+ # Skip attribute initialization
+ @initialized_state_machines = true
+ super
+ end
+ end
+
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
+ @machine.other_states(:idling)
+ @record = @model.new
+ end
+
+ def test_should_define_a_new_key_for_the_attribute
+ assert_not_nil @model.keys[:status]
+ end
+
+ def test_should_define_a_reader_attribute_for_the_attribute
+ assert @record.respond_to?(:status)
+ end
+
+ def test_should_define_a_writer_attribute_for_the_attribute
+ assert @record.respond_to?(:status=)
+ end
+
+ def test_should_define_an_attribute_predicate
+ assert @record.respond_to?(:status?)
+ end
+ end
+
+ class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
+ def setup
+ @model = new_model do
+ attr_accessor :status
+ end
+
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
+ @machine.other_states(:idling)
+ @record = @model.new
+ end
+
+ def test_should_return_false_for_predicate_if_does_not_match_current_value
+ assert !@record.status?(:idling)
+ end
+
+ def test_should_return_true_for_predicate_if_matches_current_value
+ assert @record.status?(:parked)
+ end
+
+ def test_should_raise_exception_for_predicate_if_invalid_state_specified
+ assert_raise(IndexError) { @record.status?(:invalid) }
+ end
+
+ def test_should_set_initial_state_on_created_object
+ assert_equal 'parked', @record.status
+ end
+ end
+
+ class MachineWithAliasedAttributeTest < BaseTestCase
+ def setup
+ @model = new_model do
+ alias_attribute :vehicle_status, :state
+ end
+
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :vehicle_status)
+ @machine.state :parked
+
+ @record = @model.new
+ end
+
+ def test_should_check_custom_attribute_for_predicate
+ @record.vehicle_status = nil
+ assert !@record.status?(:parked)
+
+ @record.vehicle_status = 'parked'
+ assert @record.status?(:parked)
+ end
+ end
+
+ class MachineWithInitializedStateTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
+ @machine.state nil, :idling
+ end
+
+ def test_should_allow_nil_initial_state_when_static
+ record = @model.new(:state => nil)
+ assert_nil record.state
+ end
+
+ def test_should_allow_nil_initial_state_when_dynamic
+ @machine.initial_state = lambda {:parked}
+ record = @model.new(:state => nil)
+ assert_nil record.state
+ end
+
+ def test_should_allow_different_initial_state_when_static
+ record = @model.new(:state => 'idling')
+ assert_equal 'idling', record.state
+ end
+
+ def test_should_allow_different_initial_state_when_dynamic
+ @machine.initial_state = lambda {:parked}
+ record = @model.new(:state => 'idling')
+ assert_equal 'idling', record.state
+ end
+
+ if defined?(MongoMapper::Plugins::Protected)
+ def test_should_use_default_state_if_protected
+ @model.class_eval do
+ attr_protected :state
+ end
+
+ record = @model.new(:state => 'idling')
+ assert_equal 'parked', record.state
+ end
+ end
+ end
+
+ class MachineWithLoopbackTest < BaseTestCase
+ def setup
+ @model = new_model do
+ key :updated_at, Time
+
+ before_update do |record|
+ record.updated_at = Time.now
+ end
+ end
+
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
+ @machine.event :park
+
+ @record = @model.create(:updated_at => Time.now - 1)
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
+
+ @timestamp = @record.updated_at
+ @transition.perform
+ end
+
+ def test_should_update_record
+ assert_not_equal @timestamp, @record.updated_at
+ end
+ end
+
+ class MachineWithDirtyAttributesTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
+ @machine.event :ignite
+ @machine.state :idling
+
+ @record = @model.create
+
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
+ @transition.perform(false)
+ end
+
+ def test_should_include_state_in_changed_attributes
+ assert_equal %w(state), @record.changed
+ end
+
+ def test_should_track_attribute_change
+ assert_equal %w(parked idling), @record.changes['state']
+ end
+
+ def test_should_not_reset_changes_on_multiple_transitions
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
+ transition.perform(false)
+
+ assert_equal %w(parked idling), @record.changes['state']
+ end
+
+ def test_should_not_have_changes_when_loaded_from_database
+ record = @model.find(@record.id)
+ assert !record.changed?
+ end
+ end
+
+ class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
+ @machine.event :park
+
+ @record = @model.create
+
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
+ @transition.perform(false)
+ end
+
+ def test_should_include_state_in_changed_attributes
+ assert_equal %w(state), @record.changed
+ end
+
+ def test_should_track_attribute_changes
+ assert_equal %w(parked parked), @record.changes['state']
+ end
+ end
+
+ class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
+ def setup
+ @model = new_model do
+ key :status, String, :default => 'idling'
+ end
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
+ @machine.event :ignite
+ @machine.state :idling
+
+ @record = @model.create
+
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
+ @transition.perform(false)
+ end
+
+ def test_should_include_state_in_changed_attributes
+ assert_equal %w(status), @record.changed
+ end
+
+ def test_should_track_attribute_change
+ assert_equal %w(parked idling), @record.changes['status']
+ end
+
+ def test_should_not_reset_changes_on_multiple_transitions
+ transition = StateMachine::Transition.new(@record, @machine, :ignite, :idling, :idling)
+ transition.perform(false)
+
+ assert_equal %w(parked idling), @record.changes['status']
+ end
+ end
+
+ class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
+ def setup
+ @model = new_model do
+ key :status, String, :default => 'idling'
+ end
+ @machine = StateMachine::Machine.new(@model, :status, :initial => :parked)
+ @machine.event :park
+
+ @record = @model.create
+
+ @transition = StateMachine::Transition.new(@record, @machine, :park, :parked, :parked)
+ @transition.perform(false)
+ end
+
+ def test_should_include_state_in_changed_attributes
+ assert_equal %w(status), @record.changed
+ end
+
+ def test_should_track_attribute_changes
+ assert_equal %w(parked parked), @record.changes['status']
+ end
+ end
+
+ class MachineWithCallbacksTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model, :initial => :parked)
+ @machine.other_states :idling
+ @machine.event :ignite
+
+ @record = @model.new(:state => 'parked')
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
+ end
+
+ def test_should_run_before_callbacks
+ called = false
+ @machine.before_transition {called = true}
+
+ @transition.perform
+ assert called
+ end
+
+ def test_should_pass_record_to_before_callbacks_with_one_argument
+ record = nil
+ @machine.before_transition {|arg| record = arg}
+
+ @transition.perform
+ assert_equal @record, record
+ end
+
+ def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
+ callback_args = nil
+ @machine.before_transition {|*args| callback_args = args}
+
+ @transition.perform
+ assert_equal [@record, @transition], callback_args
+ end
+
+ def test_should_run_before_callbacks_outside_the_context_of_the_record
+ context = nil
+ @machine.before_transition {context = self}
+
+ @transition.perform
+ assert_equal self, context
+ end
+
+ def test_should_run_after_callbacks
+ called = false
+ @machine.after_transition {called = true}
+
+ @transition.perform
+ assert called
+ end
+
+ def test_should_pass_record_to_after_callbacks_with_one_argument
+ record = nil
+ @machine.after_transition {|arg| record = arg}
+
+ @transition.perform
+ assert_equal @record, record
+ end
+
+ def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
+ callback_args = nil
+ @machine.after_transition {|*args| callback_args = args}
+
+ @transition.perform
+ assert_equal [@record, @transition], callback_args
+ end
+
+ def test_should_run_after_callbacks_outside_the_context_of_the_record
+ context = nil
+ @machine.after_transition {context = self}
+
+ @transition.perform
+ assert_equal self, context
+ end
+
+ def test_should_run_around_callbacks
+ before_called = false
+ after_called = false
+ @machine.around_transition {|block| before_called = true; block.call; after_called = true}
+
+ @transition.perform
+ assert before_called
+ assert after_called
+ end
+
+ def test_should_include_transition_states_in_known_states
+ @machine.before_transition :to => :first_gear, :do => lambda {}
+
+ assert_equal [:parked, :idling, :first_gear], @machine.states.map {|state| state.name}
+ end
+
+ def test_should_allow_symbolic_callbacks
+ callback_args = nil
+
+ klass = class << @record; self; end
+ klass.send(:define_method, :after_ignite) do |*args|
+ callback_args = args
+ end
+
+ @machine.before_transition(:after_ignite)
+
+ @transition.perform
+ assert_equal [@transition], callback_args
+ end
+
+ def test_should_allow_string_callbacks
+ class << @record
+ attr_reader :callback_result
+ end
+
+ @machine.before_transition('@callback_result = [1, 2, 3]')
+ @transition.perform
+
+ assert_equal [1, 2, 3], @record.callback_result
+ end
+ end
+
+ class MachineWithFailedBeforeCallbacksTest < BaseTestCase
+ def setup
+ @callbacks = []
+
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+ @machine.before_transition {@callbacks << :before_1; false}
+ @machine.before_transition {@callbacks << :before_2}
+ @machine.after_transition {@callbacks << :after}
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
+
+ @record = @model.new(:state => 'parked')
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
+ @result = @transition.perform
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_change_current_state
+ assert_equal 'idling', @record.state
+ end
+
+ def test_should_run_action
+ assert !@record.new_record?
+ end
+
+ def test_should_run_further_callbacks
+ assert_equal [:before_1, :before_2, :around_before, :around_after, :after], @callbacks
+ end
+ end
+
+ class MachineWithFailedActionTest < BaseTestCase
+ def setup
+ @model = new_model do
+ validates_inclusion_of :state, :within => %w(first_gear)
+ end
+
+ @machine = StateMachine::Machine.new(@model)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+
+ @callbacks = []
+ @machine.before_transition {@callbacks << :before}
+ @machine.after_transition {@callbacks << :after}
+ @machine.after_transition(:include_failures => true) {@callbacks << :after_failure}
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
+ @machine.around_transition(:include_failures => true) do |block|
+ @callbacks << :around_before_failure
+ block.call
+ @callbacks << :around_after_failure
+ end
+
+ @record = @model.new(:state => 'parked')
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
+ @result = @transition.perform
+ end
+
+ def test_should_not_be_successful
+ assert !@result
+ end
+
+ def test_should_not_change_current_state
+ assert_equal 'parked', @record.state
+ end
+
+ def test_should_not_save_record
+ assert @record.new_record?
+ end
+
+ def test_should_run_before_callbacks_and_after_callbacks_with_failures
+ assert_equal [:before, :around_before, :around_before_failure, :around_after_failure, :after_failure], @callbacks
+ end
+ end
+
+ class MachineWithFailedAfterCallbacksTest < BaseTestCase
+ def setup
+ @callbacks = []
+
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ @machine.state :parked, :idling
+ @machine.event :ignite
+ @machine.after_transition {@callbacks << :after_1; false}
+ @machine.after_transition {@callbacks << :after_2}
+ @machine.around_transition {|block| @callbacks << :around_before; block.call; @callbacks << :around_after}
+
+ @record = @model.new(:state => 'parked')
+ @transition = StateMachine::Transition.new(@record, @machine, :ignite, :parked, :idling)
+ @result = @transition.perform
+ end
+
+ def test_should_be_successful
+ assert @result
+ end
+
+ def test_should_change_current_state
+ assert_equal 'idling', @record.state
+ end
+
+ def test_should_save_record
+ assert !@record.new_record?
+ end
+
+ def test_should_still_run_further_after_callbacks
+ assert_equal [:around_before, :around_after, :after_1, :after_2], @callbacks
+ end
+ end
+
+ class MachineWithValidationsTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ @machine.state :parked
+
+ @record = @model.new
+ end
+
+ def test_should_invalidate_using_errors
+ @record.state = 'parked'
+
+ @machine.invalidate(@record, :state, :invalid_transition, [[:event, :park]])
+ assert_equal ['State cannot transition via "park"'], @record.errors.full_messages
+ end
+
+ def test_should_auto_prefix_custom_attributes_on_invalidation
+ @machine.invalidate(@record, :event, :invalid)
+
+ assert_equal ['State event is invalid'], @record.errors.full_messages
+ end
+
+ def test_should_clear_errors_on_reset
+ @record.state = 'parked'
+ @record.errors.add(:state, 'is invalid')
+
+ @machine.reset(@record)
+ assert_equal [], @record.errors.full_messages
+ end
+
+ def test_should_be_valid_if_state_is_known
+ @record.state = 'parked'
+
+ assert @record.valid?
+ end
+
+ def test_should_not_be_valid_if_state_is_unknown
+ @record.state = 'invalid'
+
+ assert !@record.valid?
+ assert_equal ['State is invalid'], @record.errors.full_messages
+ end
+ end
+
+ class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
+ def setup
+ @model = new_model do
+ alias_attribute :status, :state
+ end
+
+ @machine = StateMachine::Machine.new(@model, :status, :attribute => :state)
+ @machine.state :parked
+
+ @record = @model.new
+ end
+
+ def test_should_add_validation_errors_to_custom_attribute
+ @record.state = 'invalid'
+
+ assert !@record.valid?
+ assert_equal ['State is invalid'], @record.errors.full_messages
+
+ @record.state = 'parked'
+ assert @record.valid?
+ end
+ end
+
+ class MachineWithStateDrivenValidationsTest < BaseTestCase
+ def setup
+ @model = new_model do
+ attr_accessor :seatbealt
+ end
+
+ @machine = StateMachine::Machine.new(@model)
+ @machine.state :first_gear do
+ validates_presence_of :seatbelt, :key => :first_gear
+ end
+ @machine.state :second_gear do
+ validates_presence_of :seatbelt, :key => :second_gear
+ end
+ @machine.other_states :parked
+ end
+
+ def test_should_be_valid_if_validation_fails_outside_state_scope
+ record = @model.new(:state => 'parked', :seatbelt => nil)
+ assert record.valid?
+ end
+
+ def test_should_be_invalid_if_validation_fails_within_state_scope
+ record = @model.new(:state => 'first_gear', :seatbelt => nil)
+ assert !record.valid?
+ end
+
+ def test_should_be_valid_if_validation_succeeds_within_state_scope
+ record = @model.new(:state => 'second_gear', :seatbelt => true)
+ assert record.valid?
+ end
+ end
+
+ class MachineWithEventAttributesOnValidationTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ @machine.event :ignite do
+ transition :parked => :idling
+ end
+
+ @record = @model.new
+ @record.state = 'parked'
+ @record.state_event = 'ignite'
+ end
+
+ def test_should_fail_if_event_is_invalid
+ @record.state_event = 'invalid'
+ assert !@record.valid?
+ assert_equal ['State event is invalid'], @record.errors.full_messages
+ end
+
+ def test_should_fail_if_event_has_no_transition
+ @record.state = 'idling'
+ assert !@record.valid?
+ assert_equal ['State event cannot transition when idling'], @record.errors.full_messages
+ end
+
+ def test_should_be_successful_if_event_has_transition
+ assert @record.valid?
+ end
+
+ def test_should_run_before_callbacks
+ ran_callback = false
+ @machine.before_transition { ran_callback = true }
+
+ @record.valid?
+ assert ran_callback
+ end
+
+ def test_should_run_around_callbacks_before_yield
+ ran_callback = false
+ @machine.around_transition {|block| ran_callback = true; block.call }
+
+ @record.valid?
+ assert ran_callback
+ end
+
+ def test_should_persist_new_state
+ @record.valid?
+ assert_equal 'idling', @record.state
+ end
+
+ def test_should_not_run_after_callbacks
+ ran_callback = false
+ @machine.after_transition { ran_callback = true }
+
+ @record.valid?
+ assert !ran_callback
+ end
+
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
+ @model.class_eval do
+ attr_accessor :seatbelt
+ validates_presence_of :seatbelt
+ end
+
+ ran_callback = false
+ @machine.after_transition { ran_callback = true }
+
+ @record.valid?
+ assert !ran_callback
+ end
+
+ def test_should_not_run_around_callbacks_after_yield
+ ran_callback = false
+ @machine.around_transition {|block| block.call; ran_callback = true }
+
+ @record.valid?
+ assert !ran_callback
+ end
+
+ def test_should_not_run_around_callbacks_after_yield_with_failures_disabled_if_validation_fails
+ @model.class_eval do
+ attr_accessor :seatbelt
+ validates_presence_of :seatbelt
+ end
+
+ ran_callback = false
+ @machine.around_transition {|block| block.call; ran_callback = true }
+
+ @record.valid?
+ assert !ran_callback
+ end
+
+ def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_validation_fails
+ @model.class_eval do
+ attr_accessor :seatbelt
+ validates_presence_of :seatbelt
+ end
+
+ ran_callback = false
+ @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
+
+ @record.valid?
+ assert ran_callback
+ end
+
+ def test_should_run_after_callbacks_with_failures_enabled_if_validation_fails
+ @model.class_eval do
+ attr_accessor :seatbelt
+ validates_presence_of :seatbelt
+ end
+
+ ran_callback = false
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
+
+ @record.valid?
+ assert ran_callback
+ end
+ end
+
+ class MachineWithEventAttributesOnSaveTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ @machine.event :ignite do
+ transition :parked => :idling
+ end
+
+ @record = @model.new
+ @record.state = 'parked'
+ @record.state_event = 'ignite'
+ end
+
+ def test_should_fail_if_event_is_invalid
+ @record.state_event = 'invalid'
+ assert_equal false, @record.save
+ end
+
+ def test_should_fail_if_event_has_no_transition
+ @record.state = 'idling'
+ assert_equal false, @record.save
+ end
+
+ def test_should_be_successful_if_event_has_transition
+ assert_equal true, @record.save
+ end
+
+ def test_should_run_before_callbacks
+ ran_callback = false
+ @machine.before_transition { ran_callback = true }
+
+ @record.save
+ assert ran_callback
+ end
+
+ def test_should_run_before_callbacks_once
+ before_count = 0
+ @machine.before_transition { before_count += 1 }
+
+ @record.save
+ assert_equal 1, before_count
+ end
+
+ def test_should_run_around_callbacks_before_yield
+ ran_callback = false
+ @machine.around_transition {|block| ran_callback = true; block.call }
+
+ @record.save
+ assert ran_callback
+ end
+
+ def test_should_run_around_callbacks_before_yield_once
+ around_before_count = 0
+ @machine.around_transition {|block| around_before_count += 1; block.call }
+
+ @record.save
+ assert_equal 1, around_before_count
+ end
+
+ def test_should_persist_new_state
+ @record.save
+ assert_equal 'idling', @record.state
+ end
+
+ def test_should_run_after_callbacks
+ ran_callback = false
+ @machine.after_transition { ran_callback = true }
+
+ @record.save
+ assert ran_callback
+ end
+
+ def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
+ @model.class_eval do
+ validates_inclusion_of :state, :within => %w(first_gear)
+ end
+
+ ran_callback = false
+ @machine.after_transition { ran_callback = true }
+
+ begin; @record.save; rescue; end
+ assert !ran_callback
+ end
+
+ def test_should_run_after_callbacks_with_failures_enabled_if_fails
+ @model.class_eval do
+ validates_inclusion_of :state, :within => %w(first_gear)
+ end
+
+ ran_callback = false
+ @machine.after_transition(:include_failures => true) { ran_callback = true }
+
+ begin; @record.save; rescue; end
+ assert ran_callback
+ end
+
+ def test_should_not_run_around_callbacks_with_failures_disabled_if_fails
+ @model.class_eval do
+ validates_inclusion_of :state, :within => %w(first_gear)
+ end
+
+ ran_callback = false
+ @machine.around_transition {|block| block.call; ran_callback = true }
+
+ begin; @record.save; rescue; end
+ assert !ran_callback
+ end
+
+ def test_should_run_around_callbacks_after_yield
+ ran_callback = false
+ @machine.around_transition {|block| block.call; ran_callback = true }
+
+ @record.save
+ assert ran_callback
+ end
+
+ def test_should_run_around_callbacks_after_yield_with_failures_enabled_if_fails
+ @model.class_eval do
+ validates_inclusion_of :state, :within => %w(first_gear)
+ end
+
+ ran_callback = false
+ @machine.around_transition(:include_failures => true) {|block| block.call; ran_callback = true }
+
+ begin; @record.save; rescue; end
+ assert ran_callback
+ end
+ end
+
+ class MachineWithEventAttributesOnSaveBangTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ @machine.event :ignite do
+ transition :parked => :idling
+ end
+
+ @record = @model.new
+ @record.state = 'parked'
+ @record.state_event = 'ignite'
+ end
+
+ def test_should_fail_if_event_is_invalid
+ @record.state_event = 'invalid'
+ assert_raise(MongoMapper::DocumentNotValid) { @record.save! }
+ end
+
+ def test_should_fail_if_event_has_no_transition
+ @record.state = 'idling'
+ assert_raise(MongoMapper::DocumentNotValid) { @record.save! }
+ end
+
+ def test_should_be_successful_if_event_has_transition
+ assert_equal true, @record.save!
+ end
+
+ def test_should_run_before_callbacks
+ ran_callback = false
+ @machine.before_transition { ran_callback = true }
+
+ @record.save!
+ assert ran_callback
+ end
+
+ def test_should_run_before_callbacks_once
+ before_count = 0
+ @machine.before_transition { before_count += 1 }
+
+ @record.save!
+ assert_equal 1, before_count
+ end
+
+ def test_should_run_around_callbacks_before_yield
+ ran_callback = false
+ @machine.around_transition {|block| ran_callback = true; block.call }
+
+ @record.save!
+ assert ran_callback
+ end
+
+ def test_should_run_around_callbacks_before_yield_once
+ around_before_count = 0
+ @machine.around_transition {|block| around_before_count += 1; block.call }
+
+ @record.save!
+ assert_equal 1, around_before_count
+ end
+
+ def test_should_persist_new_state
+ @record.save!
+ assert_equal 'idling', @record.state
+ end
+
+ def test_should_persist_new_state
+ @record.save!
+ assert_equal 'idling', @record.state
+ end
+
+ def test_should_run_after_callbacks
+ ran_callback = false
+ @machine.after_transition { ran_callback = true }
+
+ @record.save!
+ assert ran_callback
+ end
+
+ def test_should_run_around_callbacks_after_yield
+ ran_callback = false
+ @machine.around_transition {|block| block.call; ran_callback = true }
+
+ @record.save!
+ assert ran_callback
+ end
+ end
+
+ class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
+ def setup
+ @superclass = new_model do
+ def persist
+ create_or_update
+ end
+ end
+ @model = Class.new(@superclass)
+ @machine = StateMachine::Machine.new(@model, :action => :persist)
+ @machine.event :ignite do
+ transition :parked => :idling
+ end
+
+ @record = @model.new
+ @record.state = 'parked'
+ @record.state_event = 'ignite'
+ end
+
+ def test_should_not_transition_on_valid?
+ @record.valid?
+ assert_equal 'parked', @record.state
+ end
+
+ def test_should_not_transition_on_save
+ @record.save
+ assert_equal 'parked', @record.state
+ end
+
+ def test_should_not_transition_on_save!
+ @record.save!
+ assert_equal 'parked', @record.state
+ end
+
+ def test_should_transition_on_custom_action
+ @record.persist
+ assert_equal 'idling', @record.state
+ end
+ end
+
+ class MachineWithScopesTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model)
+ @machine.state :parked, :first_gear
+ @machine.state :idling, :value => lambda {'idling'}
+ end
+
+ def test_should_create_singular_with_scope
+ assert @model.respond_to?(:with_state)
+ end
+
+ def test_should_only_include_records_with_state_in_singular_with_scope
+ parked = @model.create :state => 'parked'
+ idling = @model.create :state => 'idling'
+
+ assert_equal [parked], @model.with_state(:parked)
+ end
+
+ def test_should_create_plural_with_scope
+ assert @model.respond_to?(:with_states)
+ end
+
+ def test_should_only_include_records_with_states_in_plural_with_scope
+ parked = @model.create :state => 'parked'
+ idling = @model.create :state => 'idling'
+
+ assert_equal [parked, idling], @model.with_states(:parked, :idling)
+ end
+
+ def test_should_create_singular_without_scope
+ assert @model.respond_to?(:without_state)
+ end
+
+ def test_should_only_include_records_without_state_in_singular_without_scope
+ parked = @model.create :state => 'parked'
+ idling = @model.create :state => 'idling'
+
+ assert_equal [parked], @model.without_state(:idling)
+ end
+
+ def test_should_create_plural_without_scope
+ assert @model.respond_to?(:without_states)
+ end
+
+ def test_should_only_include_records_without_states_in_plural_without_scope
+ parked = @model.create :state => 'parked'
+ idling = @model.create :state => 'idling'
+ first_gear = @model.create :state => 'first_gear'
+
+ assert_equal [parked, idling], @model.without_states(:first_gear)
+ end
+ end
+
+ class MachineWithScopesAndOwnerSubclassTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model, :state)
+
+ @subclass = Class.new(@model)
+ @subclass_machine = @subclass.state_machine(:state) {}
+ @subclass_machine.state :parked, :idling, :first_gear
+ end
+
+ def test_should_only_include_records_with_subclass_states_in_with_scope
+ parked = @subclass.create :state => 'parked'
+ idling = @subclass.create :state => 'idling'
+
+ assert_equal [parked, idling], @subclass.with_states(:parked, :idling)
+ end
+
+ def test_should_only_include_records_without_subclass_states_in_without_scope
+ parked = @subclass.create :state => 'parked'
+ idling = @subclass.create :state => 'idling'
+ first_gear = @subclass.create :state => 'first_gear'
+
+ assert_equal [parked, idling], @subclass.without_states(:first_gear)
+ end
+ end
+
+ class MachineWithComplexPluralizationScopesTest < BaseTestCase
+ def setup
+ @model = new_model
+ @machine = StateMachine::Machine.new(@model, :status)
+ end
+
+ def test_should_create_singular_with_scope
+ assert @model.respond_to?(:with_status)
+ end
+
+ def test_should_create_plural_with_scope
+ assert @model.respond_to?(:with_statuses)
+ end
end
end

0 comments on commit ba8fdac

Please sign in to comment.
Something went wrong with that request. Please try again.