# Notes: # # create/update/delete by guest # create/update/delete by non-permitted user # create/update/delete by permitted user # # All of the above for has-many related records # All of the above for has-many through related records # All of the above for belongs-to related records # # a permitted user can't create/update/delete any of the above related records without :accessible require 'test/unit' require File.join(File.dirname(__FILE__), "models/models.rb") require 'shoulda' Models.init class PermissionsTest < Test::Unit::TestCase def assert_create_prevented(*models) counts = models.*.count assert_raises(Hobo::PermissionDeniedError) { yield } assert_equal(counts, models.*.count) end def assert_created(*args) expectations = {} args.in_groups_of(2) {|model, expected| expectations[model] = [model.count, expected]} yield expectations.each_pair do |model, nums| count = model.count delta = model.count - nums.first assert_equal(nums.last, delta, "Expected #{nums.last} #{model.name.pluralize} to be created, but #{delta} were") end end def assert_change_prevented(record, field) was = record.send(field) assert_raises(Hobo::PermissionDeniedError) { yield } record.reload assert_equal was, record.send(field) end def assert_deleted(record) assert_raises(ActiveRecord::RecordNotFound) { record.class.find(record.id) } end def assert_destroy_prevented(*records) assert_raises(Hobo::PermissionDeniedError) { yield } records.each { |record| assert record.class.find(record.id) } end def existing_recipe(user=nil) user ||= User.new :name => "existing recipe owner" Recipe.create :name => "existing recipe", :user => user end context "The permission system" do context "with a guest user acting" do setup { @guest = Hobo::Guest.new } should "prevent creation of a recipe" do assert_create_prevented(Recipe) { Recipe.user_create! @guest, :name => "test recipe" } end should "prevent update of an exiting recipe" do r = existing_recipe assert_change_prevented(r, :name) { r.user_update_attributes!(@guest, :name => "pwned!") } end should "prevent deletion of an existing recipe" do r = existing_recipe assert_destroy_prevented(r) { r.user_destroy(@guest) } end end context "with a regular user acting" do setup { @user = User.create! :name => "regular" } # --- Permitted basic actions --- # should "allow creation of a recipe" do assert_created Recipe, 1 do @recipe = Recipe.user_create! @user, :name => "test recipe" end assert_equal(@user, @recipe.user) end should "allow update of a recipe (owned by the acting user)" do r = existing_recipe(@user) r.user_update_attributes!(@user, :name => "new name") r.reload assert_equal("new name", r.name) end should "allow deletion of a recipe (owned by the acting user)" do r = existing_recipe(@user) r.user_destroy(@user) assert_deleted r end # ---- Prevented basic actions --- # should "prevent creation of a recipe owned by another user" do assert_create_prevented(Recipe) { Recipe.user_create! @user, :name => "test recipe", :user => User.new } end context "on a recipe owned by another user" do setup { @r = existing_recipe(User.create!(:name => "a n other")) } should "prevent update" do assert_change_prevented(@r, :name) { @r.user_update_attributes!(@user, :name => "pwned!") } end should "prevent deletion" do assert_destroy_prevented(@r) { @r.user_destroy(@user) } end end # --- Prevented actions on has_many related records --- # # (sanity check) should "prevent creation of images without a recipe" do assert_create_prevented(Image) { Image.user_create! @user, :name => "test image" } end should "prevent creation of images along with a recipe" do # Only paid up users can assert_create_prevented(Recipe, Image) do Recipe.user_create! @user, :name => "test recipe", :images => { '0' => { :name => "image 1" }, '1' => { :name => "image 2" } } end end context "on an another's recipe with images" do setup do user = User.create :name => "a n other" @r = Recipe.create! :name => "test recipe", :user => user @i1 = @r.images.create(:name => "image 1") @i2 = @r.images.create(:name => "image 2") end should "prevent update of those images" do assert_change_prevented(@i1, :name) do @r.user_update_attributes!(@user, :images => { '0' => { :id => @i1.id, :name => "pwned!" } }) end end should "prevent deletion of those images" do assert_destroy_prevented(@i1, @i2) { @r.user_update_attributes!(@user, :images => { }) } end end # Note: permitted actions on has-many associated records are in the paid-up user section # Tis the tyranny of the dominant hiearchy : ) # --- Permitted actions on has_many :through relationships --- # context "" do setup do @c1 = User.create :name => "Collaborator 1" @c2 = User.create :name => "Collaborator 2" end should "allow adding collaborators when creating a recipe" do @r = Recipe.user_create! @user, :name => "my recipe", :collaborators => { '0' => "@#{@c1.id}", '1' => "@#{@c2.id}" } assert_equal(@r.collaborators, [@c1, @c2]) end should "allow adding collaborators to a recipe" do @r = existing_recipe(@user) @r.user_update_attributes! @user, :collaborators => { '0' => "@#{@c1.id}", '1' => "@#{@c2.id}" } assert_equal(@r.collaborators, [@c1, @c2]) end should "allow removing collaborators from a recipe" do @r = existing_recipe(@user) @r.collaborators << @c1 @r.collaborators << @c2 join1, join2 = @r.collaboratorships # sanity check @r.reload assert_equal(@r.collaborators.length, 2) @r.user_update_attributes! @user, :collaborators => {} assert_equal(@r.collaborators.length, 0) assert_deleted(join1) assert_deleted(join2) end # --- Prevented actions on has_many :through relationships --- # context "on another's recipe" do setup do other_user = User.create :name => "a n other" @r = existing_recipe(other_user) end should "prevent adding collaborators" do assert_create_prevented(Collaboratorship) do @r.user_update_attributes! @user, :collaborators => { '0' => "@#{@c1.id}", '1' => "@#{@c2.id}" } end end should "prevent removing collaborators" do c1 = User.create :name => "Collaborator 1" c2 = User.create :name => "Collaborator 2" @r.collaborators << c1 @r.collaborators << c2 join1, join2 = @r.collaboratorships assert_destroy_prevented(join1, join2) do @r.user_update_attributes! @user, :collaborators => {} end end end end # --- Permitted actions on a belongs_to related record --- # should "allow creation of a code-example along with a recipe" do assert_created(Recipe, 1, CodeExample, 1) do @r = Recipe.user_create! @user, :name => "recipe with code example", :code_example => { :filename => "code.zip" } end assert_equal("code.zip", @r.code_example.filename) end should "allow creation of a code-example for an existing recipe" do @r = existing_recipe(@user) assert_created(CodeExample, 1) do @r.user_update_attributes! @user, :code_example => { :filename => "code.zip" } end assert_equal("code.zip", @r.code_example.filename) end should "allow the code example related to a recipe to be changed" do r = existing_recipe @user c1 = CodeExample.create! :filename => "exmaple1.zip" c2 = CodeExample.create! :filename => "exmaple2.zip" r.code_example = c1 r.save r.user_update_attributes! @user, :code_example => "@#{c2.id}" assert_equal(c2, r.code_example) end context "on an existing recipe with a code example" do setup do @ce = CodeExample.create! :filename => "exmaple.zip" @r = Recipe.create :name => "existing recipe", :user => @user, :code_example => @ce end should "allow update of the code-example" do # To update the exsting target of a belongs_to association, the id must be included in the hash # It is an error to provide any id other than that of the existing target @r.user_update_attributes! @user, :code_example => { :id => @ce.id, :filename => "changed.zip" } assert_equal("changed.zip", @r.code_example.filename) assert_equal(@ce, @r.code_example) end should "allow removal of the code-example" do @r.user_update_attributes! @user, :code_example => { } @r.reload assert_nil @r.code_example # Note - the code-example is not deleted from the database end end # --- Prevented actions on a belongs_to related record --- # context "on another's recipe" do setup { @r = existing_recipe } should "prevent creation of a code-example" do assert_create_prevented(CodeExample) do @r.user_update_attributes! @user, :code_example => { :filename => "code.zip" } end end context "with a code example" do setup do @ce = @r.code_example = CodeExample.create!(:filename => "exmaple.zip") @r.save end should "prevent update of the code-example" do assert_change_prevented(@ce, :filename) do @r.user_update_attributes! @user, :code_example => { :filename => "changed.zip" } end end should "prevent removal of the code-example" do assert_change_prevented(@r, :code_example_id) do @r.user_update_attributes! @user, :code_example => {} end end end end # --- Actions on non-accesible has_many associations --- # # When associations are not declared :accesssible => true, attemting to update them # causes a type-mismatch should "prevent creation of comments via a recipe" do r = existing_recipe assert_raises(ActiveRecord::AssociationTypeMismatch) do r.user_update_attributes! @user, :comments => { '0' => { :body => "dodgy comment"} } end end should "prevent update of comments via their recipe" do r = existing_recipe c = r.comments.create :body => "My nice comment" assert_raises(ActiveRecord::AssociationTypeMismatch) do r.user_update_attributes! @user, :comments => { '0' => { :id => c.id, :body => "dodgy comment"} } end end should "prevent deletion of comments via their recipe" do r = existing_recipe c = r.comments.create :body => "My nice comment" assert_destroy_prevented(c) do r.user_update_attributes! @user, :comments => { } end end # --- Actions on non-accessible belongs_to associations --- # should "prevent creation of a recipe when creating a comment" do count = Recipe.count assert_raises(ActiveRecord::AssociationTypeMismatch) do Comment.user_create! @user, :body => "my comment", :recipe => { :name => "I created a recipe!" } end assert_equal(count, Recipe.count) end should "prevent update of a recipe via one of its comments" do r = existing_recipe c = r.comments.create :body => "My comment" assert_raises(ActiveRecord::AssociationTypeMismatch) do c.user_update_attributes! @user, :recipe => { :id => r.id, :name => "pwned!" } end assert_equal("existing recipe", r.name) end should "prevent deletion of a recipe via one of its comments" do r = existing_recipe c = r.comments.create :body => "My comment" assert_raises(ActiveRecord::AssociationTypeMismatch) do c.user_update_attributes! @user, :recipe => { } end assert Recipe.find(r.id) end end context "with a paid up user acting" do setup { @user = User.create! :name => "paid up user", :paid_up => true } should "prevent direct creation of images" do # images must belong to a recipe assert_create_prevented(Image) { Image.user_create! @user, :name => "test image" } end # --- Permitted actions on has_many related records --- # should "allow creation of images along with a recipe" do assert_created Recipe, 1, Image, 2 do @recipe = Recipe.user_create! @user, :images => { '0' => { :name => "image 1" }, '1' => { :name => "image 2" } } assert_equal(2, @recipe.images.length) i1, i2 = @recipe.images assert_equal("image 1", i1.name) assert_equal("image 2", i2.name) assert_equal(@recipe, i1.recipe) assert_equal(@recipe, i2.recipe) end end context "on an existing recipe with images" do setup do @recipe = Recipe.create! :name => "recipe with images", :user => @user @i1 = @recipe.images.create! :name => "image 1" @i2 = @recipe.images.create! :name => "image 2" end should "allow creation of images when updating a recipe" do @recipe.user_update_attributes! @user, :images => { '0' => { :id => @i1.id }, '1' => { :id => @i2.id }, '2' => { :name => "new image" } } @recipe.reload assert_equal(3, @recipe.images.length) assert_equal("image 1", Image.find(@i1.id).name) assert_equal("image 2", Image.find(@i2.id).name) assert_equal("new image", @recipe.images[2].name) end should "allow updates to images when updating a recipe" do @recipe.user_update_attributes! @user, :images => { '0' => { :id => @i1.id, :name => "new name" }, '1' => { :id => @i2.id } } @recipe.reload assert_equal(2, @recipe.images.length) assert_equal("new name", Image.find(@i1.id).name) assert_equal("new name", @recipe.images[0].name) end should "allow deletion of images when updating a recipe" do @recipe.user_update_attributes! @user, :images => { '0' => { :id => @i1.id } } @recipe.reload assert_equal(1, @recipe.images.length) assert_deleted(@i2) end end end end end