rapns 3.3.2-java → 3.4.0-java
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +7 -0
- data/README.md +19 -21
- data/bin/rapns +14 -13
- data/lib/generators/templates/rapns.rb +8 -4
- data/lib/rapns.rb +7 -0
- data/lib/rapns/TODO +3 -0
- data/lib/rapns/apns/feedback.rb +4 -2
- data/lib/rapns/app.rb +3 -1
- data/lib/rapns/configuration.rb +8 -1
- data/lib/rapns/daemon.rb +3 -1
- data/lib/rapns/daemon/apns/app_runner.rb +3 -2
- data/lib/rapns/daemon/apns/certificate_expired_error.rb +20 -0
- data/lib/rapns/daemon/apns/connection.rb +26 -0
- data/lib/rapns/daemon/apns/delivery.rb +2 -1
- data/lib/rapns/daemon/apns/delivery_handler.rb +2 -2
- data/lib/rapns/daemon/app_runner.rb +50 -28
- data/lib/rapns/daemon/batch.rb +100 -0
- data/lib/rapns/daemon/delivery.rb +6 -10
- data/lib/rapns/daemon/delivery_handler.rb +14 -12
- data/lib/rapns/daemon/delivery_handler_collection.rb +33 -0
- data/lib/rapns/daemon/feeder.rb +3 -5
- data/lib/rapns/daemon/gcm/delivery.rb +5 -4
- data/lib/rapns/daemon/gcm/delivery_handler.rb +2 -2
- data/lib/rapns/daemon/store/active_record.rb +23 -2
- data/lib/rapns/deprecation.rb +7 -6
- data/lib/rapns/logger.rb +1 -1
- data/lib/rapns/notification.rb +5 -3
- data/lib/rapns/reflection.rb +1 -1
- data/lib/rapns/version.rb +1 -1
- data/lib/tasks/cane.rake +1 -1
- data/lib/tasks/test.rake +8 -3
- data/spec/unit/apns/app_spec.rb +4 -4
- data/spec/unit/apns_feedback_spec.rb +1 -1
- data/spec/unit/configuration_spec.rb +12 -6
- data/spec/unit/daemon/apns/app_runner_spec.rb +6 -4
- data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
- data/spec/unit/daemon/apns/connection_spec.rb +46 -10
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +24 -18
- data/spec/unit/daemon/apns/delivery_spec.rb +11 -12
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +16 -16
- data/spec/unit/daemon/app_runner_shared.rb +27 -10
- data/spec/unit/daemon/app_runner_spec.rb +48 -28
- data/spec/unit/daemon/batch_spec.rb +160 -0
- data/spec/unit/daemon/delivery_handler_collection_spec.rb +37 -0
- data/spec/unit/daemon/delivery_handler_shared.rb +20 -11
- data/spec/unit/daemon/feeder_spec.rb +12 -12
- data/spec/unit/daemon/gcm/app_runner_spec.rb +4 -2
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +18 -10
- data/spec/unit/daemon/gcm/delivery_spec.rb +47 -17
- data/spec/unit/daemon/interruptible_sleep_spec.rb +3 -3
- data/spec/unit/daemon/reflectable_spec.rb +1 -1
- data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +1 -1
- data/spec/unit/daemon/store/active_record_spec.rb +87 -10
- data/spec/unit/daemon_spec.rb +6 -6
- data/spec/unit/deprecation_spec.rb +2 -2
- data/spec/unit/logger_spec.rb +33 -17
- data/spec/unit/notification_shared.rb +7 -3
- data/spec/unit/upgraded_spec.rb +8 -14
- data/spec/unit_spec_helper.rb +9 -1
- metadata +53 -44
- data/lib/rapns/daemon/delivery_queue.rb +0 -42
- data/lib/rapns/daemon/delivery_queue_18.rb +0 -44
- data/lib/rapns/daemon/delivery_queue_19.rb +0 -42
- data/spec/acceptance/gcm_upgrade_spec.rb +0 -34
- data/spec/acceptance_spec_helper.rb +0 -85
- data/spec/unit/daemon/delivery_queue_spec.rb +0 -29
@@ -0,0 +1,100 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class Batch
|
4
|
+
include Reflectable
|
5
|
+
|
6
|
+
attr_reader :num_processed, :notifications,
|
7
|
+
:delivered, :failed, :retryable
|
8
|
+
|
9
|
+
def initialize(notifications)
|
10
|
+
@notifications = notifications
|
11
|
+
@num_processed = 0
|
12
|
+
@delivered = []
|
13
|
+
@failed = {}
|
14
|
+
@retryable = {}
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def num_notifications
|
19
|
+
@notifications.size
|
20
|
+
end
|
21
|
+
|
22
|
+
def mark_retryable(notification, deliver_after)
|
23
|
+
if Rapns.config.batch_storage_updates
|
24
|
+
@retryable[deliver_after] ||= []
|
25
|
+
@retryable[deliver_after] << notification
|
26
|
+
else
|
27
|
+
Rapns::Daemon.store.mark_retryable(notification, deliver_after)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def mark_delivered(notification)
|
32
|
+
if Rapns.config.batch_storage_updates
|
33
|
+
@delivered << notification
|
34
|
+
else
|
35
|
+
Rapns::Daemon.store.mark_delivered(notification)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def mark_failed(notification, code, description)
|
40
|
+
if Rapns.config.batch_storage_updates
|
41
|
+
key = [code, description]
|
42
|
+
@failed[key] ||= []
|
43
|
+
@failed[key] << notification
|
44
|
+
else
|
45
|
+
Rapns::Daemon.store.mark_failed(notification, code, description)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def notification_processed
|
50
|
+
@mutex.synchronize do
|
51
|
+
@num_processed += 1
|
52
|
+
complete if @num_processed >= @notifications.size
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def complete?
|
57
|
+
@complete == true
|
58
|
+
end
|
59
|
+
|
60
|
+
def describe
|
61
|
+
@notifications.map(&:id).join(', ')
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def complete
|
67
|
+
[:complete_delivered, :complete_failed, :complete_retried].each do |method|
|
68
|
+
begin
|
69
|
+
send(method)
|
70
|
+
rescue StandardError => e
|
71
|
+
Rapns.logger.error(e)
|
72
|
+
reflect(:error, e)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
@notifications.clear
|
77
|
+
@complete = true
|
78
|
+
end
|
79
|
+
|
80
|
+
def complete_delivered
|
81
|
+
Rapns::Daemon.store.mark_batch_delivered(@delivered)
|
82
|
+
@delivered.clear
|
83
|
+
end
|
84
|
+
|
85
|
+
def complete_failed
|
86
|
+
@failed.each do |(code, description), notifications|
|
87
|
+
Rapns::Daemon.store.mark_batch_failed(notifications, code, description)
|
88
|
+
end
|
89
|
+
@failed.clear
|
90
|
+
end
|
91
|
+
|
92
|
+
def complete_retried
|
93
|
+
@retryable.each do |deliver_after, notifications|
|
94
|
+
Rapns::Daemon.store.mark_batch_retryable(notifications, deliver_after)
|
95
|
+
end
|
96
|
+
@retryable.clear
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -3,26 +3,22 @@ module Rapns
|
|
3
3
|
class Delivery
|
4
4
|
include Reflectable
|
5
5
|
|
6
|
-
def
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
def retry_after(notification, deliver_after)
|
11
|
-
Rapns::Daemon.store.retry_after(notification, deliver_after)
|
6
|
+
def mark_retryable(notification, deliver_after)
|
7
|
+
@batch.mark_retryable(notification, deliver_after)
|
12
8
|
reflect(:notification_will_retry, notification)
|
13
9
|
end
|
14
10
|
|
15
|
-
def
|
16
|
-
|
11
|
+
def mark_retryable_exponential(notification)
|
12
|
+
mark_retryable(notification, Time.now + 2 ** (notification.retries + 1))
|
17
13
|
end
|
18
14
|
|
19
15
|
def mark_delivered
|
20
|
-
|
16
|
+
@batch.mark_delivered(@notification)
|
21
17
|
reflect(:notification_delivered, @notification)
|
22
18
|
end
|
23
19
|
|
24
20
|
def mark_failed(code, description)
|
25
|
-
|
21
|
+
@batch.mark_failed(@notification, code, description)
|
26
22
|
reflect(:notification_failed, @notification)
|
27
23
|
end
|
28
24
|
end
|
@@ -3,6 +3,8 @@ module Rapns
|
|
3
3
|
class DeliveryHandler
|
4
4
|
include Reflectable
|
5
5
|
|
6
|
+
WAKEUP = :wakeup
|
7
|
+
|
6
8
|
attr_accessor :queue
|
7
9
|
|
8
10
|
def start
|
@@ -16,10 +18,14 @@ module Rapns
|
|
16
18
|
|
17
19
|
def stop
|
18
20
|
@stop = true
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
end
|
22
|
+
|
23
|
+
def wakeup
|
24
|
+
queue.push(WAKEUP) if @thread
|
25
|
+
end
|
26
|
+
|
27
|
+
def wait
|
28
|
+
@thread.join if @thread
|
23
29
|
stopped
|
24
30
|
end
|
25
31
|
|
@@ -29,20 +35,16 @@ module Rapns
|
|
29
35
|
end
|
30
36
|
|
31
37
|
def handle_next_notification
|
32
|
-
|
33
|
-
|
34
|
-
rescue DeliveryQueue::WakeupError
|
35
|
-
return
|
36
|
-
end
|
38
|
+
notification, batch = queue.pop
|
39
|
+
return if notification == WAKEUP
|
37
40
|
|
38
41
|
begin
|
39
|
-
deliver(notification)
|
40
|
-
reflect(:notification_delivered, notification)
|
42
|
+
deliver(notification, batch)
|
41
43
|
rescue StandardError => e
|
42
44
|
Rapns.logger.error(e)
|
43
45
|
reflect(:error, e)
|
44
46
|
ensure
|
45
|
-
|
47
|
+
batch.notification_processed
|
46
48
|
end
|
47
49
|
end
|
48
50
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class DeliveryHandlerCollection
|
4
|
+
attr_reader :handlers
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@handlers = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def push(handler)
|
11
|
+
@handlers << handler
|
12
|
+
end
|
13
|
+
|
14
|
+
def pop
|
15
|
+
handler = @handlers.pop
|
16
|
+
handler.stop
|
17
|
+
handler.wakeup
|
18
|
+
@handlers.map(&:wakeup)
|
19
|
+
handler.wait
|
20
|
+
end
|
21
|
+
|
22
|
+
def size
|
23
|
+
@handlers.size
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
@handlers.map(&:stop)
|
28
|
+
@handlers.map(&:wakeup)
|
29
|
+
@handlers.map(&:wait)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/rapns/daemon/feeder.rb
CHANGED
@@ -38,11 +38,9 @@ module Rapns
|
|
38
38
|
def self.enqueue_notifications
|
39
39
|
begin
|
40
40
|
idle = Rapns::Daemon::AppRunner.idle.map(&:app)
|
41
|
-
|
42
|
-
Rapns::Daemon.store.deliverable_notifications(idle)
|
43
|
-
|
44
|
-
reflect(:notification_enqueued, notification)
|
45
|
-
end
|
41
|
+
return if idle.empty?
|
42
|
+
notifications = Rapns::Daemon.store.deliverable_notifications(idle)
|
43
|
+
Rapns::Daemon::AppRunner.enqueue(notifications)
|
46
44
|
rescue StandardError => e
|
47
45
|
Rapns.logger.error(e)
|
48
46
|
reflect(:error, e)
|
@@ -8,10 +8,11 @@ module Rapns
|
|
8
8
|
GCM_URI = URI.parse('https://android.googleapis.com/gcm/send')
|
9
9
|
UNAVAILABLE_STATES = ['Unavailable', 'InternalServerError']
|
10
10
|
|
11
|
-
def initialize(app, http, notification)
|
11
|
+
def initialize(app, http, notification, batch)
|
12
12
|
@app = app
|
13
13
|
@http = http
|
14
14
|
@notification = notification
|
15
|
+
@batch = batch
|
15
16
|
end
|
16
17
|
|
17
18
|
def perform
|
@@ -121,7 +122,7 @@ module Rapns
|
|
121
122
|
|
122
123
|
def deliver_after_header(response)
|
123
124
|
if response.header['retry-after']
|
124
|
-
|
125
|
+
if response.header['retry-after'].to_s =~ /^[0-9]+$/
|
125
126
|
Time.now + response.header['retry-after'].to_i
|
126
127
|
else
|
127
128
|
Time.httpdate(response.header['retry-after'])
|
@@ -131,9 +132,9 @@ module Rapns
|
|
131
132
|
|
132
133
|
def retry_delivery(notification, response)
|
133
134
|
if time = deliver_after_header(response)
|
134
|
-
|
135
|
+
mark_retryable(notification, time)
|
135
136
|
else
|
136
|
-
|
137
|
+
mark_retryable_exponential(notification)
|
137
138
|
end
|
138
139
|
end
|
139
140
|
|
@@ -7,8 +7,8 @@ module Rapns
|
|
7
7
|
@http = Net::HTTP::Persistent.new('rapns')
|
8
8
|
end
|
9
9
|
|
10
|
-
def deliver(notification)
|
11
|
-
Rapns::Daemon::Gcm::Delivery.
|
10
|
+
def deliver(notification, batch)
|
11
|
+
Rapns::Daemon::Gcm::Delivery.new(@app, @http, notification, batch).perform
|
12
12
|
end
|
13
13
|
|
14
14
|
def stopped
|
@@ -13,11 +13,11 @@ module Rapns
|
|
13
13
|
batch_size = Rapns.config.batch_size
|
14
14
|
relation = Rapns::Notification.ready_for_delivery.for_apps(apps)
|
15
15
|
relation = relation.limit(batch_size) unless Rapns.config.push
|
16
|
-
relation.
|
16
|
+
relation.to_a
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
20
|
+
def mark_retryable(notification, deliver_after)
|
21
21
|
with_database_reconnect_and_retry do
|
22
22
|
notification.retries += 1
|
23
23
|
notification.deliver_after = deliver_after
|
@@ -25,6 +25,13 @@ module Rapns
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
def mark_batch_retryable(notifications, deliver_after)
|
29
|
+
ids = notifications.map(&:id)
|
30
|
+
with_database_reconnect_and_retry do
|
31
|
+
Rapns::Notification.where(:id => ids).update_all(['retries = retries + 1, deliver_after = ?', deliver_after])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
28
35
|
def mark_delivered(notification)
|
29
36
|
with_database_reconnect_and_retry do
|
30
37
|
notification.delivered = true
|
@@ -33,6 +40,13 @@ module Rapns
|
|
33
40
|
end
|
34
41
|
end
|
35
42
|
|
43
|
+
def mark_batch_delivered(notifications)
|
44
|
+
ids = notifications.map(&:id)
|
45
|
+
with_database_reconnect_and_retry do
|
46
|
+
Rapns::Notification.where(:id => ids).update_all(['delivered = true, delivered_at = ?', Time.now])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
36
50
|
def mark_failed(notification, code, description)
|
37
51
|
with_database_reconnect_and_retry do
|
38
52
|
notification.delivered = false
|
@@ -45,6 +59,13 @@ module Rapns
|
|
45
59
|
end
|
46
60
|
end
|
47
61
|
|
62
|
+
def mark_batch_failed(notifications, code, description)
|
63
|
+
ids = notifications.map(&:id)
|
64
|
+
with_database_reconnect_and_retry do
|
65
|
+
Rapns::Notification.where(:id => ids).update_all(['delivered = false, delivered_at = NULL, failed = true, failed_at = ?, error_code = ?, error_description = ?', Time.now, code, description])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
48
69
|
def create_apns_feedback(failed_at, device_token, app)
|
49
70
|
with_database_reconnect_and_retry do
|
50
71
|
Rapns::Apns::Feedback.create!(:failed_at => failed_at,
|
data/lib/rapns/deprecation.rb
CHANGED
@@ -1,20 +1,21 @@
|
|
1
1
|
module Rapns
|
2
2
|
class Deprecation
|
3
|
-
def self.
|
3
|
+
def self.muted
|
4
4
|
begin
|
5
|
-
Thread.current[:
|
5
|
+
orig_val = Thread.current[:rapns_mute_deprecations]
|
6
|
+
Thread.current[:rapns_mute_deprecations] = true
|
6
7
|
yield
|
7
8
|
ensure
|
8
|
-
Thread.current[:
|
9
|
+
Thread.current[:rapns_mute_deprecations] = orig_val
|
9
10
|
end
|
10
11
|
end
|
11
12
|
|
12
|
-
def self.
|
13
|
-
Thread.current[:
|
13
|
+
def self.muted?
|
14
|
+
Thread.current[:rapns_mute_deprecations] == true
|
14
15
|
end
|
15
16
|
|
16
17
|
def self.warn(msg)
|
17
|
-
unless Rapns::Deprecation.
|
18
|
+
unless Rapns::Deprecation.muted?
|
18
19
|
STDERR.puts "DEPRECATION WARNING: #{msg}"
|
19
20
|
end
|
20
21
|
end
|
data/lib/rapns/logger.rb
CHANGED
@@ -32,7 +32,7 @@ module Rapns
|
|
32
32
|
def setup_logger(log)
|
33
33
|
if Rapns.config.logger
|
34
34
|
@logger = Rapns.config.logger
|
35
|
-
elsif
|
35
|
+
elsif ActiveSupport.const_defined?('BufferedLogger')
|
36
36
|
@logger = ActiveSupport::BufferedLogger.new(log, Rails.logger.level)
|
37
37
|
@logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
|
38
38
|
else
|
data/lib/rapns/notification.rb
CHANGED
@@ -9,9 +9,11 @@ module Rapns
|
|
9
9
|
|
10
10
|
belongs_to :app, :class_name => 'Rapns::App'
|
11
11
|
|
12
|
-
|
13
|
-
:
|
14
|
-
|
12
|
+
if Rapns.attr_accessible_available?
|
13
|
+
attr_accessible :badge, :device_token, :sound, :alert, :data, :expiry,:delivered,
|
14
|
+
:delivered_at, :failed, :failed_at, :error_code, :error_description, :deliver_after,
|
15
|
+
:alert_is_json, :app, :app_id, :collapse_key, :delay_while_idle, :registration_ids
|
16
|
+
end
|
15
17
|
|
16
18
|
validates :expiry, :numericality => true, :allow_nil => true
|
17
19
|
validates :app, :presence => true
|
data/lib/rapns/reflection.rb
CHANGED
@@ -13,7 +13,7 @@ module Rapns
|
|
13
13
|
REFLECTIONS = [
|
14
14
|
:apns_feedback, :notification_enqueued, :notification_delivered,
|
15
15
|
:notification_failed, :notification_will_retry, :apns_connection_lost,
|
16
|
-
:gcm_canonical_id, :error
|
16
|
+
:gcm_canonical_id, :error, :apns_certificate_will_expire
|
17
17
|
]
|
18
18
|
|
19
19
|
REFLECTIONS.each do |reflection|
|
data/lib/rapns/version.rb
CHANGED
data/lib/tasks/cane.rake
CHANGED
@@ -3,7 +3,7 @@ begin
|
|
3
3
|
|
4
4
|
desc "Run cane to check quality metrics"
|
5
5
|
Cane::RakeTask.new(:quality) do |cane|
|
6
|
-
cane.add_threshold 'coverage/covered_percent', :>=,
|
6
|
+
cane.add_threshold 'coverage/covered_percent', :>=, 98
|
7
7
|
cane.no_style = false
|
8
8
|
cane.style_measure = 1000
|
9
9
|
cane.no_doc = true
|
data/lib/tasks/test.rake
CHANGED
@@ -2,9 +2,13 @@ namespace :test do
|
|
2
2
|
task :build_rails do
|
3
3
|
require 'fileutils'
|
4
4
|
|
5
|
-
def cmd(str)
|
5
|
+
def cmd(str, clean_env = true)
|
6
6
|
puts "* #{str}"
|
7
|
-
retval =
|
7
|
+
retval = if clean_env
|
8
|
+
Bundler.with_clean_env { `#{str}` }
|
9
|
+
else
|
10
|
+
`#{str}`
|
11
|
+
end
|
8
12
|
puts retval.strip
|
9
13
|
retval
|
10
14
|
end
|
@@ -15,7 +19,8 @@ namespace :test do
|
|
15
19
|
FileUtils.mkdir_p(path)
|
16
20
|
pwd = Dir.pwd
|
17
21
|
|
18
|
-
cmd("bundle exec rails
|
22
|
+
cmd("bundle exec rails --version", false)
|
23
|
+
cmd("bundle exec rails new #{path} --skip-bundle", false)
|
19
24
|
|
20
25
|
begin
|
21
26
|
Dir.chdir(path)
|
data/spec/unit/apns/app_spec.rb
CHANGED
@@ -2,26 +2,26 @@ require 'unit_spec_helper'
|
|
2
2
|
|
3
3
|
describe Rapns::App do
|
4
4
|
it 'does not validate an app with an invalid certificate' do
|
5
|
-
app = Rapns::Apns::App.new(:
|
5
|
+
app = Rapns::Apns::App.new(:name => 'test', :environment => 'development', :certificate => 'foo')
|
6
6
|
app.valid?
|
7
7
|
app.errors[:certificate].should == ['Certificate value must contain a certificate and a private key.']
|
8
8
|
end
|
9
9
|
|
10
10
|
it 'validates a certificate without a password' do
|
11
|
-
app = Rapns::Apns::App.new :
|
11
|
+
app = Rapns::Apns::App.new :name => 'test', :environment => 'development', :certificate => TEST_CERT
|
12
12
|
app.valid?
|
13
13
|
app.errors[:certificate].should == []
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'validates a certificate with a password' do
|
17
|
-
app = Rapns::Apns::App.new :
|
17
|
+
app = Rapns::Apns::App.new :name => 'test', :environment => 'development',
|
18
18
|
:certificate => TEST_CERT_WITH_PASSWORD, :password => 'fubar'
|
19
19
|
app.valid?
|
20
20
|
app.errors[:certificate].should == []
|
21
21
|
end
|
22
22
|
|
23
23
|
it 'validates a certificate with an incorrect password' do
|
24
|
-
app = Rapns::Apns::App.new :
|
24
|
+
app = Rapns::Apns::App.new :name => 'test', :environment => 'development',
|
25
25
|
:certificate => TEST_CERT_WITH_PASSWORD, :password => 'incorrect'
|
26
26
|
app.valid?
|
27
27
|
app.errors[:certificate].should == ["Certificate value must contain a certificate and a private key."]
|