cable_room 0.5.5 → 0.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.
@@ -1,481 +0,0 @@
1
- require "spec_helper"
2
-
3
- # E2E Integration Tests for CableRoom
4
- #
5
- # These tests use the async ActionCable adapter (configured in spec_helper.rb)
6
- # instead of the test adapter to enable real message delivery during tests.
7
- # This allows us to test actual end-to-end communication flow between rooms
8
- # and room members, rather than just verifying that broadcasts were sent.
9
-
10
- RSpec.describe "CableRoom E2E Integration", type: :channel do
11
- # Setup a test room class for the e2e tests
12
- class E2ETestRoom < CableRoom::Room::Base
13
- cattr_accessor :events, default: []
14
- cattr_accessor :latest_instance
15
-
16
- after_startup do
17
- self.class.latest_instance = self
18
- self.class.events << { type: :room_started, key: key, timestamp: Time.current }
19
- self << { type: "room_ready", message: "Room is now active" }
20
- end
21
-
22
- on_user_joined do
23
- user_id = message_origin&.user
24
- self.class.events << { type: :user_joined, user: user_id, key: key, timestamp: Time.current }
25
- broadcast({ type: "user_joined_broadcast", user: user_id })
26
- end
27
-
28
- on_user_left do
29
- user_id = message_origin&.user
30
- self.class.events << { type: :user_left, user: user_id, key: key, timestamp: Time.current }
31
- broadcast({ type: "user_left_broadcast", user: user_id })
32
- end
33
-
34
- before_shutdown do
35
- self.class.events << { type: :room_shutting_down, key: key, timestamp: Time.current }
36
- end
37
-
38
- after_shutdown do
39
- self.class.events << { type: :room_shutdown, key: key, timestamp: Time.current }
40
- end
41
-
42
- # Custom message handlers
43
- def on_custom_message(msg)
44
- self.class.events << { type: :custom_message_received, data: msg['data'], key: key }
45
- broadcast({ type: "custom_response", original: msg['data'], processed: true })
46
- end
47
-
48
- def on_request_info(msg)
49
- message_origin << { type: "room_info", key: key, users: connected_users.count }
50
- end
51
- end
52
-
53
- # Setup a test channel for users to join rooms
54
- class E2ETestChannel < ApplicationCable::Channel
55
- include CableRoom::RoomMember
56
-
57
- attr_reader :room, :received_messages
58
-
59
- def initialize(*)
60
- super
61
- @received_messages = []
62
- end
63
-
64
- def current_user
65
- params[:user_id]
66
- end
67
-
68
- def subscribed
69
- key = params[:room_key] || SecureRandom.hex(8)
70
-
71
- @room = join_room(
72
- E2ETestRoom,
73
- key,
74
- create: params[:create] || false,
75
- tags: params[:tags] || [],
76
- as: current_user,
77
- on_joined: ->(membership) {
78
- @received_messages << { type: :on_joined_callback, timestamp: Time.current }
79
- },
80
- on_room_opened: ->(membership) {
81
- @received_messages << { type: :on_room_opened_callback, timestamp: Time.current }
82
- },
83
- on_room_closed: ->(membership) {
84
- @received_messages << { type: :on_room_closed_callback, timestamp: Time.current }
85
- },
86
- on_message: ->(message) {
87
- @received_messages << { type: :on_message_callback, message: message, timestamp: Time.current }
88
- }
89
- )
90
- end
91
-
92
- def unsubscribed
93
- @room&.leave!
94
- end
95
-
96
- def send_custom_message(data)
97
- @room << { type: 'custom_message', data: data }
98
- end
99
-
100
- def request_room_info
101
- @room << { type: 'request_info' }
102
- end
103
- end
104
-
105
- class ConnStub < ::ActionCable::Channel::ConnectionStub
106
- delegate :worker_pool, :logger, to: :server
107
- def initialize(...)
108
- super
109
- end
110
- end
111
-
112
- module ChannelStub
113
- def confirmed?
114
- subscription_confirmation_sent?
115
- end
116
-
117
- def rejected?
118
- subscription_rejected?
119
- end
120
-
121
- def streams; super; end
122
-
123
- # def stream_from(broadcasting, *)
124
- # super
125
- # # streams << broadcasting
126
- # end
127
-
128
- # def stream_from(broadcasting, callback = nil, coder: nil, &block)
129
- # broadcasting = String(broadcasting)
130
-
131
- # # Don't send the confirmation until pubsub#subscribe is successful
132
- # defer_subscription_confirmation!
133
-
134
- # # Build a stream handler by wrapping the user-provided callback with a decoder
135
- # # or defaulting to a JSON-decoding retransmitter.
136
- # handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder)
137
- # streams << broadcasting
138
-
139
- # connection.server.event_loop.post do
140
- # pubsub.subscribe(broadcasting, handler, lambda do
141
- # ensure_confirmation_sent
142
- # logger.info "#{self.class.name} is streaming from #{broadcasting}"
143
- # end)
144
- # end
145
- # end
146
-
147
- # def stop_all_streams
148
- # @_streams = []
149
- # end
150
-
151
- # def streams
152
- # @_streams ||= []
153
- # end
154
- end
155
-
156
- def subscribe(params={})
157
- @connection ||= stub_connection
158
- @subscription = self.class.channel_class.new(connection, "test_stub", params.with_indifferent_access)
159
- @subscription.singleton_class.include(ChannelStub)
160
- @subscription.subscribe_to_channel
161
- @subscription
162
- end
163
-
164
- before(:each) do
165
- @connection = ConnStub.new
166
- end
167
-
168
- describe E2ETestChannel, type: :channel do
169
- let(:room_key) { "e2e-test-#{SecureRandom.hex(4)}" }
170
-
171
- before(:each) do
172
- E2ETestRoom.events.clear
173
- E2ETestRoom.latest_instance = nil
174
- end
175
-
176
- describe "Room Creation and Lifecycle" do
177
- it "creates a room when a user connects with create: true" do
178
- expect {
179
- subscribe(room_key: room_key, create: true, user_id: "user1")
180
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(1)
181
-
182
- expect(subscription).to be_confirmed
183
- expect(E2ETestRoom.events).to include(hash_including(type: :room_started, key: room_key))
184
- end
185
-
186
- it "does not create a room when create: false" do
187
- expect {
188
- subscribe(room_key: room_key, create: false, user_id: "user1")
189
- }.not_to change { CableRoom::ChannelTracker.instance.room_channels.count }
190
- end
191
-
192
- it "properly shuts down a room when sent KILL message" do
193
- subscribe(room_key: room_key, create: true, user_id: "user1")
194
-
195
- initial_count = CableRoom::ChannelTracker.instance.room_channels.count
196
-
197
- E2ETestRoom.send_message(room_key, "KILL", port: :to_room)
198
- sleep 0.2 # Give time for shutdown to complete
199
-
200
- expect(CableRoom::ChannelTracker.instance.room_channels.count).to eq(initial_count - 1)
201
- expect(E2ETestRoom.events).to include(hash_including(type: :room_shutdown, key: room_key))
202
- end
203
- end
204
-
205
- describe "Port Connections" do
206
- it "establishes proper port connections for room communication" do
207
- subscribe(room_key: room_key, create: true, user_id: "user1")
208
-
209
- # Verify the expected streams are opened
210
- expect(subscription.streams.keys).to include(/E2ETestRoom:.*:from_room/)
211
- expect(subscription.streams.keys).to include(/E2ETestRoom:.*:[0-9a-f]{32}/) # Token-based port
212
- end
213
-
214
- it "creates separate port streams for different users" do
215
- # First user subscribes
216
- subscribe(room_key: room_key, create: true, user_id: "user1")
217
- first_subscription = subscription
218
-
219
- # Second user subscribes to same room
220
- subscribe(room_key: room_key, create: false, user_id: "user2")
221
- second_subscription = subscription
222
-
223
- # Should have the shared broadcast stream
224
- expect(first_subscription.streams.keys).to include(/E2ETestRoom:.*:from_room/)
225
- expect(second_subscription.streams.keys).to include(/E2ETestRoom:.*:from_room/)
226
- end
227
-
228
- it "supports tagged ports for selective broadcasting" do
229
- subscribe(room_key: room_key, create: true, user_id: "user1", tags: ["admin", "moderator"])
230
-
231
- # Give time for connection to establish
232
- sleep 0.1
233
-
234
- # User should be listening to tagged ports
235
- room = E2ETestRoom.latest_instance
236
- expect(room).not_to be_nil
237
- end
238
- end
239
-
240
- describe "User Joining and Leaving" do
241
- it "properly handles a user joining the room" do
242
- subscribe(room_key: room_key, create: true, user_id: "user1")
243
-
244
- # Give time for join callbacks to fire
245
- sleep 0.1
246
-
247
- expect(E2ETestRoom.events).to include(hash_including(type: :user_joined, user: "user1"))
248
- expect(subscription.received_messages).to include(hash_including(type: :on_joined_callback))
249
- end
250
-
251
- it "handles multiple users joining the same room" do
252
- # First user joins and creates room
253
- subscribe(room_key: room_key, create: true, user_id: "user1")
254
- sleep 0.1
255
-
256
- first_user_count = E2ETestRoom.events.count { |e| e[:type] == :user_joined }
257
-
258
- # Second user joins existing room
259
- subscribe(room_key: room_key, create: false, user_id: "user2")
260
- sleep 0.1
261
-
262
- # Should have two user_joined events
263
- expect(E2ETestRoom.events.count { |e| e[:type] == :user_joined }).to eq(first_user_count + 1)
264
-
265
- # Verify both users are tracked
266
- expect(E2ETestRoom.events.map { |e| e[:user] if e[:type] == :user_joined }.compact).to include("user1", "user2")
267
- end
268
-
269
- it "properly handles a user leaving the room" do
270
- subscribe(room_key: room_key, create: true, user_id: "user1")
271
- sleep 0.1
272
-
273
- # User leaves
274
- unsubscribe
275
- sleep 0.1
276
-
277
- expect(E2ETestRoom.events).to include(hash_including(type: :user_left, user: "user1"))
278
- end
279
-
280
- it "handles same user connecting from multiple ports" do
281
- # Same user subscribes twice (e.g., multiple browser tabs)
282
- subscribe(room_key: room_key, create: true, user_id: "user1")
283
- first_subscription = subscription
284
- sleep 0.1
285
-
286
- initial_join_count = E2ETestRoom.events.count { |e| e[:type] == :user_joined }
287
-
288
- subscribe(room_key: room_key, create: false, user_id: "user1")
289
- sleep 0.1
290
-
291
- # Should only trigger one user_joined event (same user)
292
- expect(E2ETestRoom.events.count { |e| e[:type] == :user_joined }).to eq(initial_join_count)
293
-
294
- # Close one connection
295
- first_subscription.unsubscribe_from_channel
296
- sleep 0.1
297
-
298
- # User should not have left yet (still has one connection)
299
- final_left_count = E2ETestRoom.events.count { |e| e[:type] == :user_left && e[:user] == "user1" }
300
- expect(final_left_count).to eq(0)
301
- end
302
- end
303
-
304
- describe "Message Broadcasting and Communication" do
305
- it "broadcasts messages to all connected users" do
306
- # Two users join
307
- subscribe(room_key: room_key, create: true, user_id: "user1")
308
- user1 = subscription
309
- sleep 0.1
310
-
311
- subscribe(room_key: room_key, create: false, user_id: "user2")
312
- user2 = subscription
313
- sleep 0.1
314
-
315
- # User 1 sends a custom message
316
- user1.send_custom_message({ text: "Hello everyone!" })
317
- sleep 0.1
318
-
319
- # Both users should receive the broadcast response
320
- expect(user1.received_messages).to include(
321
- hash_including(
322
- type: :on_message_callback,
323
- message: hash_including("type" => "custom_response", "processed" => true)
324
- )
325
- )
326
- expect(user2.received_messages).to include(
327
- hash_including(
328
- type: :on_message_callback,
329
- message: hash_including("type" => "custom_response", "processed" => true)
330
- )
331
- )
332
-
333
- # Room should have recorded the custom message
334
- expect(E2ETestRoom.events).to include(
335
- hash_including(type: :custom_message_received, data: { "text" => "Hello everyone!" })
336
- )
337
- end
338
-
339
- it "supports direct messaging to specific users via their port" do
340
- subscribe(room_key: room_key, create: true, user_id: "user1")
341
- user1 = subscription
342
- sleep 0.1
343
-
344
- subscribe(room_key: room_key, create: false, user_id: "user2")
345
- user2 = subscription
346
- sleep 0.1
347
-
348
- # User 2 requests room info (should get private response)
349
- user2.request_room_info
350
- sleep 0.1
351
-
352
- # User 2 should receive room info
353
- expect(user2.received_messages).to include(
354
- hash_including(
355
- type: :on_message_callback,
356
- message: hash_including("type" => "room_info")
357
- )
358
- )
359
- end
360
- end
361
-
362
- describe "Complete E2E Flow" do
363
- it "handles full lifecycle: create, multiple users join/leave, messages, shutdown" do
364
- # Step 1: First user creates room
365
- subscribe(room_key: room_key, create: true, user_id: "alice")
366
- alice = subscription
367
- sleep 0.1
368
-
369
- expect(CableRoom::ChannelTracker.instance.room_channels.count).to be > 0
370
- expect(E2ETestRoom.events).to include(hash_including(type: :room_started))
371
- expect(E2ETestRoom.events).to include(hash_including(type: :user_joined, user: "alice"))
372
-
373
- # Step 2: Second user joins
374
- subscribe(room_key: room_key, create: false, user_id: "bob")
375
- bob = subscription
376
- sleep 0.1
377
-
378
- expect(E2ETestRoom.events).to include(hash_including(type: :user_joined, user: "bob"))
379
-
380
- # Step 3: Third user joins
381
- subscribe(room_key: room_key, create: false, user_id: "charlie")
382
- charlie = subscription
383
- sleep 0.1
384
-
385
- expect(E2ETestRoom.events).to include(hash_including(type: :user_joined, user: "charlie"))
386
-
387
- # Step 4: Users exchange messages
388
- alice.send_custom_message({ from: "alice", text: "Hello room!" })
389
- sleep 0.1
390
-
391
- bob.send_custom_message({ from: "bob", text: "Hi alice!" })
392
- sleep 0.1
393
-
394
- # All users should have received broadcasts
395
- expect(alice.received_messages.count { |m| m[:type] == :on_message_callback && m[:message]["type"] == "custom_response" }).to eq(2)
396
- expect(bob.received_messages.count { |m| m[:type] == :on_message_callback && m[:message]["type"] == "custom_response" }).to eq(2)
397
- expect(charlie.received_messages.count { |m| m[:type] == :on_message_callback && m[:message]["type"] == "custom_response" }).to eq(2)
398
-
399
- # Step 5: One user leaves
400
- bob.unsubscribe_from_channel
401
- sleep 0.1
402
-
403
- expect(E2ETestRoom.events).to include(hash_including(type: :user_left, user: "bob"))
404
-
405
- # Step 6: Another message is sent (bob shouldn't receive it)
406
- bob_message_count_before = bob.received_messages.count
407
- alice.send_custom_message({ from: "alice", text: "Bob left!" })
408
- sleep 0.1
409
-
410
- expect(bob.received_messages.count).to eq(bob_message_count_before) # No new messages for bob
411
- expect(charlie.received_messages.count { |m| m[:type] == :on_message_callback && m[:message]["type"] == "custom_response" }).to eq(3)
412
-
413
- # Step 7: Shutdown the room
414
- room_count_before = CableRoom::ChannelTracker.instance.room_channels.count
415
- E2ETestRoom.send_message(room_key, "KILL", port: :to_room)
416
- sleep 0.2
417
-
418
- # Room should be shut down
419
- expect(CableRoom::ChannelTracker.instance.room_channels.count).to eq(room_count_before - 1)
420
- expect(E2ETestRoom.events).to include(hash_including(type: :room_shutting_down))
421
- expect(E2ETestRoom.events).to include(hash_including(type: :room_shutdown))
422
-
423
- # Users should receive room_closed notifications
424
- expect(alice.received_messages).to include(hash_including(type: :on_room_closed_callback))
425
- expect(charlie.received_messages).to include(hash_including(type: :on_room_closed_callback))
426
- end
427
- end
428
-
429
- describe "Room Reaping and Idle Timeout" do
430
- it "automatically shuts down idle rooms" do
431
- subscribe(room_key: room_key, create: true, user_id: "user1")
432
- room = E2ETestRoom.latest_instance
433
- vchan = room.instance_variable_get(:@cable_channel)
434
-
435
- unsubscribe
436
- sleep 0.1
437
-
438
- # Room should still exist (hasn't timed out yet)
439
- expect(CableRoom::ChannelTracker.instance.room_channels).to include(vchan)
440
-
441
- # Simulate watchdog timeout
442
- vchan.instance_variable_set(:@last_watchdog_ping_at, 2.hours.ago)
443
-
444
- room_count_before = CableRoom::ChannelTracker.instance.room_channels.count
445
- vchan.send(:check_room_watchdog)
446
- sleep 0.2
447
-
448
- # Room should be reaped
449
- expect(CableRoom::ChannelTracker.instance.room_channels.count).to eq(room_count_before - 1)
450
- expect(E2ETestRoom.events).to include(hash_including(type: :room_shutdown))
451
- end
452
- end
453
-
454
- describe "Concurrent Room Operations" do
455
- it "handles multiple rooms simultaneously" do
456
- room1_key = "concurrent-room-1-#{SecureRandom.hex(4)}"
457
- room2_key = "concurrent-room-2-#{SecureRandom.hex(4)}"
458
-
459
- # Create two separate rooms
460
- subscribe(room_key: room1_key, create: true, user_id: "user1")
461
- room1_sub = subscription
462
- sleep 0.1
463
-
464
- subscribe(room_key: room2_key, create: true, user_id: "user2")
465
- room2_sub = subscription
466
- sleep 0.1
467
-
468
- # Both rooms should exist
469
- expect(E2ETestRoom.events.count { |e| e[:type] == :room_started }).to be >= 2
470
-
471
- # Send messages in both rooms
472
- room1_sub.send_custom_message({ room: "room1" })
473
- room2_sub.send_custom_message({ room: "room2" })
474
- sleep 0.1
475
-
476
- # Each room should have processed its own message
477
- expect(E2ETestRoom.events.count { |e| e[:type] == :custom_message_received }).to eq(2)
478
- end
479
- end
480
- end
481
- end
@@ -1,113 +0,0 @@
1
- require "spec_helper"
2
-
3
- RSpec.describe CableRoom::RoomMember do
4
- RUN_KEY = SecureRandom.hex(8)
5
-
6
- class TestRoom < CableRoom::Room::Base
7
- cattr_accessor :latest_instance
8
-
9
- after_startup do
10
- self.class.latest_instance = self
11
- self << { type: "custom_started" }
12
- end
13
-
14
- before_shutdown do
15
-
16
- end
17
- end
18
-
19
- class TestChannel < ApplicationCable::Channel
20
- include CableRoom::RoomMember
21
-
22
- attr_reader :room
23
-
24
- def subscribed
25
- key = RUN_KEY + (params[:key] || SecureRandom.hex(8))
26
- @room = join_room(TestRoom, key, create: params[:create])
27
- end
28
-
29
- def unsubscribed
30
- @room.leave!
31
- end
32
- end
33
-
34
- describe TestChannel, type: :channel do
35
- before do
36
- stub_connection current_user: nil
37
- end
38
-
39
- it "provisions a room if it doesn't exist" do
40
- expect {
41
- subscribe create: true
42
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(1)
43
- unsubscribe
44
- end
45
-
46
- it "opens the expected streams" do
47
- key = 'ABC'
48
- subscribe key:, create: true
49
- expect(subscription).to be_confirmed
50
- expect(subscription).to have_stream_from(/TestRoom:\w+ABC:from_room/)
51
- expect(subscription).to have_stream_from(/TestRoom:\w+ABC:\w+/)
52
- unsubscribe
53
- end
54
-
55
- it "does not provision a room if `create: false`" do
56
- expect {
57
- subscribe create: false
58
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(0)
59
- end
60
-
61
- it "does not provision a duplicate room" do
62
- key = SecureRandom.hex(8)
63
- expect {
64
- subscribe key: key, create: true
65
- subscribe key: key, create: true
66
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(1)
67
- unsubscribe
68
- end
69
-
70
- it "can provision multiple rooms" do
71
- expect {
72
- subscribe key: 'A', create: true
73
- subscribe key: 'B', create: true
74
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(2)
75
- unsubscribe
76
- end
77
-
78
- it "does not shutdown immediately on unsubscribe" do
79
- subscribe create: true
80
- expect {
81
- unsubscribe
82
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(0)
83
- end
84
-
85
- it "shuts down when the room receives a KILL message" do
86
- subscribe create: true
87
- expect {
88
- room = subscription.room
89
- TestRoom.send_message(room.key, "KILL", port: :to_room)
90
- sleep 0.1
91
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(-1)
92
- end
93
-
94
- it "shuts down when the room is idle" do
95
- subscribe create: true
96
- unsubscribe
97
-
98
- room = TestRoom.latest_instance
99
- vchan = room.instance_variable_get(:@cable_channel)
100
- expect {
101
- room.send(:ping_watchdog)
102
- vchan.send(:check_room_watchdog)
103
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(0)
104
-
105
- vchan.instance_variable_set(:@last_watchdog_ping_at, 1.hour.ago)
106
-
107
- expect {
108
- vchan.send(:check_room_watchdog)
109
- sleep 0.1
110
- }.to change { CableRoom::ChannelTracker.instance.room_channels.count }.by(-1)
111
- end
112
- end
113
- end
@@ -1,2 +0,0 @@
1
- test:
2
- adapter: async
@@ -1,5 +0,0 @@
1
- test:
2
- database: cable-room-test
3
- adapter: postgresql
4
- encoding: unicode
5
- pool: 15
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Rails.application.routes.draw do
4
- # Add your own routes here, or remove this file if you don't have need for it.
5
- end
@@ -1,3 +0,0 @@
1
- test:
2
- service: Disk
3
- root: /Users/eknapp/code/CableRoom/tmp/storage
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ActiveRecord::Schema.define do
4
- # Set up any tables you need to exist for your test suite that don't belong
5
- # in migrations.
6
- end