11require 'rails_helper'
22
33RSpec . describe CollaborativeStreamsChannel , type : :channel do
4- pending "add some examples to (or delete) #{ __FILE__ } "
5- end
4+ let ( :user ) { create ( :user , :admin ) }
5+ let ( :stream ) { create ( :stream ) }
6+
7+ before do
8+ stub_connection current_user : user
9+ end
10+
11+ describe '#subscribed' do
12+ it 'successfully subscribes to collaborative_streams channel' do
13+ subscribe
14+
15+ expect ( subscription ) . to be_confirmed
16+ expect ( subscription ) . to have_stream_from ( "collaborative_streams" )
17+ end
18+
19+ it 'broadcasts initial presence' do
20+ expect { subscribe } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
21+ expect ( data [ :action ] ) . to eq ( "user_joined" )
22+ expect ( data [ :user_id ] ) . to eq ( user . id )
23+ expect ( data [ :user_email ] ) . to eq ( user . email )
24+ expect ( data [ :user_color ] ) . to be_present
25+ }
26+ end
27+ end
28+
29+ describe '#unsubscribed' do
30+ before { subscribe }
31+
32+ it 'broadcasts user departure' do
33+ expect { unsubscribe } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
34+ expect ( data [ :action ] ) . to eq ( "user_left" )
35+ expect ( data [ :user_id ] ) . to eq ( user . id )
36+ }
37+ end
38+
39+ it 'releases all locks' do
40+ # Simulate having a lock
41+ Redis . current . setex ( "stream_lock:#{ stream . id } :source" , 300 , user . id )
42+
43+ unsubscribe
44+
45+ expect ( Redis . current . get ( "stream_lock:#{ stream . id } :source" ) ) . to be_nil
46+ end
47+ end
48+
49+ describe '#start_editing' do
50+ before { subscribe }
51+
52+ it 'acquires lock when field is not locked' do
53+ perform :start_editing , stream_id : stream . id , field : 'source'
54+
55+ expect ( Redis . current . get ( "stream_lock:#{ stream . id } :source" ) ) . to eq ( user . id . to_s )
56+ end
57+
58+ it 'broadcasts lock acquired' do
59+ expect {
60+ perform :start_editing , stream_id : stream . id , field : 'source'
61+ } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
62+ expect ( data [ :action ] ) . to eq ( "lock_acquired" )
63+ expect ( data [ :stream_id ] ) . to eq ( stream . id )
64+ expect ( data [ :field ] ) . to eq ( 'source' )
65+ expect ( data [ :user_id ] ) . to eq ( user . id )
66+ }
67+ end
68+
69+ it 'rejects lock when field is already locked by another user' do
70+ other_user = create ( :user , :admin )
71+ Redis . current . setex ( "stream_lock:#{ stream . id } :source" , 300 , other_user . id )
72+
73+ expect {
74+ perform :start_editing , stream_id : stream . id , field : 'source'
75+ } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
76+ expect ( data [ :action ] ) . to eq ( "lock_denied" )
77+ expect ( data [ :stream_id ] ) . to eq ( stream . id )
78+ expect ( data [ :field ] ) . to eq ( 'source' )
79+ expect ( data [ :locked_by ] ) . to eq ( other_user . id )
80+ }
81+ end
82+
83+ it 'allows same user to re-acquire their own lock' do
84+ Redis . current . setex ( "stream_lock:#{ stream . id } :source" , 300 , user . id )
85+
86+ expect {
87+ perform :start_editing , stream_id : stream . id , field : 'source'
88+ } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
89+ expect ( data [ :action ] ) . to eq ( "lock_acquired" )
90+ }
91+ end
92+ end
93+
94+ describe '#stop_editing' do
95+ before do
96+ subscribe
97+ Redis . current . setex ( "stream_lock:#{ stream . id } :source" , 300 , user . id )
98+ end
99+
100+ it 'releases lock when user owns it' do
101+ perform :stop_editing , stream_id : stream . id , field : 'source'
102+
103+ expect ( Redis . current . get ( "stream_lock:#{ stream . id } :source" ) ) . to be_nil
104+ end
105+
106+ it 'broadcasts lock released' do
107+ expect {
108+ perform :stop_editing , stream_id : stream . id , field : 'source'
109+ } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
110+ expect ( data [ :action ] ) . to eq ( "lock_released" )
111+ expect ( data [ :stream_id ] ) . to eq ( stream . id )
112+ expect ( data [ :field ] ) . to eq ( 'source' )
113+ expect ( data [ :user_id ] ) . to eq ( user . id )
114+ }
115+ end
116+
117+ it 'does not release lock owned by another user' do
118+ other_user = create ( :user , :admin )
119+ Redis . current . setex ( "stream_lock:#{ stream . id } :source" , 300 , other_user . id )
120+
121+ perform :stop_editing , stream_id : stream . id , field : 'source'
122+
123+ expect ( Redis . current . get ( "stream_lock:#{ stream . id } :source" ) ) . to eq ( other_user . id . to_s )
124+ end
125+ end
126+
127+ describe '#update_field' do
128+ before do
129+ subscribe
130+ Redis . current . setex ( "stream_lock:#{ stream . id } :source" , 300 , user . id )
131+ end
132+
133+ it 'broadcasts field update when user has lock' do
134+ expect {
135+ perform :update_field , stream_id : stream . id , field : 'source' , value : 'New Source'
136+ } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
137+ expect ( data [ :action ] ) . to eq ( "field_updated" )
138+ expect ( data [ :stream_id ] ) . to eq ( stream . id )
139+ expect ( data [ :field ] ) . to eq ( 'source' )
140+ expect ( data [ :value ] ) . to eq ( 'New Source' )
141+ expect ( data [ :user_id ] ) . to eq ( user . id )
142+ }
143+ end
144+
145+ it 'does not broadcast when user does not have lock' do
146+ Redis . current . del ( "stream_lock:#{ stream . id } :source" )
147+
148+ expect {
149+ perform :update_field , stream_id : stream . id , field : 'source' , value : 'New Source'
150+ } . not_to have_broadcasted_to ( "collaborative_streams" )
151+ end
152+ end
153+
154+ describe '#cursor_position' do
155+ before { subscribe }
156+
157+ it 'broadcasts cursor position to other users' do
158+ expect {
159+ perform :cursor_position , stream_id : stream . id , field : 'source' , position : 10
160+ } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
161+ expect ( data [ :action ] ) . to eq ( "cursor_moved" )
162+ expect ( data [ :stream_id ] ) . to eq ( stream . id )
163+ expect ( data [ :field ] ) . to eq ( 'source' )
164+ expect ( data [ :position ] ) . to eq ( 10 )
165+ expect ( data [ :user_id ] ) . to eq ( user . id )
166+ }
167+ end
168+ end
169+
170+ describe '#request_presence' do
171+ before { subscribe }
172+
173+ it 'broadcasts presence list' do
174+ # Add some presence data
175+ Redis . current . hset ( "presence:collaborative_streams" , user . id , {
176+ email : user . email ,
177+ color : '#FF0000' ,
178+ joined_at : Time . current . to_i
179+ } . to_json )
180+
181+ expect {
182+ perform :request_presence
183+ } . to have_broadcasted_to ( "collaborative_streams" ) . with { |data |
184+ expect ( data [ :action ] ) . to eq ( "presence_list" )
185+ expect ( data [ :users ] ) . to be_an ( Array )
186+ expect ( data [ :users ] . first [ :id ] ) . to eq ( user . id )
187+ }
188+ end
189+ end
190+
191+ describe 'edge cases' do
192+ before { subscribe }
193+
194+ it 'handles missing stream_id gracefully' do
195+ expect {
196+ perform :start_editing , stream_id : nil , field : 'source'
197+ } . not_to raise_error
198+ end
199+
200+ it 'handles invalid field names' do
201+ expect {
202+ perform :start_editing , stream_id : stream . id , field : 'invalid_field'
203+ } . not_to raise_error
204+ end
205+
206+ it 'handles Redis connection errors' do
207+ allow ( Redis . current ) . to receive ( :setex ) . and_raise ( Redis ::ConnectionError )
208+
209+ expect {
210+ perform :start_editing , stream_id : stream . id , field : 'source'
211+ } . not_to raise_error
212+ end
213+ end
214+ end
0 commit comments