1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
|
# frozen_string_literal: true
require "active_support"
require "active_support/test_case"
require "active_support/core_ext/hash/indifferent_access"
require "action_dispatch"
require "action_dispatch/http/headers"
require "action_dispatch/testing/test_request"
module ActionCable
module Connection
class NonInferrableConnectionError < ::StandardError
def initialize(name)
super "Unable to determine the connection to test from #{name}. " +
"You'll need to specify it using `tests YourConnection` in your " +
"test case definition."
end
end
module Assertions
# Asserts that the connection is rejected (via +reject_unauthorized_connection+).
#
# # Asserts that connection without user_id fails
# assert_reject_connection { connect params: { user_id: '' } }
def assert_reject_connection(&block)
res =
begin
block.call
false
rescue ActionCable::Connection::Authorization::UnauthorizedError
true
end
assert res, "Expected to reject connection but no rejection were made"
end
end
# We don't want to use the whole "encryption stack" for connection
# unit-tests, but we want to make sure that users test against the correct types
# of cookies (i.e. signed or encrypted or plain)
class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
def signed
self[:signed] ||= {}.with_indifferent_access
end
def encrypted
self[:encrypted] ||= {}.with_indifferent_access
end
end
class TestRequest < ActionDispatch::TestRequest
attr_accessor :session, :cookie_jar
attr_writer :cookie_jar
end
module TestConnection
attr_reader :logger, :request
def initialize(request)
inner_logger = ActiveSupport::Logger.new(StringIO.new)
tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
@logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
@request = request
@env = request.env
end
end
# Superclass for Action Cable connection unit tests.
#
# == Basic example
#
# Unit tests are written as follows:
# 1. First, one uses the +connect+ method to simulate connection.
# 2. Then, one asserts whether the current state is as expected (e.g. identifiers).
#
# For example:
#
# module ApplicationCable
# class ConnectionTest < ActionCable::Connection::TestCase
# def test_connects_with_cookies
# cookies["user_id"] = users[:john].id
# # Simulate a connection
# connect
#
# # Asserts that the connection identifier is correct
# assert_equal "John", connection.user.name
# end
#
# def test_does_not_connect_without_user
# assert_reject_connection do
# connect
# end
# end
# end
# end
#
# You can also provide additional information about underlying HTTP request
# (params, headers, session and Rack env):
#
# def test_connect_with_headers_and_query_string
# connect "/cable?user_id=1", headers: { "X-API-TOKEN" => 'secret-my' }
#
# assert_equal connection.user_id, "1"
# end
#
# def test_connect_with_params
# connect params: { user_id: 1 }
#
# assert_equal connection.user_id, "1"
# end
#
# You can also manage request cookies:
#
# def test_connect_with_cookies
# # plain cookies
# cookies["user_id"] = 1
# # or signed/encrypted
# # cookies.signed["user_id"] = 1
#
# connect
#
# assert_equal connection.user_id, "1"
# end
#
# == Connection is automatically inferred
#
# ActionCable::Connection::TestCase will automatically infer the connection under test
# from the test class name. If the channel cannot be inferred from the test
# class name, you can explicitly set it with +tests+.
#
# class ConnectionTest < ActionCable::Connection::TestCase
# tests ApplicationCable::Connection
# end
#
class TestCase < ActiveSupport::TestCase
module Behavior
extend ActiveSupport::Concern
DEFAULT_PATH = "/cable"
include ActiveSupport::Testing::ConstantLookup
include Assertions
included do
class_attribute :_connection_class
attr_reader :connection
ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
end
module ClassMethods
def tests(connection)
case connection
when String, Symbol
self._connection_class = connection.to_s.camelize.constantize
when Module
self._connection_class = connection
else
raise NonInferrableConnectionError.new(connection)
end
end
def connection_class
if connection = self._connection_class
connection
else
tests determine_default_connection(name)
end
end
def determine_default_connection(name)
connection = determine_constant_from_test_name(name) do |constant|
Class === constant && constant < ActionCable::Connection::Base
end
raise NonInferrableConnectionError.new(name) if connection.nil?
connection
end
end
# Performs connection attempt (i.e. calls #connect method).
#
# Accepts request path as the first argument and the following request options:
# - params – url parameters (Hash)
# - headers – request headers (Hash)
# - session – session data (Hash)
# - env – addittional Rack env configuration (Hash)
def connect(path = ActionCable.server.config.mount_path, **request_params)
path ||= DEFAULT_PATH
connection = self.class.connection_class.allocate
connection.singleton_class.include(TestConnection)
connection.send(:initialize, build_test_request(path, request_params))
connection.connect if connection.respond_to?(:connect)
# Only set instance variable if connected successfully
@connection = connection
end
# Disconnect the connection under test (i.e. calls #disconnect)
def disconnect
raise "Must be connected!" if connection.nil?
connection.disconnect if connection.respond_to?(:disconnect)
@connection = nil
end
def cookies
@cookie_jar ||= TestCookieJar.new
end
private
def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
uri = URI.parse(path)
query_string = params.nil? ? uri.query : params.to_query
request_env = {
"QUERY_STRING" => query_string,
"PATH_INFO" => uri.path
}.merge(env)
if wrapped_headers.present?
ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
end
TestRequest.create(request_env).tap do |request|
request.session = session.with_indifferent_access
request.cookie_jar = cookies
end
end
end
include Behavior
end
end
end
|