Browse files

Reflection API

  • Loading branch information...
2 parents a463904 + bc8422b commit b49d4f3ab4469594b3844cc2ad3073039d1f872f @ileitch committed Dec 21, 2012
View
5 TODO
@@ -1,9 +1,8 @@
# 3.0
* Document config.rb
-* Do not exit() from ensure_upgraded when embedded.
# 3.1
-* Add option to quit after sending pending notifications.
* Warn if number of workers is more than db pool size?
-* statsd probes?
+* Bugsnag
+* Generator should merge rapns.rb
View
53 lib/generators/templates/rapns.rb
@@ -23,17 +23,48 @@
# Path to write PID file. Relative to Rails root unless absolute.
# config.pid_file = '/path/to/rapns.pid'
- # Define a block that will be called with a Rapns::Apns::Feedback instance
- # when feedback is received from the APNs that a notification has
- # failed to be delivered. Further notifications should not be sent to the device.
+ end
+
+Rapns.reflect do |on|
+
+ # Called with a Rapns::Apns::Feedback instance when feedback is received
+ # from the APNs that a notification has failed to be delivered.
+ # Further notifications should not be sent to the device.
+ # on.apns_feedback do |feedback|
+ # end
+
+ # Called when a notification is queued internally for delivery.
+ # The internal queue for each app runner can be inspected:
#
- # Example:
- # config.on_apns_feedback do |feedback|
- # device = Device.find_by_device_token(feedback.device_token)
- # if device
- # device.active = false
- # device.save!
- # end
+ # Rapns::Daemon::AppRunner.runners.each do |app_id, runner|
+ # runner.app
+ # runner.queue_size
+ # end
+ #
+ # on.notification_enqueued do |notification|
# end
- end
+ # Called when a notification is successfully delivered.
+ # on.notification_delivered do |notification|
+ # end
+
+ # Called when notification delivery failed.
+ # Call 'error_code' and 'error_description' on the notification for the cause.
+ # on.notification_failed do |notification|
+ # end
+
+ # Called when a notification will be retried at a later date.
+ # Call 'deliver_after' on the notification for the next delivery date
+ # and 'retries' for the number of times this notification has been retried.
+ # on.notification_will_retry do |notification|
+ # end
+
+ # Called when an APNs connection is lost and will be reconnected.
+ # on.apns_connection_lost do |app, error|
+ # end
+
+ # Called when an exception is raised.
+ # on.error do |error|
+ # end
+
+end
View
1 lib/rapns.rb
@@ -8,6 +8,7 @@
require 'rapns/notification'
require 'rapns/app'
require 'rapns/configuration'
+require 'rapns/reflection'
require 'rapns/embed'
require 'rapns/push'
View
11 lib/rapns/configuration.rb
@@ -15,6 +15,8 @@ class ConfigurationWithoutDefaults < Struct.new(*CONFIG_ATTRS)
end
class Configuration < Struct.new(*CONFIG_ATTRS)
+ include Deprecatable
+
attr_accessor :apns_feedback_callback
def initialize
@@ -29,10 +31,6 @@ def update(other)
end
end
- def on_apns_feedback(&block)
- self.apns_feedback_callback = block
- end
-
def pid_file=(path)
if path && !Pathname.new(path).absolute?
super(File.join(Rails.root, path))
@@ -41,6 +39,11 @@ def pid_file=(path)
end
end
+ def on_apns_feedback(&block)
+ self.apns_feedback_callback = block
+ end
+ deprecated(:on_apns_feedback, 3.2, "Please use the Rapns.reflect API instead.")
+
def reset
set_defaults
end
View
1 lib/rapns/daemon.rb
@@ -5,6 +5,7 @@
require 'net/http/persistent'
+require 'rapns/daemon/reflectable'
require 'rapns/daemon/interruptible_sleep'
require 'rapns/daemon/delivery_error'
require 'rapns/daemon/database_reconnectable'
View
21 lib/rapns/daemon/apns/connection.rb
@@ -4,18 +4,20 @@ module Apns
class ConnectionError < StandardError; end
class Connection
+ include Reflectable
+
attr_accessor :last_write
def self.idle_period
30.minutes
end
- def initialize(name, host, port, certificate, password)
- @name = name
+ def initialize(app, host, port)
+ @app = app
@host = host
@port = port
- @certificate = certificate
- @password = password
+ @certificate = app.certificate
+ @password = app.password
written
end
@@ -51,15 +53,16 @@ def write(data)
retry_count += 1;
if retry_count == 1
- Rapns::Daemon.logger.error("[#{@name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
+ Rapns::Daemon.logger.error("[#{@app.name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
+ reflect(:apns_connection_lost, @app, e)
end
if retry_count <= 3
reconnect
sleep 1
retry
else
- raise ConnectionError, "#{@name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
+ raise ConnectionError, "#{@app.name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
end
end
end
@@ -72,7 +75,7 @@ def reconnect
protected
def reconnect_idle
- Rapns::Daemon.logger.info("[#{@name}] Idle period exceeded, reconnecting...")
+ Rapns::Daemon.logger.info("[#{@app.name}] Idle period exceeded, reconnecting...")
reconnect
end
@@ -104,10 +107,10 @@ def connect_socket
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
ssl_socket.sync = true
ssl_socket.connect
- Rapns::Daemon.logger.info("[#{@name}] Connected to #{@host}:#{@port}")
+ Rapns::Daemon.logger.info("[#{@app.name}] Connected to #{@host}:#{@port}")
[tcp_socket, ssl_socket]
end
end
end
end
-end
+end
View
2 lib/rapns/daemon/apns/delivery_handler.rb
@@ -4,7 +4,7 @@ module Apns
class DeliveryHandler < Rapns::Daemon::DeliveryHandler
def initialize(app, host, port)
@app = app
- @connection = Connection.new(@app.name, host, port, @app.certificate, @app.password)
+ @connection = Connection.new(@app, host, port)
@connection.connect
end
View
8 lib/rapns/daemon/apns/feedback_receiver.rb
@@ -2,6 +2,7 @@ module Rapns
module Daemon
module Apns
class FeedbackReceiver
+ include Reflectable
include InterruptibleSleep
include DatabaseReconnectable
@@ -35,7 +36,7 @@ def stop
def check_for_feedback
connection = nil
begin
- connection = Connection.new("FeedbackReceiver:#{@app.name}", @host, @port, @certificate, @password)
+ connection = Connection.new(@app, @host, @port)
connection.connect
while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
@@ -59,8 +60,11 @@ def parse_tuple(tuple)
def create_feedback(failed_at, device_token)
formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
with_database_reconnect_and_retry do
- Rapns::Daemon.logger.info("[FeedbackReceiver:#{@app.name}] Delivery failed at #{formatted_failed_at} for #{device_token}")
+ Rapns::Daemon.logger.info("[#{@app.name}] [FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}.")
feedback = Rapns::Apns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
+ reflect(:apns_feedback, feedback)
+
+ # Deprecated.
begin
Rapns.config.apns_feedback_callback.call(feedback) if Rapns.config.apns_feedback_callback
rescue StandardError => e
View
30 lib/rapns/daemon/app_runner.rb
@@ -2,7 +2,7 @@ module Rapns
module Daemon
class AppRunner
class << self
- attr_reader :runners # TODO: Needed?
+ attr_reader :runners
end
@runners = {}
@@ -86,20 +86,28 @@ def sync(app)
diff = handlers.size - app.connections
return if diff == 0
if diff > 0
- diff.times { handlers.pop.stop }
- Rapns::Daemon.logger.info("[#{app.name}] Terminated #{handlers_str(diff)}. #{handlers_str} remaining.")
+ diff.times { decrement_handlers }
+ Rapns::Daemon.logger.info("[#{app.name}] Stopped #{handlers_str(diff)}. #{handlers_str} remaining.")
else
- diff.abs.times { handlers << start_handler }
- Rapns::Daemon.logger.info("[#{app.name}] Added #{handlers_str(diff)}. #{handlers_str} remaining.")
+ diff.abs.times { increment_handlers }
+ Rapns::Daemon.logger.info("[#{app.name}] Started #{handlers_str(diff)}. #{handlers_str} remaining.")
end
end
+ def decrement_handlers
+ handlers.pop.stop
+ end
+
+ def increment_handlers
+ handlers << start_handler
+ end
+
def debug
Rapns::Daemon.logger.info <<-EOS
#{@app.name}:
- handlers: #{handlers.size}
- queued: #{queue.size}
+ handlers: #{num_handlers}
+ queued: #{queue_size}
idle: #{idle?}
EOS
end
@@ -108,6 +116,14 @@ def idle?
queue.notifications_processed?
end
+ def queue_size
+ queue.size
+ end
+
+ def num_handlers
+ handlers.size
+ end
+
protected
def start_handler
View
6 lib/rapns/daemon/delivery.rb
@@ -2,6 +2,7 @@ module Rapns
module Daemon
class Delivery
include DatabaseReconnectable
+ include Reflectable
def self.perform(*args)
new(*args).perform
@@ -13,6 +14,7 @@ def retry_after(notification, deliver_after)
notification.deliver_after = deliver_after
notification.save!(:validate => false)
end
+ reflect(:notification_will_retry, notification)
end
def retry_exponentially(notification)
@@ -25,6 +27,7 @@ def mark_delivered
@notification.delivered_at = Time.now
@notification.save!(:validate => false)
end
+ reflect(:notification_delivered, @notification)
end
def mark_failed(code, description)
@@ -37,7 +40,8 @@ def mark_failed(code, description)
@notification.error_description = description
@notification.save!(:validate => false)
end
+ reflect(:notification_failed, @notification)
end
end
end
-end
+end
View
4 lib/rapns/daemon/delivery_handler.rb
@@ -1,6 +1,8 @@
module Rapns
module Daemon
class DeliveryHandler
+ include Reflectable
+
attr_accessor :queue
def start
@@ -35,8 +37,10 @@ def handle_next_notification
begin
deliver(notification)
+ reflect(:notification_delivered, notification)
rescue StandardError => e
Rapns::Daemon.logger.error(e)
+ reflect(:error, e)
ensure
queue.notification_processed
end
View
3 lib/rapns/daemon/feeder.rb
@@ -3,6 +3,7 @@ module Daemon
class Feeder
extend InterruptibleSleep
extend DatabaseReconnectable
+ extend Reflectable
def self.start
@stop = false
@@ -44,10 +45,12 @@ def self.enqueue_notifications
relation = relation.limit(batch_size) unless Rapns.config.push
relation.each do |notification|
Rapns::Daemon::AppRunner.enqueue(notification)
+ reflect(:notification_enqueued, notification)
end
end
rescue StandardError => e
Rapns::Daemon.logger.error(e)
+ reflect(:error, e)
end
end
end
View
13 lib/rapns/daemon/reflectable.rb
@@ -0,0 +1,13 @@
+module Rapns
+ module Daemon
+ module Reflectable
+ def reflect(name, *args)
+ begin
+ Rapns.reflections.__dispatch(name, *args)
+ rescue StandardError => e
+ Rapns::Daemon.logger.error(e)
+ end
+ end
+ end
+ end
+end
View
44 lib/rapns/reflection.rb
@@ -0,0 +1,44 @@
+module Rapns
+ def self.reflect
+ yield reflections if block_given?
+ end
+
+ def self.reflections
+ @reflections ||= Reflections.new
+ end
+
+ class Reflections
+ class NoSuchReflectionError < StandardError; end
+
+ REFLECTIONS = [
+ :apns_feedback, :notification_enqueued, :notification_delivered,
+ :notification_failed, :notification_will_retry, :apns_connection_lost,
+ :error
+ ]
+
+ REFLECTIONS.each do |reflection|
+ class_eval(<<-RUBY, __FILE__, __LINE__)
+ def #{reflection}(*args, &blk)
+ raise "block required" unless block_given?
+ reflections[:#{reflection}] = blk
+ end
+ RUBY
+ end
+
+ def __dispatch(reflection, *args)
+ unless REFLECTIONS.include?(reflection.to_sym)
+ raise NoSuchReflectionError, reflection
+ end
+
+ if reflections[reflection]
+ reflections[reflection].call(*args)
+ end
+ end
+
+ private
+
+ def reflections
+ @reflections ||= {}
+ end
+ end
+end
View
4 spec/unit/configuration_spec.rb
@@ -15,7 +15,9 @@
it 'configures a feedback callback' do
b = Proc.new {}
- config.on_apns_feedback(&b)
+ Rapns::Deprecation.silenced do
+ config.on_apns_feedback(&b)
+ end
config.apns_feedback_callback.should == b
end
View
13 spec/unit/daemon/apns/connection_spec.rb
@@ -11,7 +11,8 @@
let(:tcp_socket) { stub(:setsockopt => nil, :close => nil) }
let(:ssl_socket) { stub(:sync= => nil, :connect => nil, :close => nil, :write => nil, :flush => nil) }
let(:logger) { stub(:info => nil, :error => nil) }
- let(:connection) { Rapns::Daemon::Apns::Connection.new('Connection 0', host, port, certificate, password) }
+ let(:app) { stub(:name => 'Connection 0', :certificate => certificate, :password => password)}
+ let(:connection) { Rapns::Daemon::Apns::Connection.new(app, host, port) }
before do
OpenSSL::SSL::SSLContext.stub(:new => ssl_context)
@@ -121,6 +122,14 @@
ssl_socket.stub(:write).and_raise(error_type)
end
+ it 'reflects the connection has been lost' do
+ connection.should_receive(:reflect).with(:apns_connection_lost, app, kind_of(error_type))
+ begin
+ connection.write(nil)
+ rescue Rapns::Daemon::Apns::ConnectionError
+ end
+ end
+
it "logs that the connection has been lost once only" do
logger.should_receive(:error).with("[Connection 0] Lost connection to gateway.push.apple.com:2195 (#{error_type.name}), reconnecting...").once
begin
@@ -231,4 +240,4 @@ def error_type
connection.write('blah')
end
end
-end
+end
View
2 spec/unit/daemon/apns/delivery_handler_spec.rb
@@ -23,7 +23,7 @@
end
it "instantiates a new connection" do
- Rapns::Daemon::Apns::Connection.should_receive(:new).with(app.name, host, port, certificate, password)
+ Rapns::Daemon::Apns::Connection.should_receive(:new).with(app, host, port)
Rapns::Daemon::Apns::DeliveryHandler.new(app, host, port)
end
View
10 spec/unit/daemon/apns/delivery_spec.rb
@@ -37,6 +37,11 @@ def perform
perform
end
+ it 'reflects the notification was delivered' do
+ delivery.should_receive(:reflect).with(:notification_delivered, notification)
+ perform
+ end
+
it "sets the time the notification was delivered" do
now = Time.now
Time.stub(:now).and_return(now)
@@ -83,6 +88,11 @@ def perform
perform
end
+ it 'reflects the notification delivery failed' do
+ delivery.should_receive(:reflect).with(:notification_failed, notification)
+ perform
+ end
+
it "sets the notification failed_at timestamp" do
now = Time.now
Time.stub(:now).and_return(now)
View
23 spec/unit/daemon/apns/feedback_receiver_spec.rb
@@ -10,12 +10,13 @@
let(:connection) { stub(:connect => nil, :read => nil, :close => nil) }
let(:logger) { stub(:error => nil, :info => nil) }
let(:receiver) { Rapns::Daemon::Apns::FeedbackReceiver.new(app, host, port, poll) }
+ let(:feedback) { stub }
before do
receiver.stub(:interruptible_sleep)
Rapns::Daemon.logger = logger
Rapns::Daemon::Apns::Connection.stub(:new => connection)
- Rapns::Apns::Feedback.stub(:create!)
+ Rapns::Apns::Feedback.stub(:create! => feedback)
receiver.instance_variable_set("@stop", false)
end
@@ -31,7 +32,7 @@ def connection.read(bytes)
end
it 'instantiates a new connection' do
- Rapns::Daemon::Apns::Connection.should_receive(:new).with("FeedbackReceiver:#{app.name}", host, port, certificate, password)
+ Rapns::Daemon::Apns::Connection.should_receive(:new).with(app, host, port)
receiver.check_for_feedback
end
@@ -52,7 +53,7 @@ def connection.read(bytes)
it 'logs the feedback' do
stub_connection_read_with_tuple
- Rapns::Daemon.logger.should_receive(:info).with("[FeedbackReceiver:my_app] Delivery failed at 2011-12-10 16:08:45 UTC for 834f786655eb9f84614a05ad7d00af31e5cfe93ac3ea078f1da44d2a4eb0ce17")
+ Rapns::Daemon.logger.should_receive(:info).with("[my_app] [FeedbackReceiver] Delivery failed at 2011-12-10 16:08:45 UTC for 834f786655eb9f84614a05ad7d00af31e5cfe93ac3ea078f1da44d2a4eb0ce17.")
receiver.check_for_feedback
end
@@ -90,11 +91,15 @@ def connection.read(bytes)
receiver.stop
end
+ it 'reflects feedback was received' do
+ stub_connection_read_with_tuple
+ receiver.should_receive(:reflect).with(:apns_feedback, feedback)
+ receiver.check_for_feedback
+ end
+
it 'calls the apns_feedback_callback when feedback is received and the callback is set' do
stub_connection_read_with_tuple
Rapns.config.apns_feedback_callback = Proc.new {}
- feedback = Object.new
- Rapns::Apns::Feedback.stub(:create! => feedback)
Rapns.config.apns_feedback_callback.should_receive(:call).with(feedback)
receiver.check_for_feedback
end
@@ -103,7 +108,9 @@ def connection.read(bytes)
error = StandardError.new('bork!')
stub_connection_read_with_tuple
callback = Proc.new { raise error }
- Rapns.config.on_apns_feedback &callback
+ Rapns::Deprecation.silenced do
+ Rapns.config.on_apns_feedback &callback
+ end
expect { receiver.check_for_feedback }.not_to raise_error
end
@@ -112,7 +119,9 @@ def connection.read(bytes)
stub_connection_read_with_tuple
callback = Proc.new { raise error }
Rapns::Daemon.logger.should_receive(:error).with(error)
- Rapns.config.on_apns_feedback &callback
+ Rapns::Deprecation.silenced do
+ Rapns.config.on_apns_feedback &callback
+ end
receiver.check_for_feedback
end
end
View
8 spec/unit/daemon/delivery_handler_shared.rb
@@ -8,6 +8,14 @@
delivery_handler.send(:handle_next_notification)
end
+ it 'reflects an exception' do
+ Rapns::Daemon.stub(:logger => stub(:error => nil))
+ error = StandardError.new
+ delivery_handler.stub(:deliver).and_raise(error)
+ delivery_handler.should_receive(:reflect).with(:error, error)
+ delivery_handler.send(:handle_next_notification)
+ end
+
it "instructs the queue to wakeup the thread when told to stop" do
thread = stub(:join => nil)
Thread.stub(:new => thread)
View
9 spec/unit/daemon/feeder_spec.rb
@@ -51,12 +51,19 @@ def start
start
end
- it "enqueue the notification" do
+ it "enqueues the notification" do
notification.update_attributes!(:delivered => false)
Rapns::Daemon::AppRunner.should_receive(:enqueue).with(notification)
start
end
+ it 'reflects the notification has been enqueued' do
+ notification.update_attributes!(:delivered => false)
+ Rapns::Daemon::AppRunner.stub(:enqueue)
+ Rapns::Daemon::Feeder.should_receive(:reflect).with(:notification_enqueued, notification)
+ start
+ end
+
it 'does not enqueue the notification if the app runner is still processing the previous batch' do
Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
start
View
34 spec/unit/daemon/gcm/delivery_spec.rb
@@ -7,9 +7,10 @@
let(:response) { stub(:code => 200, :header => {}) }
let(:http) { stub(:shutdown => nil, :request => response)}
let(:now) { Time.parse('2012-10-14 00:00:00') }
+ let(:delivery) { Rapns::Daemon::Gcm::Delivery.new(app, http, notification) }
def perform
- Rapns::Daemon::Gcm::Delivery.perform(app, http, notification)
+ delivery.perform
end
before do
@@ -29,6 +30,12 @@ def perform
end.to change(notification, :delivered).to(true)
end
+ it 'reflects the notification was delivered' do
+ response.stub(:body => JSON.dump({ 'failure' => 0 }))
+ delivery.should_receive(:reflect).with(:notification_delivered, notification)
+ perform
+ end
+
it 'logs that the notification was delivered' do
response.stub(:body => JSON.dump({ 'failure' => 0 }))
logger.should_receive(:info).with("[MyApp] 1 sent to xyz")
@@ -105,6 +112,11 @@ def perform
notification.error_description.should == error_description
end
+ it 'reflects the notification delivery failed' do
+ delivery.should_receive(:reflect).with(:notification_failed, notification)
+ perform rescue Rapns::DeliveryError
+ end
+
it 'creates a new notification for the unavailable devices' do
notification.update_attributes(:registration_ids => ['id_0', 'id_1', 'id_2'], :data => {'one' => 1}, :collapse_key => 'thing', :delay_while_idle => true)
perform rescue Rapns::DeliveryError
@@ -179,6 +191,11 @@ def perform
perform
end.to change(notification, :deliver_after).to(now + 2 ** 1)
end
+
+ it 'reflects the notification will be retried' do
+ delivery.should_receive(:reflect).with(:notification_will_retry, notification)
+ perform
+ end
end
describe 'an 500 response' do
@@ -198,6 +215,11 @@ def perform
notification.reload
end.to change(notification, :deliver_after).to(now + 2 ** 3)
end
+
+ it 'reflects the notification will be retried' do
+ delivery.should_receive(:reflect).with(:notification_will_retry, notification)
+ perform
+ end
end
describe 'an 401 response' do
@@ -219,6 +241,11 @@ def perform
notification.error_code.should == 400
notification.error_description.should == 'GCM failed to parse the JSON request. Possibly an rapns bug, please open an issue.'
end
+
+ it 'reflects the notification delivery failed' do
+ delivery.should_receive(:reflect).with(:notification_failed, notification)
+ perform rescue Rapns::DeliveryError
+ end
end
describe 'an un-handled response' do
@@ -232,5 +259,10 @@ def perform
notification.error_code.should == 418
notification.error_description.should == "I'm a Teapot"
end
+
+ it 'reflects the notification delivery failed' do
+ delivery.should_receive(:reflect).with(:notification_failed, notification)
+ perform rescue Rapns::DeliveryError
+ end
end
end
View
27 spec/unit/daemon/reflectable_spec.rb
@@ -0,0 +1,27 @@
+require 'unit_spec_helper'
+
+describe Rapns::Daemon::Reflectable do
+ class TestReflectable
+ include Rapns::Daemon::Reflectable
+ end
+
+ let(:logger) { stub(:error => nil) }
+ let(:test_reflectable) { TestReflectable.new }
+
+ before do
+ Rapns.reflections.stub(:__dispatch)
+ Rapns::Daemon.stub(:logger => logger)
+ end
+
+ it 'dispatches the given reflection' do
+ Rapns.reflections.should_receive(:__dispatch).with(:error)
+ test_reflectable.reflect(:error)
+ end
+
+ it 'logs errors raise by the reflection' do
+ error = StandardError.new
+ Rapns.reflections.stub(:__dispatch).and_raise(error)
+ Rapns::Daemon.logger.should_receive(:error).with(error)
+ test_reflectable.reflect(:error)
+ end
+end
View
34 spec/unit/reflection_spec.rb
@@ -0,0 +1,34 @@
+ require 'unit_spec_helper'
+
+describe Rapns do
+ it 'yields reflections for configuration' do
+ did_yield = false
+ Rapns.reflect { |on| did_yield = true }
+ did_yield.should be_true
+ end
+
+ it 'returns all reflections' do
+ Rapns.reflections.should be_kind_of(Rapns::Reflections)
+ end
+end
+
+# :apns_feedback, :notification_enqueued, :notification_delivered,
+# :notification_failed, :notification_will_retry, :apns_connection_lost,
+# :error
+
+describe Rapns::Reflections do
+ it 'dispatches the given reflection' do
+ did_yield = false
+ Rapns.reflect do |on|
+ on.error { did_yield = true }
+ end
+ Rapns.reflections.__dispatch(:error)
+ did_yield.should be_true
+ end
+
+ it 'raises an error when trying to dispatch and unknown reflection' do
+ expect do
+ Rapns.reflections.__dispatch(:unknown)
+ end.to raise_error(Rapns::Reflections::NoSuchReflectionError)
+ end
+end

0 comments on commit b49d4f3

Please sign in to comment.