noticed 1.3.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +76 -177
- data/lib/generators/noticed/model_generator.rb +10 -2
- data/lib/generators/noticed/templates/README +1 -1
- data/lib/noticed/base.rb +23 -7
- data/lib/noticed/coder.rb +2 -0
- data/lib/noticed/delivery_methods/action_cable.rb +13 -1
- data/lib/noticed/delivery_methods/base.rb +17 -10
- data/lib/noticed/delivery_methods/email.rb +28 -3
- data/lib/noticed/delivery_methods/fcm.rb +96 -0
- data/lib/noticed/delivery_methods/ios.rb +171 -0
- data/lib/noticed/engine.rb +4 -0
- data/lib/noticed/has_notifications.rb +21 -4
- data/lib/noticed/model.rb +17 -1
- data/lib/noticed/translation.rb +5 -1
- data/lib/noticed/version.rb +1 -1
- data/lib/noticed.rb +4 -11
- data/lib/rails_6_polyfills/actioncable/test_adapter.rb +70 -0
- data/lib/rails_6_polyfills/actioncable/test_helper.rb +143 -0
- data/lib/rails_6_polyfills/activejob/serializers.rb +240 -0
- data/lib/rails_6_polyfills/base.rb +18 -0
- metadata +11 -5
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "googleauth"
|
|
2
|
+
|
|
3
|
+
# class CommentNotifier
|
|
4
|
+
# deliver_by :fcm, credentials: Rails.root.join("config/certs/fcm.json"), format: :format_notification
|
|
5
|
+
#
|
|
6
|
+
# deliver_by :fcm, credentials: :fcm_credentials
|
|
7
|
+
# def fcm_credentials
|
|
8
|
+
# { project_id: "api-12345" }
|
|
9
|
+
# end
|
|
10
|
+
# end
|
|
11
|
+
|
|
12
|
+
module Noticed
|
|
13
|
+
module DeliveryMethods
|
|
14
|
+
class Fcm < Base
|
|
15
|
+
BASE_URI = "https://fcm.googleapis.com/v1/projects/"
|
|
16
|
+
|
|
17
|
+
option :format
|
|
18
|
+
|
|
19
|
+
def deliver
|
|
20
|
+
device_tokens.each do |device_token|
|
|
21
|
+
post("#{BASE_URI}#{project_id}/messages:send", headers: {authorization: "Bearer #{access_token}"}, json: {message: format(device_token)})
|
|
22
|
+
rescue ResponseUnsuccessful => exception
|
|
23
|
+
if exception.response.code == 404
|
|
24
|
+
cleanup_invalid_token(device_token)
|
|
25
|
+
else
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def cleanup_invalid_token(device_token)
|
|
32
|
+
return unless notification.respond_to?(:cleanup_device_token)
|
|
33
|
+
notification.send(:cleanup_device_token, token: device_token, platform: "fcm")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def credentials
|
|
37
|
+
@credentials ||= begin
|
|
38
|
+
option = options[:credentials]
|
|
39
|
+
credentials_hash = case option
|
|
40
|
+
when Hash
|
|
41
|
+
option
|
|
42
|
+
when Pathname
|
|
43
|
+
load_json(option)
|
|
44
|
+
when String
|
|
45
|
+
load_json(Rails.root.join(option))
|
|
46
|
+
when Symbol
|
|
47
|
+
notification.send(option)
|
|
48
|
+
else
|
|
49
|
+
Rails.application.credentials.fcm
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
credentials_hash.symbolize_keys
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_json(path)
|
|
57
|
+
JSON.parse(File.read(path))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def project_id
|
|
61
|
+
credentials[:project_id]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def access_token
|
|
65
|
+
token = authorizer.fetch_access_token!
|
|
66
|
+
token["access_token"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def authorizer
|
|
70
|
+
@authorizer ||= options.fetch(:authorizer, Google::Auth::ServiceAccountCredentials).make_creds(
|
|
71
|
+
json_key_io: StringIO.new(credentials.to_json),
|
|
72
|
+
scope: "https://www.googleapis.com/auth/firebase.messaging"
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def format(device_token)
|
|
77
|
+
notification.send(options[:format], device_token)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def device_tokens
|
|
81
|
+
if notification.respond_to?(:fcm_device_tokens)
|
|
82
|
+
Array.wrap(notification.fcm_device_tokens(recipient))
|
|
83
|
+
else
|
|
84
|
+
raise NoMethodError, <<~MESSAGE
|
|
85
|
+
You must implement `fcm_device_tokens` to send Firebase Cloud Messaging notifications
|
|
86
|
+
|
|
87
|
+
# This must return an Array of FCM device tokens
|
|
88
|
+
def fcm_device_tokens(recipient)
|
|
89
|
+
recipient.fcm_device_tokens.pluck(:token)
|
|
90
|
+
end
|
|
91
|
+
MESSAGE
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require "apnotic"
|
|
2
|
+
|
|
3
|
+
module Noticed
|
|
4
|
+
module DeliveryMethods
|
|
5
|
+
class Ios < Base
|
|
6
|
+
cattr_accessor :connection_pool
|
|
7
|
+
|
|
8
|
+
def deliver
|
|
9
|
+
raise ArgumentError, "bundle_identifier is missing" if bundle_identifier.blank?
|
|
10
|
+
raise ArgumentError, "key_id is missing" if key_id.blank?
|
|
11
|
+
raise ArgumentError, "team_id is missing" if team_id.blank?
|
|
12
|
+
raise ArgumentError, "Could not find APN cert at '#{cert_path}'" unless valid_cert_path?
|
|
13
|
+
|
|
14
|
+
device_tokens.each do |device_token|
|
|
15
|
+
connection_pool.with do |connection|
|
|
16
|
+
apn = Apnotic::Notification.new(device_token)
|
|
17
|
+
format_notification(apn)
|
|
18
|
+
|
|
19
|
+
response = connection.push(apn)
|
|
20
|
+
raise "Timeout sending iOS push notification" unless response
|
|
21
|
+
|
|
22
|
+
if bad_token?(response)
|
|
23
|
+
# Allow notification to cleanup invalid iOS device tokens
|
|
24
|
+
cleanup_invalid_token(device_token)
|
|
25
|
+
elsif !response.ok?
|
|
26
|
+
raise "Request failed #{response.body}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def format_notification(apn)
|
|
35
|
+
apn.topic = bundle_identifier
|
|
36
|
+
|
|
37
|
+
if (method = options[:format])
|
|
38
|
+
notification.send(method, apn)
|
|
39
|
+
elsif params[:message].present?
|
|
40
|
+
apn.alert = params[:message]
|
|
41
|
+
else
|
|
42
|
+
raise ArgumentError, "No message for iOS delivery. Either include message in params or add the 'format' option in 'deliver_by :ios'."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def device_tokens
|
|
47
|
+
if notification.respond_to?(:ios_device_tokens)
|
|
48
|
+
Array.wrap(notification.ios_device_tokens(recipient))
|
|
49
|
+
else
|
|
50
|
+
raise NoMethodError, <<~MESSAGE
|
|
51
|
+
You must implement `ios_device_tokens` to send iOS notifications
|
|
52
|
+
|
|
53
|
+
# This must return an Array of iOS device tokens
|
|
54
|
+
def ios_device_tokens(recipient)
|
|
55
|
+
recipient.ios_device_tokens.pluck(:token)
|
|
56
|
+
end
|
|
57
|
+
MESSAGE
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def bad_token?(response)
|
|
62
|
+
response.status == "410" || (response.status == "400" && response.body["reason"] == "BadDeviceToken")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cleanup_invalid_token(token)
|
|
66
|
+
return unless notification.respond_to?(:cleanup_device_token)
|
|
67
|
+
notification.send(:cleanup_device_token, token: token, platform: "iOS")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def connection_pool
|
|
71
|
+
self.class.connection_pool ||= new_connection_pool
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def new_connection_pool
|
|
75
|
+
handler = proc do |connection|
|
|
76
|
+
connection.on(:error) do |exception|
|
|
77
|
+
Rails.logger.info "Apnotic exception raised: #{exception}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if development?
|
|
82
|
+
Apnotic::ConnectionPool.development(connection_pool_options, pool_options, &handler)
|
|
83
|
+
else
|
|
84
|
+
Apnotic::ConnectionPool.new(connection_pool_options, pool_options, &handler)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def connection_pool_options
|
|
89
|
+
{
|
|
90
|
+
auth_method: :token,
|
|
91
|
+
cert_path: cert_path,
|
|
92
|
+
key_id: key_id,
|
|
93
|
+
team_id: team_id
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def bundle_identifier
|
|
98
|
+
option = options[:bundle_identifier]
|
|
99
|
+
case option
|
|
100
|
+
when String
|
|
101
|
+
option
|
|
102
|
+
when Symbol
|
|
103
|
+
notification.send(option)
|
|
104
|
+
else
|
|
105
|
+
Rails.application.credentials.dig(:ios, :bundle_identifier)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def cert_path
|
|
110
|
+
option = options[:cert_path]
|
|
111
|
+
case option
|
|
112
|
+
when String
|
|
113
|
+
option
|
|
114
|
+
when Symbol
|
|
115
|
+
notification.send(option)
|
|
116
|
+
else
|
|
117
|
+
Rails.root.join("config/certs/ios/apns.p8")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def key_id
|
|
122
|
+
option = options[:key_id]
|
|
123
|
+
case option
|
|
124
|
+
when String
|
|
125
|
+
option
|
|
126
|
+
when Symbol
|
|
127
|
+
notification.send(option)
|
|
128
|
+
else
|
|
129
|
+
Rails.application.credentials.dig(:ios, :key_id)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def team_id
|
|
134
|
+
option = options[:team_id]
|
|
135
|
+
case option
|
|
136
|
+
when String
|
|
137
|
+
option
|
|
138
|
+
when Symbol
|
|
139
|
+
notification.send(option)
|
|
140
|
+
else
|
|
141
|
+
Rails.application.credentials.dig(:ios, :team_id)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def development?
|
|
146
|
+
option = options[:development]
|
|
147
|
+
case option
|
|
148
|
+
when Symbol
|
|
149
|
+
!!notification.send(option)
|
|
150
|
+
else
|
|
151
|
+
!!option
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def valid_cert_path?
|
|
156
|
+
case cert_path
|
|
157
|
+
when File, StringIO
|
|
158
|
+
cert_path.size > 0
|
|
159
|
+
else
|
|
160
|
+
File.exist?(cert_path)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def pool_options
|
|
165
|
+
{
|
|
166
|
+
size: options.fetch(:pool_size, 5)
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
data/lib/noticed/engine.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Noticed
|
|
2
2
|
module HasNotifications
|
|
3
|
-
# Defines a method for the association and a
|
|
3
|
+
# Defines a method for the association and a before_destroy callback to remove notifications
|
|
4
4
|
# where this record is a param
|
|
5
5
|
#
|
|
6
6
|
# class User < ApplicationRecord
|
|
@@ -15,10 +15,19 @@ module Noticed
|
|
|
15
15
|
|
|
16
16
|
class_methods do
|
|
17
17
|
def has_noticed_notifications(param_name: model_name.singular, **options)
|
|
18
|
-
model = options.fetch(:model_name, "Notification").constantize
|
|
19
|
-
|
|
20
18
|
define_method "notifications_as_#{param_name}" do
|
|
21
|
-
model.
|
|
19
|
+
model = options.fetch(:model_name, "Notification").constantize
|
|
20
|
+
case current_adapter
|
|
21
|
+
when "postgresql", "postgis"
|
|
22
|
+
model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json)
|
|
23
|
+
when "mysql2"
|
|
24
|
+
model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json)
|
|
25
|
+
when "sqlite3"
|
|
26
|
+
model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json)
|
|
27
|
+
else
|
|
28
|
+
# This will perform an exact match which isn't ideal
|
|
29
|
+
model.where(params: {param_name.to_sym => self})
|
|
30
|
+
end
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
if options.fetch(:destroy, true)
|
|
@@ -28,5 +37,13 @@ module Noticed
|
|
|
28
37
|
end
|
|
29
38
|
end
|
|
30
39
|
end
|
|
40
|
+
|
|
41
|
+
def current_adapter
|
|
42
|
+
if ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
43
|
+
ActiveRecord::Base.connection_db_config.adapter
|
|
44
|
+
else
|
|
45
|
+
ActiveRecord::Base.connection_config[:adapter]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
31
48
|
end
|
|
32
49
|
end
|
data/lib/noticed/model.rb
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
module Noticed
|
|
2
2
|
module Model
|
|
3
|
+
DATABASE_ERROR_CLASS_NAMES = lambda {
|
|
4
|
+
classes = [ActiveRecord::NoDatabaseError]
|
|
5
|
+
classes << ActiveRecord::ConnectionNotEstablished
|
|
6
|
+
classes << Mysql2::Error if defined?(::Mysql2)
|
|
7
|
+
classes << PG::ConnectionBad if defined?(::PG)
|
|
8
|
+
classes
|
|
9
|
+
}.call.freeze
|
|
10
|
+
|
|
3
11
|
extend ActiveSupport::Concern
|
|
4
12
|
|
|
5
13
|
included do
|
|
@@ -32,7 +40,9 @@ module Noticed
|
|
|
32
40
|
else
|
|
33
41
|
Noticed::TextCoder
|
|
34
42
|
end
|
|
35
|
-
rescue
|
|
43
|
+
rescue *DATABASE_ERROR_CLASS_NAMES => _error
|
|
44
|
+
warn("Noticed was unable to bootstrap correctly as the database is unavailable.")
|
|
45
|
+
|
|
36
46
|
Noticed::TextCoder
|
|
37
47
|
end
|
|
38
48
|
end
|
|
@@ -42,6 +52,7 @@ module Noticed
|
|
|
42
52
|
@_notification ||= begin
|
|
43
53
|
instance = type.constantize.with(params)
|
|
44
54
|
instance.record = self
|
|
55
|
+
instance.recipient = recipient
|
|
45
56
|
instance
|
|
46
57
|
end
|
|
47
58
|
end
|
|
@@ -61,5 +72,10 @@ module Noticed
|
|
|
61
72
|
def read?
|
|
62
73
|
read_at?
|
|
63
74
|
end
|
|
75
|
+
|
|
76
|
+
# If a GlobalID record in params is no longer found, the params will default with a noticed_error key
|
|
77
|
+
def deserialize_error?
|
|
78
|
+
!!params[:noticed_error]
|
|
79
|
+
end
|
|
64
80
|
end
|
|
65
81
|
end
|
data/lib/noticed/translation.rb
CHANGED
|
@@ -7,6 +7,10 @@ module Noticed
|
|
|
7
7
|
:notifications
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
def class_scope
|
|
11
|
+
self.class.name.underscore.tr("/", ".")
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def translate(key, **options)
|
|
11
15
|
I18n.translate(scope_translation_key(key), **options)
|
|
12
16
|
end
|
|
@@ -14,7 +18,7 @@ module Noticed
|
|
|
14
18
|
|
|
15
19
|
def scope_translation_key(key)
|
|
16
20
|
if key.to_s.start_with?(".")
|
|
17
|
-
"#{i18n_scope}.#{
|
|
21
|
+
"#{i18n_scope}.#{class_scope}#{key}"
|
|
18
22
|
else
|
|
19
23
|
key
|
|
20
24
|
end
|
data/lib/noticed/version.rb
CHANGED
data/lib/noticed.rb
CHANGED
|
@@ -12,26 +12,19 @@ module Noticed
|
|
|
12
12
|
autoload :NotificationChannel, "noticed/notification_channel"
|
|
13
13
|
|
|
14
14
|
module DeliveryMethods
|
|
15
|
-
autoload :Base, "noticed/delivery_methods/base"
|
|
16
15
|
autoload :ActionCable, "noticed/delivery_methods/action_cable"
|
|
16
|
+
autoload :Base, "noticed/delivery_methods/base"
|
|
17
17
|
autoload :Database, "noticed/delivery_methods/database"
|
|
18
18
|
autoload :Email, "noticed/delivery_methods/email"
|
|
19
|
-
autoload :
|
|
19
|
+
autoload :Fcm, "noticed/delivery_methods/fcm"
|
|
20
|
+
autoload :Ios, "noticed/delivery_methods/ios"
|
|
20
21
|
autoload :MicrosoftTeams, "noticed/delivery_methods/microsoft_teams"
|
|
22
|
+
autoload :Slack, "noticed/delivery_methods/slack"
|
|
21
23
|
autoload :Test, "noticed/delivery_methods/test"
|
|
22
24
|
autoload :Twilio, "noticed/delivery_methods/twilio"
|
|
23
25
|
autoload :Vonage, "noticed/delivery_methods/vonage"
|
|
24
26
|
end
|
|
25
27
|
|
|
26
|
-
def self.notify(recipients:, notification:)
|
|
27
|
-
recipients.each do |recipient|
|
|
28
|
-
notification.notify(recipient)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Clear the recipient after sending to the group
|
|
32
|
-
notification.recipient = nil
|
|
33
|
-
end
|
|
34
|
-
|
|
35
28
|
mattr_accessor :parent_class
|
|
36
29
|
@@parent_class = "ApplicationJob"
|
|
37
30
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_cable/subscription_adapter/base"
|
|
4
|
+
require "action_cable/subscription_adapter/subscriber_map"
|
|
5
|
+
require "action_cable/subscription_adapter/async"
|
|
6
|
+
|
|
7
|
+
module ActionCable
|
|
8
|
+
module SubscriptionAdapter
|
|
9
|
+
# == Test adapter for Action Cable
|
|
10
|
+
#
|
|
11
|
+
# The test adapter should be used only in testing. Along with
|
|
12
|
+
# <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
|
|
13
|
+
#
|
|
14
|
+
# To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
|
|
15
|
+
#
|
|
16
|
+
# NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
|
|
17
|
+
# so it could be used in system tests too.
|
|
18
|
+
class Test < Async
|
|
19
|
+
def broadcast(channel, payload)
|
|
20
|
+
broadcasts(channel) << payload
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def broadcasts(channel)
|
|
25
|
+
channels_data[channel] ||= []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clear_messages(channel)
|
|
29
|
+
channels_data[channel] = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clear
|
|
33
|
+
@channels_data = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def channels_data
|
|
39
|
+
@channels_data ||= {}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Update how broadcast_for determines the channel name so it's consistent with the Rails 6 way
|
|
45
|
+
module Channel
|
|
46
|
+
module Broadcasting
|
|
47
|
+
delegate :broadcast_to, to: :class
|
|
48
|
+
module ClassMethods
|
|
49
|
+
def broadcast_to(model, message)
|
|
50
|
+
ActionCable.server.broadcast(broadcasting_for(model), message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def broadcasting_for(model)
|
|
54
|
+
serialize_broadcasting([channel_name, model])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def serialize_broadcasting(object) # :nodoc:
|
|
58
|
+
case # standard:disable Style/EmptyCaseCondition
|
|
59
|
+
when object.is_a?(Array)
|
|
60
|
+
object.map { |m| serialize_broadcasting(m) }.join(":")
|
|
61
|
+
when object.respond_to?(:to_gid_param)
|
|
62
|
+
object.to_gid_param
|
|
63
|
+
else
|
|
64
|
+
object.to_param
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionCable
|
|
4
|
+
# Have ActionCable pick its Test SubscriptionAdapter when it's called for in cable.yml
|
|
5
|
+
module Server
|
|
6
|
+
class Configuration
|
|
7
|
+
def pubsub_adapter
|
|
8
|
+
cable["adapter"] == "test" ? ActionCable::SubscriptionAdapter::Test : super
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Provides helper methods for testing Action Cable broadcasting
|
|
14
|
+
module TestHelper
|
|
15
|
+
def before_setup # :nodoc:
|
|
16
|
+
server = ActionCable.server
|
|
17
|
+
test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
|
|
18
|
+
|
|
19
|
+
@old_pubsub_adapter = server.pubsub
|
|
20
|
+
|
|
21
|
+
server.instance_variable_set(:@pubsub, test_adapter)
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def after_teardown # :nodoc:
|
|
26
|
+
super
|
|
27
|
+
ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Asserts that the number of broadcasted messages to the stream matches the given number.
|
|
31
|
+
#
|
|
32
|
+
# def test_broadcasts
|
|
33
|
+
# assert_broadcasts 'messages', 0
|
|
34
|
+
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
|
35
|
+
# assert_broadcasts 'messages', 1
|
|
36
|
+
# ActionCable.server.broadcast 'messages', { text: 'world' }
|
|
37
|
+
# assert_broadcasts 'messages', 2
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# If a block is passed, that block should cause the specified number of
|
|
41
|
+
# messages to be broadcasted.
|
|
42
|
+
#
|
|
43
|
+
# def test_broadcasts_again
|
|
44
|
+
# assert_broadcasts('messages', 1) do
|
|
45
|
+
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# assert_broadcasts('messages', 2) do
|
|
49
|
+
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
|
50
|
+
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
|
51
|
+
# end
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
def assert_broadcasts(stream, number)
|
|
55
|
+
if block_given?
|
|
56
|
+
original_count = broadcasts_size(stream)
|
|
57
|
+
yield
|
|
58
|
+
new_count = broadcasts_size(stream)
|
|
59
|
+
actual_count = new_count - original_count
|
|
60
|
+
else
|
|
61
|
+
actual_count = broadcasts_size(stream)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Asserts that no messages have been sent to the stream.
|
|
68
|
+
#
|
|
69
|
+
# def test_no_broadcasts
|
|
70
|
+
# assert_no_broadcasts 'messages'
|
|
71
|
+
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
|
72
|
+
# assert_broadcasts 'messages', 1
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
# If a block is passed, that block should not cause any message to be sent.
|
|
76
|
+
#
|
|
77
|
+
# def test_broadcasts_again
|
|
78
|
+
# assert_no_broadcasts 'messages' do
|
|
79
|
+
# # No job messages should be sent from this block
|
|
80
|
+
# end
|
|
81
|
+
# end
|
|
82
|
+
#
|
|
83
|
+
# Note: This assertion is simply a shortcut for:
|
|
84
|
+
#
|
|
85
|
+
# assert_broadcasts 'messages', 0, &block
|
|
86
|
+
#
|
|
87
|
+
def assert_no_broadcasts(stream, &block)
|
|
88
|
+
assert_broadcasts stream, 0, &block
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Asserts that the specified message has been sent to the stream.
|
|
92
|
+
#
|
|
93
|
+
# def test_assert_transmitted_message
|
|
94
|
+
# ActionCable.server.broadcast 'messages', text: 'hello'
|
|
95
|
+
# assert_broadcast_on('messages', text: 'hello')
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
# If a block is passed, that block should cause a message with the specified data to be sent.
|
|
99
|
+
#
|
|
100
|
+
# def test_assert_broadcast_on_again
|
|
101
|
+
# assert_broadcast_on('messages', text: 'hello') do
|
|
102
|
+
# ActionCable.server.broadcast 'messages', text: 'hello'
|
|
103
|
+
# end
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
def assert_broadcast_on(stream, data)
|
|
107
|
+
# Encode to JSON and back–we want to use this value to compare
|
|
108
|
+
# with decoded JSON.
|
|
109
|
+
# Comparing JSON strings doesn't work due to the order of the keys.
|
|
110
|
+
serialized_msg =
|
|
111
|
+
ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
|
|
112
|
+
|
|
113
|
+
new_messages = broadcasts(stream)
|
|
114
|
+
if block_given?
|
|
115
|
+
old_messages = new_messages
|
|
116
|
+
clear_messages(stream)
|
|
117
|
+
|
|
118
|
+
yield
|
|
119
|
+
new_messages = broadcasts(stream)
|
|
120
|
+
clear_messages(stream)
|
|
121
|
+
|
|
122
|
+
# Restore all sent messages
|
|
123
|
+
(old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
|
|
127
|
+
|
|
128
|
+
assert message, "No messages sent with #{data} to #{stream}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def pubsub_adapter # :nodoc:
|
|
132
|
+
ActionCable.server.pubsub
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
delegate :broadcasts, :clear_messages, to: :pubsub_adapter
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def broadcasts_size(channel)
|
|
140
|
+
broadcasts(channel).size
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|