Sunday, October 21st, 2007...11:07 am

Integration testing with rfacebook - solving the session problem


Jump to Comments

It’s always very important to write integration tests. Hell, it’s not just very important, it’s critical. Some systems make it easier than others to do so. Rails, for instance, provides many, many hooks and bits of framework to make your apps easily testable. Facebook, on the other hand, is a major pain in the ass.

One of the many ways in which Facebook (or more precisely, rfacebook) makes it difficult to write integration tests, is the session problem. Here’s how it goes:

1. You write your integration test, including a fresh set of fbparams to make the request pass through rfacebook’s require_facebook_install
2. It works, it passes, you’re happy and you buy everyone a round of drinks.
3. Some time later, for no apparent reason, your integration breaks and seems to think that your test fbparams don’t actually represent a valid user.
4. You shoot your coworkers after spending 3 hours trying to find what’s changed in your code to make it fail.

Now, we don’t want that, do we?

Symptoms

This is often visible through a spec failing with an error message something like:

  Spec::Expectations::ExpectationNotMetError: expected /Test message/, got <html><body>You are being <a href=\”http://www.example.com/facebook\”>redirected</a>.</body></html>

After tearing your hair out just enough to make you look like a Benedictine monk, you’ll check the test.log file and find something like this:

Processing FacebookController#create_message_profile (for 127.0.0.1 at 2007-10-21 10:47:54) [POST]
  Session ID: 30f78183e4c851ef6af04c479bb34224
  Parameters: {message=>{contents=>}, post_form_id=>8a47588a90e2b89399aa4621eedaf9b4, fb_sig_time=>1192733662.7756, fb_sig_is_mockajax=>1, fb_sig=>460d0402bf9475b18963ca8417a24a93, action=>create_message_profile, fb_sig_session_key=><snip>, fb_sig_profile=><snip>, controller=>facebook, fb_sig_expires=>0, fb_sig_added=>1, fb_sig_api_key=><snip>, fb_sig_user=><snip>, fb_sig_profile_update_time=>1192638467}
Can only render or redirect once per action
/Users/<snip>/Sites/<snip>/vendor/rails/actionpack/lib/action_controller/base.rb:714:in render_with_no_layout
/Users/<snip>/Sites/<snip>/vendor/rails/actionpack/lib/action_controller/layout.rb:256:in render_without_benchmark
/Users/<snip>/Sites/<snip>/vendor/rails/actionpack/lib/action_controller/benchmarking.rb:50:in render

At this point, you should be just about ready to go postal. If you are fortunate to work from home, like me, however, you don’t have any coworkers to shoot. This leaves you with only one option: figuring out what the hell is going on in there.

You might be tempted to try scraping a new set of test parameters from a “real” request to your system. If you do so, you’ll find that works. If you then play with which field exactly causes it to work again, you’ll find two fields must be updated in your test data for your integration test to work again: fb_sig and fb_sig_time. Since those fields are not actually required to create a new session, you might be inclined at this point to curse rfacebook. And, in a sense, you’d be right.

What’s actually happening in the guts of rfacebook is this: rfacebook not only checks that your session is valid. Before it even bothers doing that, it checks that your session parameters are consistent with each other. One of the checks is to see whether fb_sig_time is less than 48 hours ago. If it isn’t, ie if your beautiful integration test has been running for a staggering two days, it will wipe all the fb_sig params, right away - which obviously causes issue when the rest of rfacebook tries to determine whether a user is logged in. This is what causes the redirect.

Here’s the guilty method:

    # Function: get_fb_sig_params
    #   Returns the fb_sig params from Hash that has all request params.  Hash is empty if the
    #   signature was invalid.
    #
    # Parameters:
    #   originalParams - a Hash that contains the fb_sig_* params (i.e. Rails params)
    #
    def get_fb_sig_params(originalParams)

      # setup
      timeout = 48*3600
      prefix = fb_sig_

      # get the params prefixed by “fb_sig_” (and remove the prefix)
      sigParams = {}
      originalParams.each do |k,v|
        oldLen = k.length
        newK = k.sub(prefix, )
        if oldLen != newK.length
          sigParams[newK] = v
        end
      end

      # handle invalidation
      if (timeout and (sigParams[time].nil? or (Time.now.to_i - sigParams[time].to_i > timeout.to_i)))
        # invalidate if the timeout has been reached
        #log_debug “** RFACEBOOK(GEM) - fbparams is empty because the signature was timed out”
        sigParams = {}
      end

      # check that the signatures match
      expectedSig = originalParams[fb_sig]
      if !(sigParams and expectedSig and generate_signature(sigParams, @api_secret) == expectedSig)
        # didn’t match, empty out the params
        #log_debug “** RFACEBOOK(GEM) - fbparams is empty because the signature did not match”
        sigParams = {}
      end

      return sigParams

    end

Now, personally, I think that’s appalling behaviour for any code. It’s quietly swallowing one issue that it finds, “safe” in the knowledge that some other bit will break later because of this. In the realm of trying to stop developers from figuring out why their code breaks, this is pretty clever. Matt Pizzimenti must have a special deal with arms dealers located near technology hubs.

The solution

Now that we’ve found this awful piece of code, what can we do about it? We don’t want to just hack the gem and remove it, because god knows what might happen then. Who knows what other part of the code might blow up when we make such changes. Adding a special parameter to make it ignore fb_sig and fb_sig_time when that parameter is present would be a security issue. And anyway, the spirit of the code is right (checking session validity is a good thing), it’s just the implementation which sucks.

Turns out Ruby, with its wonderful dynamic aptitudes, provides more than enough rope to quickly hang rfacebook by the ear. You can overwrite that method from within your own code. This means that your integration tests will pass, and that your fixing code will never be overwritten by an rfacebook gem update - although it *is* possible that it would cause weird errors should Matt decide to change that method and move that check about. Hopefully he would make a specific announcement about that when releasing the update though.

To solve this issue, just add the following to your spec/story helper:

module RFacebook

  class FacebookWebSession
    def get_fb_sig_params(originalParams)

      # setup
      timeout = 48*3600
      prefix = fb_sig_

      # get the params prefixed by “fb_sig_” (and remove the prefix)
      sigParams = {}
      originalParams.each do |k,v|
        oldLen = k.length
        newK = k.sub(prefix, )
        if oldLen != newK.length
          sigParams[newK] = v
        end
      end

      # # handle invalidation
      # if (timeout and (sigParams[”time”].nil? or (Time.now.to_i - sigParams[”time”].to_i > timeout.to_i)))
      #   # invalidate if the timeout has been reached
      #   #log_debug “** RFACEBOOK(GEM) - fbparams is empty because the signature was timed out”
      #   sigParams = {}
      # end
      #
      # # check that the signatures match
      # expectedSig = originalParams[”fb_sig”]
      # if !(sigParams and expectedSig and generate_signature(sigParams, @api_secret) == expectedSig)
      #   # didn’t match, empty out the params
      #   #log_debug “** RFACEBOOK(GEM) - fbparams is empty because the signature did not match”
      #   sigParams = {}
      # end

      return sigParams

    end
  end
end

Now you can have any fb_sig_time and fb_sig you want in your integration tests. Best part is, it will only affect your tests - your production code will continue to check sessions correctly.

[?]
Share

Leave a Reply

Close
E-mail It