Skip to content

Instantly share code, notes, and snippets.

@endymion
Last active May 6, 2025 00:21

Revisions

  1. endymion revised this gist Jun 20, 2013. 5 changed files with 177 additions and 84 deletions.
    6 changes: 4 additions & 2 deletions contact.rb
    Original file line number Diff line number Diff line change
    @@ -3,8 +3,10 @@ class Contact < ActiveRecord::Base
    ...

    def after_create
    Resque.enqueue(Hook, self.class.name, self.id)
    # To trigger directly without Resque: Hook.trigger('new_contact', self)
    if Hook.hooks_exist?('new_contact', self)
    Resque.enqueue(Hook, self.class.name, self.id)
    # To trigger directly without Resque: Hook.trigger('new_contact', self)
    end
    end

    end
    37 changes: 37 additions & 0 deletions contact_test.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,37 @@
    class ContactTest < ActiveSupport::TestCase

    ...

    require 'fakeweb'
    def test_trigger_rest_hook_on_contact_creation
    account = Factory(:account)
    subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"

    hook = Hook.create(
    {
    "event" => "new_contact",
    "account_id" => account.id,
    "subscription_url" => subscription_url,
    "target_url" => target_url
    }
    )

    FakeWeb.register_uri(
    :post,
    target_url,
    :body => 'irrelevant',
    :status => ['200', 'Triggered']
    )

    contact = Factory(:contact, :account => account,
    :first => 'Ryan', :last => 'Porter', :email => 'xyz@venuedriver.com')

    # Simulate a Resque worker.
    Hook.perform('Contact', contact.id)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal HookEncoder.encode(contact), FakeWeb.last_request.body
    end

    end
    47 changes: 35 additions & 12 deletions hook.rb
    Original file line number Diff line number Diff line change
    @@ -1,22 +1,27 @@
    require 'resque-retry'

    class Hook < ActiveRecord::Base
    attr_accessible :event, :account_id, :subscription_url, :target_url
    validates_presence_of :event, :account_id, :subscription_url, :target_url

    # Looks for an appropriate REST hook that matches the record, and triggers the hook if one exists.
    def self.trigger(event, record)
    hooks = Hook.find(:all, :conditions => {
    :event => event,
    :account_id => record.account_id,
    })
    hooks = self.hooks(event, record)
    return if hooks.empty?

    unless Rails.env.development?
    hook = hooks.first
    Rails.logger.info "Triggering REST hook: #{hook.inspect}"
    RestClient.post(hook.target_url, record.to_json) do |response, request, result|
    if response.code.eql? 410
    Rails.logger.info "Destroying REST hook because of 410 response: #{hook.inspect}"
    hook.destroy
    # Trigger each hook if there is more than one for an account, which can happen.
    hooks.each do |hook|
    # These use puts instead of Rails.logger.info because this happens from a Resque worker.
    puts "Triggering REST hook: #{hook.inspect}"
    puts "REST hook event: #{event}"
    encoded_record = HookEncoder.encode(record)
    puts "REST hook record: #{encoded_record}"
    RestClient.post(hook.target_url, encoded_record) do |response, request, result|
    if response.code.eql? 410
    puts "Destroying REST hook because of 410 response: #{hook.inspect}"
    hook.destroy
    end
    end
    end
    end
    @@ -25,10 +30,28 @@ def self.trigger(event, record)
    # This method is called by a Resque worker. Resque stores the record's class and ID, and the
    # Resque worker provides those values as parameters to this method.
    def self.perform(klass, id)
    event = "new_#{klass.to_s.downcase}"
    # puts "Performing REST hook Resque job: #{klass} #{id}"
    event = "new_#{klass.to_s.underscore}"
    record = klass.camelize.constantize.find(id)
    Hook.trigger(event, record)
    end
    @queue = :rest_hook
    extend Resque::Plugins::Retry
    @retry_limit = 3
    @retry_delay = 5

    # Returns all hooks for a given event and account.
    def self.hooks(event, record)
    Hook.find(:all, :conditions => {
    :event => event,
    :account_id => record.account_id,
    })
    end

    # Tests whether any hooks exist for a given event and account, for deciding whether or not to
    # enqueue Resque jobs.
    def self.hooks_exist?(event, record)
    self.hooks(event, record).size > 0
    end

    end
    end
    35 changes: 31 additions & 4 deletions hook_test.rb
    Original file line number Diff line number Diff line change
    @@ -14,7 +14,7 @@ def setup
    @contact = Factory(:contact, :account => @account,
    :first => 'Ryan', :last => 'Porter', :email => 'xyz@venuedriver.com')

    hook = Hook.create(
    @hook = Hook.create(
    {
    "event" => "new_contact",
    "account_id" => @account.id,
    @@ -24,6 +24,33 @@ def setup
    )
    end

    def test_hooks_exist
    assert_equal true, Hook.hooks_exist?('new_contact', @contact)

    Hook.destroy_all

    assert_equal false, Hook.hooks_exist?('new_contact', @contact)
    end

    def test_hooks
    hooks = Hook.hooks('new_contact', @contact)
    assert_equal 1, hooks.size
    assert_equal @hook, hooks.first

    second_hook = Hook.create(
    {
    "event" => "new_contact",
    "account_id" => @account.id,
    "subscription_url" => @subscription_url,
    "target_url" => @target_url
    }
    )

    hooks = Hook.hooks('new_contact', @contact)
    assert_equal 2, hooks.size
    assert_equal second_hook, hooks.last
    end

    def test_trigger
    FakeWeb.register_uri(
    :post,
    @@ -36,7 +63,7 @@ def test_trigger
    Hook.trigger('new_contact', @contact)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal @contact.to_json, FakeWeb.last_request.body
    assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body
    assert_equal 1, Hook.count # The hook should not have been deleted.
    end

    @@ -52,7 +79,7 @@ def test_trigger_remove_hook_on_410_response
    Hook.trigger('new_contact', @contact)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal @contact.to_json, FakeWeb.last_request.body
    assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body

    assert_equal 0, Hook.count # The 410 response should trigger removal of the hook.
    end
    @@ -71,7 +98,7 @@ def test_resque_background_job
    Hook.perform(Contact.name, @contact.id)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal @contact.to_json, FakeWeb.last_request.body
    assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body
    assert_equal 1, Hook.count # The hook should not have been deleted.
    end

    136 changes: 70 additions & 66 deletions hooks_controller_test.rb
    Original file line number Diff line number Diff line change
    @@ -1,101 +1,105 @@
    require 'test_helper'
    require 'fakeweb'

    class HooksControllerTest < ActionController::TestCase
    class HookTest < ActiveSupport::TestCase

    def setup
    @account = Factory(:account)
    @user = Factory(:user, :username => 'username') # Password: "password", by default.
    @staff = Factory(:staff, :account => @account, :user => @user)
    end

    def test_subscribe_requires_authentication
    post :create,
    @subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    @target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"

    # Create the record before the hook is created or else the after_create Active Model hook
    # will trigger the REST hook prematurely during testing.
    @contact = Factory(:contact, :account => @account,
    :first => 'Ryan', :last => 'Porter', :email => 'xyz@venuedriver.com')

    @hook = Hook.create(
    {
    "event" => "new_contact",
    "subscription_url" => "whatever",
    "target_url" => "whatever"
    "account_id" => @account.id,
    "subscription_url" => @subscription_url,
    "target_url" => @target_url
    }

    assert_redirected_to :controller => 'public/signin', :action => 'signin'
    )
    end

    def test_subscribe
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")
    def test_hooks_exist
    assert_equal true, Hook.hooks_exist?('new_contact', @contact)

    Hook.destroy_all

    subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    assert_equal false, Hook.hooks_exist?('new_contact', @contact)
    end

    post :create,
    def test_hooks
    hooks = Hook.hooks('new_contact', @contact)
    assert_equal 1, hooks.size
    assert_equal @hook, hooks.first

    second_hook = Hook.create(
    {
    "event" => "new_contact",
    "account_id" => @account.id,
    "subscription_url" => subscription_url,
    "target_url" => target_url
    "subscription_url" => @subscription_url,
    "target_url" => @target_url
    }
    )

    assert_response 201
    assert_equal 1, Hook.count

    hook = Hook.last
    assert_equal %Q[{"id":#{hook.id}}], @response.body
    assert_equal "new_contact", hook.event
    assert_equal @account.id, hook.account_id.to_i
    assert_equal subscription_url, hook.subscription_url
    assert_equal target_url, hook.target_url
    hooks = Hook.hooks('new_contact', @contact)
    assert_equal 2, hooks.size
    assert_equal second_hook, hooks.last
    end

    def test_subscribe_error
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")
    def test_trigger
    FakeWeb.register_uri(
    :post,
    @target_url,
    :body => 'irrelevant',
    :status => ['200', 'Triggered']
    )
    FakeWeb.allow_net_connect = false

    post :create, nil
    Hook.trigger('new_contact', @contact)

    assert_response 500
    assert_equal 0, Hook.count
    assert_equal "POST", FakeWeb.last_request.method
    assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body
    assert_equal 1, Hook.count # The hook should not have been deleted.
    end

    def test_unsubscribe_rest
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")

    hook = Hook.create(
    :event => "new_contact",
    :account_id => @account.id,
    :subscription_url => 'whatever',
    :target_url => 'whatever'
    def test_trigger_remove_hook_on_410_response
    FakeWeb.register_uri(
    :post,
    @target_url,
    :body => 'irrelevant',
    :status => ['410', 'Danger, Will Robinson!']
    )
    assert_equal 1, Hook.count

    post :destroy,
    {
    :id => hook.id.to_s,
    :subscription_url => 'whatever'
    }
    FakeWeb.allow_net_connect = false

    assert_response 200
    assert_equal 0, Hook.count
    end
    Hook.trigger('new_contact', @contact)

    def test_unsubscribe_the_hacky_way
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")
    assert_equal "POST", FakeWeb.last_request.method
    assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body

    subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    assert_equal 0, Hook.count # The 410 response should trigger removal of the hook.
    end

    Hook.create(
    :event => "new_contact",
    :account_id => @account.id,
    :subscription_url => subscription_url,
    :target_url => target_url
    def test_resque_background_job
    FakeWeb.register_uri(
    :post,
    @target_url,
    :body => 'irrelevant',
    :status => ['200', 'Triggered']
    )
    assert_equal 1, Hook.count
    FakeWeb.allow_net_connect = false

    post :destroy,
    {
    "subscription_url" => subscription_url
    }
    # A Resque worker will normally do this, which should have the same effect as when the hook
    # is manually triggered in the test_trigger test.
    Hook.perform(Contact.name, @contact.id)

    assert_response 200
    assert_equal 0, Hook.count
    assert_equal "POST", FakeWeb.last_request.method
    assert_equal HookEncoder.encode(@contact), FakeWeb.last_request.body
    assert_equal 1, Hook.count # The hook should not have been deleted.
    end


    end
  2. endymion revised this gist Jun 18, 2013. 1 changed file with 10 additions and 0 deletions.
    10 changes: 10 additions & 0 deletions contact.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    class Contact < ActiveRecord::Base

    ...

    def after_create
    Resque.enqueue(Hook, self.class.name, self.id)
    # To trigger directly without Resque: Hook.trigger('new_contact', self)
    end

    end
  3. endymion revised this gist Jun 18, 2013. 2 changed files with 35 additions and 3 deletions.
    9 changes: 9 additions & 0 deletions hook.rb
    Original file line number Diff line number Diff line change
    @@ -22,4 +22,13 @@ def self.trigger(event, record)
    end
    end

    # This method is called by a Resque worker. Resque stores the record's class and ID, and the
    # Resque worker provides those values as parameters to this method.
    def self.perform(klass, id)
    event = "new_#{klass.to_s.downcase}"
    record = klass.camelize.constantize.find(id)
    Hook.trigger(event, record)
    end
    @queue = :rest_hook

    end
    29 changes: 26 additions & 3 deletions hook_test.rb
    Original file line number Diff line number Diff line change
    @@ -9,6 +9,11 @@ def setup
    @subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    @target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"

    # Create the record before the hook is created or else the after_create Active Model hook
    # will trigger the REST hook prematurely during testing.
    @contact = Factory(:contact, :account => @account,
    :first => 'Ryan', :last => 'Porter', :email => 'xyz@venuedriver.com')

    hook = Hook.create(
    {
    "event" => "new_contact",
    @@ -17,9 +22,6 @@ def setup
    "target_url" => @target_url
    }
    )

    @contact = Factory(:contact, :account => @account,
    :first => 'Ryan', :last => 'Porter', :email => 'xyz@venuedriver.com')
    end

    def test_trigger
    @@ -29,11 +31,13 @@ def test_trigger
    :body => 'irrelevant',
    :status => ['200', 'Triggered']
    )
    FakeWeb.allow_net_connect = false

    Hook.trigger('new_contact', @contact)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal @contact.to_json, FakeWeb.last_request.body
    assert_equal 1, Hook.count # The hook should not have been deleted.
    end

    def test_trigger_remove_hook_on_410_response
    @@ -43,6 +47,7 @@ def test_trigger_remove_hook_on_410_response
    :body => 'irrelevant',
    :status => ['410', 'Danger, Will Robinson!']
    )
    FakeWeb.allow_net_connect = false

    Hook.trigger('new_contact', @contact)

    @@ -52,4 +57,22 @@ def test_trigger_remove_hook_on_410_response
    assert_equal 0, Hook.count # The 410 response should trigger removal of the hook.
    end

    def test_resque_background_job
    FakeWeb.register_uri(
    :post,
    @target_url,
    :body => 'irrelevant',
    :status => ['200', 'Triggered']
    )
    FakeWeb.allow_net_connect = false

    # A Resque worker will normally do this, which should have the same effect as when the hook
    # is manually triggered in the test_trigger test.
    Hook.perform(Contact.name, @contact.id)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal @contact.to_json, FakeWeb.last_request.body
    assert_equal 1, Hook.count # The hook should not have been deleted.
    end

    end
  4. endymion created this gist Jun 18, 2013.
    25 changes: 25 additions & 0 deletions hook.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,25 @@
    class Hook < ActiveRecord::Base
    attr_accessible :event, :account_id, :subscription_url, :target_url
    validates_presence_of :event, :account_id, :subscription_url, :target_url

    # Looks for an appropriate REST hook that matches the record, and triggers the hook if one exists.
    def self.trigger(event, record)
    hooks = Hook.find(:all, :conditions => {
    :event => event,
    :account_id => record.account_id,
    })
    return if hooks.empty?

    unless Rails.env.development?
    hook = hooks.first
    Rails.logger.info "Triggering REST hook: #{hook.inspect}"
    RestClient.post(hook.target_url, record.to_json) do |response, request, result|
    if response.code.eql? 410
    Rails.logger.info "Destroying REST hook because of 410 response: #{hook.inspect}"
    hook.destroy
    end
    end
    end
    end

    end
    55 changes: 55 additions & 0 deletions hook_test.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,55 @@
    require 'test_helper'
    require 'fakeweb'

    class HookTest < ActiveSupport::TestCase

    def setup
    @account = Factory(:account)

    @subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    @target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"

    hook = Hook.create(
    {
    "event" => "new_contact",
    "account_id" => @account.id,
    "subscription_url" => @subscription_url,
    "target_url" => @target_url
    }
    )

    @contact = Factory(:contact, :account => @account,
    :first => 'Ryan', :last => 'Porter', :email => 'xyz@venuedriver.com')
    end

    def test_trigger
    FakeWeb.register_uri(
    :post,
    @target_url,
    :body => 'irrelevant',
    :status => ['200', 'Triggered']
    )

    Hook.trigger('new_contact', @contact)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal @contact.to_json, FakeWeb.last_request.body
    end

    def test_trigger_remove_hook_on_410_response
    FakeWeb.register_uri(
    :post,
    @target_url,
    :body => 'irrelevant',
    :status => ['410', 'Danger, Will Robinson!']
    )

    Hook.trigger('new_contact', @contact)

    assert_equal "POST", FakeWeb.last_request.method
    assert_equal @contact.to_json, FakeWeb.last_request.body

    assert_equal 0, Hook.count # The 410 response should trigger removal of the hook.
    end

    end
    19 changes: 19 additions & 0 deletions hooks_controller.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    class HooksController < ApplicationController
    def create
    hook = Hook.new params
    render :nothing => true, :status => 500 and return unless hook.save
    Rails.logger.info "Created REST hook: #{hook.inspect}"
    # The Zapier documentation says to return 201 - Created.
    render :json => hook.to_json(:only => :id), :status => 201
    end

    def destroy
    hook = Hook.find(params[:id]) if params[:id]
    if hook.nil? && params[:subscription_url]
    hook = Hook.find_by_subscription_url(params[:subscription_url]).destroy
    end
    Rails.logger.info "Destroying REST hook: #{hook.inspect}"
    hook.destroy
    render :nothing => true, :status => 200
    end
    end
    101 changes: 101 additions & 0 deletions hooks_controller_test.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,101 @@
    require 'test_helper'

    class HooksControllerTest < ActionController::TestCase

    def setup
    @account = Factory(:account)
    @user = Factory(:user, :username => 'username') # Password: "password", by default.
    @staff = Factory(:staff, :account => @account, :user => @user)
    end

    def test_subscribe_requires_authentication
    post :create,
    {
    "event" => "new_contact",
    "subscription_url" => "whatever",
    "target_url" => "whatever"
    }

    assert_redirected_to :controller => 'public/signin', :action => 'signin'
    end

    def test_subscribe
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")

    subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"

    post :create,
    {
    "event" => "new_contact",
    "account_id" => @account.id,
    "subscription_url" => subscription_url,
    "target_url" => target_url
    }

    assert_response 201
    assert_equal 1, Hook.count

    hook = Hook.last
    assert_equal %Q[{"id":#{hook.id}}], @response.body
    assert_equal "new_contact", hook.event
    assert_equal @account.id, hook.account_id.to_i
    assert_equal subscription_url, hook.subscription_url
    assert_equal target_url, hook.target_url
    end

    def test_subscribe_error
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")

    post :create, nil

    assert_response 500
    assert_equal 0, Hook.count
    end

    def test_unsubscribe_rest
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")

    hook = Hook.create(
    :event => "new_contact",
    :account_id => @account.id,
    :subscription_url => 'whatever',
    :target_url => 'whatever'
    )
    assert_equal 1, Hook.count

    post :destroy,
    {
    :id => hook.id.to_s,
    :subscription_url => 'whatever'
    }

    assert_response 200
    assert_equal 0, Hook.count
    end

    def test_unsubscribe_the_hacky_way
    @request.env["HTTP_AUTHORIZATION"] = "Basic " + Base64::encode64("username:password")

    subscription_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"
    target_url = "https://zapier.com/hooks/standard/wpGRPPcRxZt2GxBbSSeUAlWPBnhLiRWB/"

    Hook.create(
    :event => "new_contact",
    :account_id => @account.id,
    :subscription_url => subscription_url,
    :target_url => target_url
    )
    assert_equal 1, Hook.count

    post :destroy,
    {
    "subscription_url" => subscription_url
    }

    assert_response 200
    assert_equal 0, Hook.count
    end


    end