moonmaster9000 / dupe

BDD your (ActiveResource compatible) services from the client-side, before they exist.
62 stars 17 forks source link

Custom mocks are only called on the *first* request to that URL, not subsequent ones #9

Closed nzifnab closed 14 years ago

nzifnab commented 14 years ago

I'm trying to use dupe for cucumber (and soon rspec, but i'll do cucumber for now) but am running into some problems with custom mock declarations. For instance, let's say I run this in the script/console command window:

require 'dupe'

Post %r{/sessions\.xml} do |params|
  user = Dupe.find(:user){|u| u.email == params["email"] && u.encrypted_password == params["password"]}
  raise ActiveResource::UnauthorizedAccess.new(DupeUnauthorizedAccess.new) if user.nil?
  Dupe.create(:session, :id => user.id)
end

# This is just so I have something with a 'code' method without having to mock a full 401 response object for that ActiveResource::UnauthorizedAccess above
class DupeUnauthorizedAccess
  def code
    401
  end
end

Now here's what happens: a Post request should (as I see it) always go through this method and evaluate what to return as appropriate. As long as the posted params email and password match up with a dupe user's email and encrypted_password respectively, then a Session object should be returned with an id identical to the user found. If no such user is found (the email or password are incorrect), then perform an ActiveResource::UnauthorizedAccess error.

This works quite flawlessly...on all calls to Session.create(options) up to and including the first successful one, but any calls after that skip the Post method I defined entirely. Continue in that same console session with this code to see what I mean:

Dupe.create(:user, :email => 'my_user@example.com', :encrypted_password => 'password')

Session.create(:email => 'my_user@example.com', :password => 'bad_password')

This should raise an ActiveResource::UnauthorizedAccess error. It does for me, and that's what we want (the passwords didn't match, see). Now, let us continue.

Session.create(:email => 'my_user@example.com', :password => 'password')

This one was our first 'successful' response from that post method. We can even see that the only thing it made in our sessions model was an id:

Dupe.find(:sessions) => [<#Duped::Session id=1>]

However, now any calls to create a session will succeed regardless of whether or not the password matches. We will do the exact same call we did a couple of lines ago that did raise an error (we'll print our users out first just to make sure):

>> Dupe.find(:users)
=> [<#Duped::User email="my_user@example.com" encrypted_password="password" id=1>]

>> Session.create(:email => 'my_user@example.com', :password => 'bad_password')
=> #<Session:0x20aa3f0 @attributes={"id"=>2, "password"=>"bad_password", "email"=>"my_user@example.com"}, @prefix_options={}>

From our printed users, we can see that the password was 'password. Then we run the create method (which should be sending to that Post mock, no?), which should be giving us that same ActiveResource::UnauthorizedAccess error that we saw earlier...but it passed instead. You can see the extent of the failure in the sessions find:

>> Dupe.find(:sessions)
=> [<#Duped::Session id=1>, <#Duped::Session email="my_user@example.com" password="bad_password" id=2>]

Our first one is there, but the second one is too somehow...and with more than just an id (and with the wrong id at that)

I've put some raises all over the Post method to see if it got in there at all, but as long as the Post succeeds once, any subsequent calls to it regardless of the params will always create a new session, with not necessarily the properties that I want.

moonmaster9000 commented 14 years ago

hi nzifnab, before defining your Post custom intercept, define your Dupe model factory "Dupe.define :session", and everything should work as your expecting it to.

require 'dupe'
Dupe.define :session
Post %r{/sessions\.xml}# etc....
moonmaster9000 commented 14 years ago

fyi - the reason this is happening: Dupe.create :session will first look to see if you already have a Dupe :session factory defined. if not, it will define one (an empty one). when Dupe.define gets called, it creates generic GET (for find by id, and find :all), POST, PUT, and DELETE mocks for the resource factory being defined. so, first you had created a custom intercept mock for Posting to sessions. the first time it got called and successfully fell all the way through to Dupe.create :session, it discovered there was no Dupe model factory definition, so it created one. when it created it, the generic POST intercept mock got added to the front of the list, meaning it took precedence over your custom mock.

nzifnab commented 14 years ago

Thanks a ton for the explanation and super fast response :) (it worked as expected when I did what you mentioned).

This could probably be explained in the documentation a little better because I wasn't able to find this information. I was going to suggest a warning if you define a VERB call mock without a related definition, but when you're doing a verb mock you don't necessarily know what object it's supposed to be related to so that might not be a good idea.

Either way, thanks a ton that helped a lot :)

moonmaster9000 commented 14 years ago

no probs. i will update the docs ASAP. thanks for using Dupe! tell all your friends about it :-)