bigbluebutton_rails 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -1
- data/CHANGELOG.rdoc +8 -0
- data/Gemfile +13 -7
- data/Gemfile.lock +112 -86
- data/README.rdoc +26 -80
- data/Rakefile +2 -1
- data/TODO_08 +13 -0
- data/app/controllers/bigbluebutton/rooms_controller.rb +9 -9
- data/app/controllers/bigbluebutton/servers_controller.rb +1 -1
- data/app/models/bigbluebutton_room.rb +16 -14
- data/app/models/bigbluebutton_server.rb +1 -1
- data/app/views/bigbluebutton/rooms/_form.html.erb +1 -1
- data/app/views/bigbluebutton/rooms/external.html.erb +3 -3
- data/app/views/bigbluebutton/rooms/invite.html.erb +3 -3
- data/app/views/bigbluebutton/servers/_form.html.erb +1 -1
- data/bigbluebutton_rails.gemspec +2 -2
- data/lib/bigbluebutton_rails/version.rb +1 -1
- data/spec/controllers/bigbluebutton/rooms_controller_spec.rb +42 -37
- data/spec/factories/bigbluebutton_room.rb +3 -1
- data/spec/models/bigbluebutton_room_db_spec.rb +34 -0
- data/spec/models/bigbluebutton_room_spec.rb +452 -457
- data/spec/models/bigbluebutton_server_db_spec.rb +14 -0
- data/spec/models/bigbluebutton_server_spec.rb +162 -176
- data/spec/rails_app/.gitignore +2 -1
- data/spec/rails_app/db/seeds.rb +16 -4
- data/spec/rails_app/features/activity_monitor_servers.feature +53 -0
- data/spec/rails_app/features/config.yml.example +13 -0
- data/spec/rails_app/features/create_rooms.feature +17 -0
- data/spec/rails_app/features/create_servers.feature +17 -0
- data/spec/rails_app/features/destroy_rooms.feature +12 -0
- data/spec/rails_app/features/destroy_servers.feature +11 -0
- data/spec/rails_app/features/edit_rooms.feature +26 -0
- data/spec/rails_app/features/edit_servers.feature +24 -0
- data/spec/rails_app/features/join_external_rooms.feature +61 -0
- data/spec/rails_app/features/join_mobile.feature +10 -0
- data/spec/rails_app/features/join_rooms.feature +117 -0
- data/spec/rails_app/features/list_and_show_rooms.feature +18 -0
- data/spec/rails_app/features/list_and_show_servers.feature +16 -0
- data/spec/rails_app/features/step_definitions/activity_monitor_servers_step.rb +102 -0
- data/spec/rails_app/features/step_definitions/common_steps.rb +99 -3
- data/spec/rails_app/features/step_definitions/create_rooms_steps.rb +38 -0
- data/spec/rails_app/features/step_definitions/{bigbluebutton_server_steps.rb → create_servers_steps.rb} +11 -6
- data/spec/rails_app/features/step_definitions/destroy_rooms_steps.rb +17 -0
- data/spec/rails_app/features/step_definitions/destroy_servers_steps.rb +15 -0
- data/spec/rails_app/features/step_definitions/edit_rooms_steps.rb +15 -0
- data/spec/rails_app/features/step_definitions/edit_servers_steps.rb +15 -0
- data/spec/rails_app/features/step_definitions/join_mobile_steps.rb +5 -0
- data/spec/rails_app/features/step_definitions/join_rooms_steps.rb +49 -0
- data/spec/rails_app/features/step_definitions/list_and_show_rooms_steps.rb +11 -0
- data/spec/rails_app/features/step_definitions/list_and_show_servers_steps.rb +11 -0
- data/spec/rails_app/features/support/configurations.rb +34 -0
- data/spec/rails_app/features/support/content.rb +27 -7
- data/spec/rails_app/features/support/env_custom.rb +21 -0
- data/spec/rails_app/features/support/factories/bigbluebutton_server_integration.rb +5 -0
- data/spec/rails_app/features/support/hooks.rb +14 -0
- data/spec/rails_app/features/support/locales.rb +18 -0
- data/spec/rails_app/features/support/paths.rb +25 -10
- data/spec/rails_app/features/support/selectors.rb +26 -0
- data/spec/rails_app/features/support/templates.rb +241 -0
- metadata +43 -20
- data/spec/factories/integration/bigbluebutton_server_integration.rb +0 -8
- data/spec/integration_conf.yml.example +0 -5
- data/spec/rails_app/features/join_external_bigbluebutton_rooms.feature +0 -19
- data/spec/rails_app/features/manage_bigbluebutton_rooms.feature +0 -9
- data/spec/rails_app/features/manage_bigbluebutton_servers.feature +0 -9
- data/spec/rails_app/features/step_definitions/bigbluebutton_room_steps.rb +0 -64
- data/spec/rails_app/features/support/env_gem.rb +0 -10
- data/spec/rails_app/features/support/forms.rb +0 -12
- data/spec/support/integration/integration_conf.rb +0 -16
data/Rakefile
CHANGED
@@ -56,8 +56,9 @@ end
|
|
56
56
|
task :cucumber do
|
57
57
|
if File.exists? "features/"
|
58
58
|
puts "* Gem features"
|
59
|
-
sh
|
59
|
+
sh "cucumber features/"
|
60
60
|
end
|
61
|
+
|
61
62
|
puts "* Dummy app features"
|
62
63
|
cd File.join(File.dirname(__FILE__), "spec", "rails_app")
|
63
64
|
sh "cucumber features/"
|
data/TODO_08
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
TODO for BigBlueButton 0.8:
|
2
|
+
|
3
|
+
See:
|
4
|
+
* http://code.google.com/p/bigbluebutton/wiki/API#Version_0.8
|
5
|
+
* http://groups.google.com/group/bigbluebutton-dev/browse_thread/thread/c214cbe9bdb2268a?pli=1
|
6
|
+
|
7
|
+
TODO:
|
8
|
+
* Support for recordings
|
9
|
+
* Keep track of older meetings?
|
10
|
+
* Pre-upload of slides
|
11
|
+
* New arguments in some methods (e.g. userID, record, duration, meta_*)
|
12
|
+
* New return values (e.g createTime).
|
13
|
+
* We don't need to randomize the meeting_id anymore (to confirm)
|
@@ -165,7 +165,7 @@ class Bigbluebutton::RoomsController < ApplicationController
|
|
165
165
|
role = @room.user_role(params[:user])
|
166
166
|
end
|
167
167
|
|
168
|
-
unless role.nil? or name.nil?
|
168
|
+
unless role.nil? or name.nil? or name.empty?
|
169
169
|
join_internal(name, role, :invite)
|
170
170
|
else
|
171
171
|
flash[:error] = t('bigbluebutton_rails.rooms.errors.auth.failure')
|
@@ -185,11 +185,11 @@ class Bigbluebutton::RoomsController < ApplicationController
|
|
185
185
|
# Authenticates an user using name and password passed in the params from #external
|
186
186
|
# Uses params[:meeting] to get the meetingID of the target room
|
187
187
|
def external_auth
|
188
|
-
@room = nil
|
189
188
|
if !params[:meeting].blank? && !params[:user].blank?
|
190
189
|
@server.fetch_meetings
|
191
190
|
@room = @server.meetings.select{ |r| r.meetingid == params[:meeting] }.first
|
192
191
|
else
|
192
|
+
@room = nil
|
193
193
|
message = t('bigbluebutton_rails.rooms.errors.auth.wrong_params')
|
194
194
|
redirect_to request.referer, :notice => message
|
195
195
|
return
|
@@ -202,11 +202,11 @@ class Bigbluebutton::RoomsController < ApplicationController
|
|
202
202
|
name = bigbluebutton_user.nil? ? params[:user][:name] : bigbluebutton_user.name
|
203
203
|
role = @room.user_role(params[:user])
|
204
204
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
redirect_to(
|
205
|
+
# FIXME: use internal_join ?
|
206
|
+
unless role.nil? or name.nil? or name.empty?
|
207
|
+
url = @room.perform_join(name, role, request)
|
208
|
+
unless url.nil?
|
209
|
+
redirect_to(url)
|
210
210
|
else
|
211
211
|
flash[:error] = t('bigbluebutton_rails.rooms.errors.auth.not_running')
|
212
212
|
render :external
|
@@ -264,13 +264,13 @@ class Bigbluebutton::RoomsController < ApplicationController
|
|
264
264
|
end
|
265
265
|
|
266
266
|
def join_mobile
|
267
|
-
@join_url =
|
267
|
+
@join_url = join_bigbluebutton_server_room_url(@server, @room, :mobile => '1')
|
268
268
|
@join_url.gsub!(/http:\/\//i, "bigbluebutton://")
|
269
269
|
|
270
270
|
# TODO: we can't use the mconf url because the mobile client scanning the qrcode is not
|
271
271
|
# logged. so we are using the full BBB url for now.
|
272
272
|
@qrcode_url = @room.join_url(bigbluebutton_user.name, bigbluebutton_role(@room))
|
273
|
-
@qrcode_url.gsub!(
|
273
|
+
@qrcode_url.gsub!(/http:\/\//i, "bigbluebutton://")
|
274
274
|
end
|
275
275
|
|
276
276
|
protected
|
@@ -34,7 +34,7 @@ class Bigbluebutton::ServersController < ApplicationController
|
|
34
34
|
|
35
35
|
# update_list works only for html
|
36
36
|
if params[:update_list] && (params[:format].nil? || params[:format].to_s == "html")
|
37
|
-
render :
|
37
|
+
render :partial => 'activity_list', :locals => { :server => @server }
|
38
38
|
return
|
39
39
|
end
|
40
40
|
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'active_support/secure_random'
|
2
|
-
|
3
1
|
class BigbluebuttonRoom < ActiveRecord::Base
|
4
2
|
belongs_to :server, :class_name => 'BigbluebuttonServer'
|
5
3
|
belongs_to :owner, :polymorphic => true
|
@@ -41,6 +39,9 @@ class BigbluebuttonRoom < ActiveRecord::Base
|
|
41
39
|
after_initialize :init
|
42
40
|
before_validation :set_param
|
43
41
|
|
42
|
+
# the full logout_url used when logout_url is a relative path
|
43
|
+
attr_accessor :full_logout_url
|
44
|
+
|
44
45
|
# Convenience method to access the attribute <tt>running</tt>
|
45
46
|
def is_running?
|
46
47
|
@running
|
@@ -201,10 +202,9 @@ class BigbluebuttonRoom < ActiveRecord::Base
|
|
201
202
|
def perform_join(username, role, request=nil)
|
202
203
|
fetch_is_running?
|
203
204
|
|
204
|
-
# if the
|
205
|
-
# and join it
|
205
|
+
# if the user is a moderator, create the room (if needed) and join it
|
206
206
|
if role == :moderator
|
207
|
-
add_domain_to_logout_url(request.protocol, request.
|
207
|
+
add_domain_to_logout_url(request.protocol, request.host_with_port) unless request.nil?
|
208
208
|
send_create unless is_running?
|
209
209
|
ret = join_url(username, role)
|
210
210
|
|
@@ -218,6 +218,7 @@ class BigbluebuttonRoom < ActiveRecord::Base
|
|
218
218
|
end
|
219
219
|
|
220
220
|
# add a domain name and/or protocol to the logout_url if needed
|
221
|
+
# it doesn't save in the db, just updates the instance
|
221
222
|
def add_domain_to_logout_url(protocol, host)
|
222
223
|
unless logout_url.nil?
|
223
224
|
url = logout_url.downcase
|
@@ -227,7 +228,7 @@ class BigbluebuttonRoom < ActiveRecord::Base
|
|
227
228
|
end
|
228
229
|
url = protocol + url
|
229
230
|
end
|
230
|
-
|
231
|
+
self.full_logout_url = url.downcase
|
231
232
|
end
|
232
233
|
end
|
233
234
|
|
@@ -248,29 +249,30 @@ class BigbluebuttonRoom < ActiveRecord::Base
|
|
248
249
|
end
|
249
250
|
|
250
251
|
def random_meetingid
|
251
|
-
#ActiveSupport::SecureRandom.hex(16)
|
252
252
|
# TODO temporarily using the name to get a friendlier meetingid
|
253
253
|
if self[:name].blank?
|
254
|
-
|
254
|
+
SecureRandom.hex(8)
|
255
255
|
else
|
256
|
-
self[:name] + '-' +
|
256
|
+
self[:name] + '-' + SecureRandom.random_number(9999).to_s
|
257
257
|
end
|
258
258
|
end
|
259
259
|
|
260
260
|
def random_voice_bridge
|
261
|
-
value = (70000 +
|
261
|
+
value = (70000 + SecureRandom.random_number(9999)).to_s
|
262
262
|
count = 1
|
263
263
|
while not BigbluebuttonRoom.find_by_voice_bridge(value).nil? and count < 10
|
264
264
|
count += 1
|
265
|
-
value = (70000 +
|
265
|
+
value = (70000 + SecureRandom.random_number(9999)).to_s
|
266
266
|
end
|
267
267
|
value
|
268
268
|
end
|
269
269
|
|
270
270
|
def do_create_meeting
|
271
|
-
|
272
|
-
|
273
|
-
|
271
|
+
opts = { :moderatorPW => self.moderator_password, :attendeePW => self.attendee_password,
|
272
|
+
:welcome => self.welcome_msg, :dialNumber => self.dial_number,
|
273
|
+
:logoutURL => self.full_logout_url || self.logout_url,
|
274
|
+
:maxParticipants => self.max_participants, :voiceBridge => self.voice_bridge }
|
275
|
+
self.server.api.create_meeting(self.name, self.meetingid, opts)
|
274
276
|
end
|
275
277
|
|
276
278
|
# if :param wasn't set, sets it as :name downcase and parameterized
|
@@ -7,7 +7,7 @@
|
|
7
7
|
<%= form_for [@server, @room], :url => url do |f| %>
|
8
8
|
<% if @room.errors.any? %>
|
9
9
|
<div id="error_explanation">
|
10
|
-
<h2><%= pluralize(@room.errors.count, "error")
|
10
|
+
<h2><%= pluralize(@room.errors.count, "error") %>:</h2>
|
11
11
|
<ul>
|
12
12
|
<% @room.errors.full_messages.each do |msg| %>
|
13
13
|
<li><%= msg %></li>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<h1>Join the meeting <%= @room.name %></h1>
|
2
2
|
|
3
|
-
<
|
3
|
+
<div id="error_explanation"><%= flash[:error] %></div>
|
4
4
|
|
5
5
|
<p>This meeting was created from another application.<br/>You must provide a name and a password to join:</p>
|
6
6
|
|
@@ -8,7 +8,7 @@
|
|
8
8
|
<%= hidden_field_tag "meeting", params[:meeting] %>
|
9
9
|
|
10
10
|
<div class="field">
|
11
|
-
Name
|
11
|
+
<label for="user_name">Name:</label><br />
|
12
12
|
<% if bigbluebutton_user.nil? %>
|
13
13
|
<%= text_field_tag "user[name]", "" %>
|
14
14
|
<% else %>
|
@@ -16,7 +16,7 @@
|
|
16
16
|
<% end %>
|
17
17
|
</div>
|
18
18
|
<div class="field">
|
19
|
-
Password
|
19
|
+
<label for="user_password">Password:</label><br />
|
20
20
|
<%= password_field_tag "user[password]", "" %>
|
21
21
|
</div>
|
22
22
|
|
@@ -28,7 +28,7 @@ $(document).ready(function(){
|
|
28
28
|
|
29
29
|
<h1>Invite to the meeting <%= @room.name %></h1>
|
30
30
|
|
31
|
-
<
|
31
|
+
<div id="error_explanation"><%= flash[:error] %></div>
|
32
32
|
|
33
33
|
<p>Meeting status: <span id="meeting_status">-</span> (<a href="javascript:ajax_request();">refresh</a>)</p>
|
34
34
|
<p><%= @room.name %> requires a name and/or password to join:</p>
|
@@ -36,7 +36,7 @@ $(document).ready(function(){
|
|
36
36
|
<%= form_tag join_bigbluebutton_server_room_path(@server, @room) do %>
|
37
37
|
|
38
38
|
<div class="field">
|
39
|
-
Name
|
39
|
+
<label for="user_name">Name:</label><br />
|
40
40
|
<% if bigbluebutton_user.nil? %>
|
41
41
|
<%= text_field_tag "user[name]", "" %>
|
42
42
|
<% else %>
|
@@ -44,7 +44,7 @@ $(document).ready(function(){
|
|
44
44
|
<% end %>
|
45
45
|
</div>
|
46
46
|
<div class="field">
|
47
|
-
Password
|
47
|
+
<label for="user_password">Password:</label><br />
|
48
48
|
<% if @room.private %>
|
49
49
|
<%= password_field_tag "user[password]", "" %>
|
50
50
|
<% else %>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<%= form_for(@server) do |f| %>
|
2
2
|
<% if @server.errors.any? %>
|
3
3
|
<div id="error_explanation">
|
4
|
-
<h2><%= pluralize(@server.errors.count, "error")
|
4
|
+
<h2><%= pluralize(@server.errors.count, "error") %>:</h2>
|
5
5
|
<ul>
|
6
6
|
<% @server.errors.full_messages.each do |msg| %>
|
7
7
|
<li><%= msg %></li>
|
data/bigbluebutton_rails.gemspec
CHANGED
@@ -15,6 +15,6 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
16
|
s.require_paths = ["lib"]
|
17
17
|
|
18
|
-
s.add_dependency("rails", ">= 3.0.
|
19
|
-
s.add_dependency("bigbluebutton-api-ruby", "~> 0.0
|
18
|
+
s.add_dependency("rails", ">= 3.0.0")
|
19
|
+
s.add_dependency("bigbluebutton-api-ruby", "~> 0.1.0")
|
20
20
|
end
|
@@ -49,7 +49,7 @@ describe Bigbluebutton::RoomsController do
|
|
49
49
|
mock_server_and_api
|
50
50
|
controller.stub(:bigbluebutton_user) { user }
|
51
51
|
controller.should_receive(:bigbluebutton_role).and_return(:moderator)
|
52
|
-
controller.should_receive(:
|
52
|
+
controller.should_receive(:join_bigbluebutton_server_room_url).
|
53
53
|
with(mocked_server, room, :mobile => '1').
|
54
54
|
and_return("http://test.com/join/url?mobile=1")
|
55
55
|
mocked_api.should_receive(:join_meeting_url).
|
@@ -482,6 +482,14 @@ describe Bigbluebutton::RoomsController do
|
|
482
482
|
it { should set_the_flash.to(I18n.t('bigbluebutton_rails.rooms.errors.auth.failure')) }
|
483
483
|
end
|
484
484
|
|
485
|
+
context "when name is set but empty" do
|
486
|
+
let(:user_hash) { { :password => room.moderator_password, :name => "" } }
|
487
|
+
it { should respond_with(:unauthorized) }
|
488
|
+
it { should assign_to(:room).with(room) }
|
489
|
+
it { should render_template(:invite) }
|
490
|
+
it { should set_the_flash.to(I18n.t('bigbluebutton_rails.rooms.errors.auth.failure')) }
|
491
|
+
end
|
492
|
+
|
485
493
|
context "when the password is wrong" do
|
486
494
|
let(:user_hash) { { :name => "Elftor", :password => nil } }
|
487
495
|
it { should respond_with(:unauthorized) }
|
@@ -534,8 +542,7 @@ describe Bigbluebutton::RoomsController do
|
|
534
542
|
end # #external
|
535
543
|
|
536
544
|
describe "#external_auth" do
|
537
|
-
let(:
|
538
|
-
let(:user_hash) { { :name => "Elftor", :password => new_room.attendee_password } }
|
545
|
+
let(:user_hash) { { :name => "Any Name", :password => new_room.attendee_password } }
|
539
546
|
let(:meetingid) { "my-meeting-id" }
|
540
547
|
let(:new_room) { BigbluebuttonRoom.new(:meetingid => meetingid,
|
541
548
|
:attendee_password => Forgery(:basic).password,
|
@@ -554,7 +561,7 @@ describe Bigbluebutton::RoomsController do
|
|
554
561
|
before { # TODO: this block is being repeated several times, put it in a method or something
|
555
562
|
mocked_server.should_receive(:fetch_meetings)
|
556
563
|
mocked_server.should_receive(:meetings).and_return(meetings)
|
557
|
-
new_room.should_receive(:
|
564
|
+
new_room.should_receive(:perform_join)
|
558
565
|
}
|
559
566
|
before(:each) { post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash }
|
560
567
|
it { should assign_to(:room).with(new_room) }
|
@@ -592,11 +599,6 @@ describe Bigbluebutton::RoomsController do
|
|
592
599
|
}.should raise_error(BigbluebuttonRails::RoomAccessDenied)
|
593
600
|
end
|
594
601
|
|
595
|
-
it "fetches meeting info" do
|
596
|
-
new_room.should_receive(:fetch_meeting_info)
|
597
|
-
post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash
|
598
|
-
end
|
599
|
-
|
600
602
|
context "validates user input and shows error" do
|
601
603
|
before(:each) { post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash }
|
602
604
|
|
@@ -608,6 +610,14 @@ describe Bigbluebutton::RoomsController do
|
|
608
610
|
it { should set_the_flash.to(I18n.t('bigbluebutton_rails.rooms.errors.auth.failure')) }
|
609
611
|
end
|
610
612
|
|
613
|
+
context "when name is empty not set" do
|
614
|
+
let(:user_hash) { { :password => room.moderator_password, :name => "" } }
|
615
|
+
it { should respond_with(:unauthorized) }
|
616
|
+
it { should assign_to(:room).with(new_room) }
|
617
|
+
it { should render_template(:external) }
|
618
|
+
it { should set_the_flash.to(I18n.t('bigbluebutton_rails.rooms.errors.auth.failure')) }
|
619
|
+
end
|
620
|
+
|
611
621
|
context "when the password is wrong" do
|
612
622
|
let(:user_hash) { { :name => "Elftor", :password => nil } }
|
613
623
|
it { should respond_with(:unauthorized) }
|
@@ -617,39 +627,34 @@ describe Bigbluebutton::RoomsController do
|
|
617
627
|
end
|
618
628
|
end
|
619
629
|
|
620
|
-
context "
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
with(new_room.meetingid, user.name, new_room.attendee_password). # here's the validation
|
630
|
-
and_return("http://test.com/attendee/join")
|
631
|
-
post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash
|
630
|
+
context "calls room#perform_join" do
|
631
|
+
context "and redirects to the url received" do
|
632
|
+
before {
|
633
|
+
new_room.should_receive(:perform_join).with(anything, :attendee, request).
|
634
|
+
and_return("http://test.com/attendee/join")
|
635
|
+
}
|
636
|
+
before(:each) { post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash }
|
637
|
+
it { should respond_with(:redirect) }
|
638
|
+
it { should redirect_to("http://test.com/attendee/join") }
|
632
639
|
end
|
633
640
|
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash
|
639
|
-
should respond_with(:
|
640
|
-
should
|
641
|
+
context "and shows error if it returns nil" do
|
642
|
+
before {
|
643
|
+
new_room.should_receive(:perform_join).with(user_hash[:name], :attendee, request).and_return(nil)
|
644
|
+
}
|
645
|
+
before(:each) { post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash }
|
646
|
+
it { should respond_with(:success) }
|
647
|
+
it { should render_template(:external) }
|
648
|
+
it { should set_the_flash.to(I18n.t('bigbluebutton_rails.rooms.errors.auth.not_running')) }
|
641
649
|
end
|
642
650
|
end
|
643
651
|
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
it { should respond_with(:success) }
|
651
|
-
it { should render_template(:external) }
|
652
|
-
it { should set_the_flash.to(I18n.t('bigbluebutton_rails.rooms.errors.auth.not_running')) }
|
652
|
+
it "if there's a user logged, should use his name" do
|
653
|
+
user = Factory.build(:user)
|
654
|
+
controller.stub(:bigbluebutton_user).and_return(user)
|
655
|
+
new_room.should_receive(:perform_join).with(user.name, anything, anything). # here's the validation
|
656
|
+
and_return("http://test.com/attendee/join")
|
657
|
+
post :external_auth, :server_id => mocked_server.to_param, :meeting => new_room.meetingid, :user => user_hash
|
653
658
|
end
|
654
659
|
|
655
660
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
Factory.define :bigbluebutton_room do |r|
|
2
|
+
# meetingid with a random factor to avoid duplicated ids in consecutive test runs
|
3
|
+
r.sequence(:meetingid) { |n| "meeting-#{n}-" + SecureRandom.hex(4) }
|
4
|
+
|
2
5
|
r.association :server, :factory => :bigbluebutton_server
|
3
|
-
r.sequence(:meetingid) { |n| "MeetingID#{n}" }
|
4
6
|
r.sequence(:name) { |n| "Name#{n}" }
|
5
7
|
r.attendee_password { Forgery(:basic).password :at_least => 10, :at_most => 16 }
|
6
8
|
r.moderator_password { Forgery(:basic).password :at_least => 10, :at_most => 16 }
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BigbluebuttonRoom do
|
4
|
+
|
5
|
+
# to ensure that the migration is correct
|
6
|
+
context "db" do
|
7
|
+
it { should have_db_column(:server_id).of_type(:integer) }
|
8
|
+
it { should have_db_column(:owner_id).of_type(:integer) }
|
9
|
+
it { should have_db_column(:owner_type).of_type(:string) }
|
10
|
+
it { should have_db_column(:meetingid).of_type(:string) }
|
11
|
+
it { should have_db_column(:name).of_type(:string) }
|
12
|
+
it { should have_db_column(:attendee_password).of_type(:string) }
|
13
|
+
it { should have_db_column(:moderator_password).of_type(:string) }
|
14
|
+
it { should have_db_column(:welcome_msg).of_type(:string) }
|
15
|
+
it { should have_db_column(:dial_number).of_type(:string) }
|
16
|
+
it { should have_db_column(:logout_url).of_type(:string) }
|
17
|
+
it { should have_db_column(:voice_bridge).of_type(:string) }
|
18
|
+
it { should have_db_column(:max_participants).of_type(:integer) }
|
19
|
+
it { should have_db_column(:private).of_type(:boolean) }
|
20
|
+
it { should have_db_column(:randomize_meetingid).of_type(:boolean) }
|
21
|
+
it { should have_db_column(:external).of_type(:boolean) }
|
22
|
+
it { should have_db_column(:param).of_type(:string) }
|
23
|
+
it { should have_db_index(:server_id) }
|
24
|
+
it { should have_db_index(:meetingid).unique(true) }
|
25
|
+
it { should have_db_index(:voice_bridge).unique(true) }
|
26
|
+
it "default values" do
|
27
|
+
room = BigbluebuttonRoom.new
|
28
|
+
room.private.should be_false
|
29
|
+
room.randomize_meetingid.should be_true
|
30
|
+
room.external.should be_false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -6,551 +6,546 @@ describe BigbluebuttonRoom do
|
|
6
6
|
BigbluebuttonRoom.new.should be_a_kind_of(ActiveRecord::Base)
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
it
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
9
|
+
before { Factory.create(:bigbluebutton_room) }
|
10
|
+
|
11
|
+
it { should belong_to(:server) }
|
12
|
+
it { should belong_to(:owner) }
|
13
|
+
it { should_not validate_presence_of(:owner_id) }
|
14
|
+
it { should_not validate_presence_of(:owner_type) }
|
15
|
+
|
16
|
+
it { should validate_presence_of(:server_id) }
|
17
|
+
it { should validate_presence_of(:meetingid) }
|
18
|
+
it { should validate_presence_of(:voice_bridge) }
|
19
|
+
it { should validate_presence_of(:name) }
|
20
|
+
it { should validate_presence_of(:param) }
|
21
|
+
|
22
|
+
it { should be_boolean(:private) }
|
23
|
+
it { should be_boolean(:randomize_meetingid) }
|
24
|
+
|
25
|
+
[:name, :server_id, :meetingid, :attendee_password, :moderator_password,
|
26
|
+
:welcome_msg, :owner, :server, :private, :logout_url, :dial_number,
|
27
|
+
:voice_bridge, :max_participants, :owner_id, :owner_type,
|
28
|
+
:randomize_meetingid, :param].
|
29
|
+
each do |attribute|
|
30
|
+
it { should allow_mass_assignment_of(attribute) }
|
31
|
+
end
|
32
|
+
it { should_not allow_mass_assignment_of(:id) }
|
33
|
+
|
34
|
+
it { should validate_uniqueness_of(:meetingid) }
|
35
|
+
it { should validate_uniqueness_of(:name) }
|
36
|
+
it { should validate_uniqueness_of(:voice_bridge) }
|
37
|
+
it { should validate_uniqueness_of(:param) }
|
38
|
+
|
39
|
+
it {
|
40
|
+
room = Factory.create(:bigbluebutton_room)
|
41
|
+
room.server.should_not be_nil
|
42
|
+
}
|
43
|
+
|
44
|
+
it { should ensure_length_of(:meetingid).is_at_least(1).is_at_most(100) }
|
45
|
+
it { should ensure_length_of(:name).is_at_least(1).is_at_most(150) }
|
46
|
+
it { should ensure_length_of(:attendee_password).is_at_most(16) }
|
47
|
+
it { should ensure_length_of(:moderator_password).is_at_most(16) }
|
48
|
+
it { should ensure_length_of(:welcome_msg).is_at_most(250) }
|
49
|
+
it { should ensure_length_of(:param).is_at_least(3) }
|
50
|
+
|
51
|
+
# attr_accessors
|
52
|
+
[:running, :participant_count, :moderator_count, :attendees,
|
53
|
+
:has_been_forcibly_ended, :start_time, :end_time, :external].each do |attr|
|
54
|
+
it { should respond_to(attr) }
|
55
|
+
it { should respond_to("#{attr}=") }
|
36
56
|
end
|
37
57
|
|
38
|
-
context do
|
39
|
-
|
40
|
-
|
41
|
-
it { should belong_to(:server) }
|
42
|
-
it { should belong_to(:owner) }
|
43
|
-
it { should_not validate_presence_of(:owner_id) }
|
44
|
-
it { should_not validate_presence_of(:owner_type) }
|
45
|
-
|
46
|
-
it { should validate_presence_of(:server_id) }
|
47
|
-
it { should validate_presence_of(:meetingid) }
|
48
|
-
it { should validate_presence_of(:voice_bridge) }
|
49
|
-
it { should validate_presence_of(:name) }
|
50
|
-
it { should validate_presence_of(:param) }
|
51
|
-
|
52
|
-
it { should be_boolean(:private) }
|
53
|
-
it { should be_boolean(:randomize_meetingid) }
|
54
|
-
|
55
|
-
[:name, :server_id, :meetingid, :attendee_password, :moderator_password,
|
56
|
-
:welcome_msg, :owner, :server, :private, :logout_url, :dial_number,
|
57
|
-
:voice_bridge, :max_participants, :owner_id, :owner_type,
|
58
|
-
:randomize_meetingid, :param].
|
59
|
-
each do |attribute|
|
60
|
-
it { should allow_mass_assignment_of(attribute) }
|
61
|
-
end
|
62
|
-
it { should_not allow_mass_assignment_of(:id) }
|
63
|
-
|
64
|
-
it { should validate_uniqueness_of(:meetingid) }
|
65
|
-
it { should validate_uniqueness_of(:name) }
|
66
|
-
it { should validate_uniqueness_of(:voice_bridge) }
|
67
|
-
it { should validate_uniqueness_of(:param) }
|
68
|
-
|
58
|
+
context ".to_param" do
|
59
|
+
it { should respond_to(:to_param) }
|
69
60
|
it {
|
70
|
-
|
71
|
-
|
61
|
+
r = Factory.create(:bigbluebutton_room)
|
62
|
+
r.to_param.should be(r.param)
|
72
63
|
}
|
64
|
+
end
|
73
65
|
|
74
|
-
|
75
|
-
it { should ensure_length_of(:name).is_at_least(1).is_at_most(150) }
|
76
|
-
it { should ensure_length_of(:attendee_password).is_at_most(16) }
|
77
|
-
it { should ensure_length_of(:moderator_password).is_at_most(16) }
|
78
|
-
it { should ensure_length_of(:welcome_msg).is_at_most(250) }
|
79
|
-
it { should ensure_length_of(:param).is_at_least(3) }
|
80
|
-
|
81
|
-
# attr_accessors
|
82
|
-
[:running, :participant_count, :moderator_count, :attendees,
|
83
|
-
:has_been_forcibly_ended, :start_time, :end_time, :external].each do |attr|
|
84
|
-
it { should respond_to(attr) }
|
85
|
-
it { should respond_to("#{attr}=") }
|
86
|
-
end
|
87
|
-
|
88
|
-
context ".to_param" do
|
89
|
-
it { should respond_to(:to_param) }
|
90
|
-
it {
|
91
|
-
r = Factory.create(:bigbluebutton_room)
|
92
|
-
r.to_param.should be(r.param)
|
93
|
-
}
|
94
|
-
end
|
66
|
+
it { should respond_to(:is_running?) }
|
95
67
|
|
96
|
-
|
68
|
+
describe "#user_role" do
|
69
|
+
let(:room) { Factory.build(:bigbluebutton_room, :moderator_password => "mod", :attendee_password => "att") }
|
70
|
+
it { should respond_to(:user_role) }
|
71
|
+
it { room.user_role({ :password => room.moderator_password }).should == :moderator }
|
72
|
+
it { room.user_role({ :password => room.attendee_password }).should == :attendee }
|
73
|
+
it { room.user_role({ :password => "wrong" }).should == nil }
|
74
|
+
it { room.user_role({ :password => nil }).should == nil }
|
75
|
+
it { room.user_role({ :not_password => "any" }).should == nil }
|
76
|
+
end
|
97
77
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
78
|
+
describe "#instance_variables_compare" do
|
79
|
+
let(:room) { Factory.create(:bigbluebutton_room) }
|
80
|
+
let(:room2) { BigbluebuttonRoom.last }
|
81
|
+
it { should respond_to(:instance_variables_compare) }
|
82
|
+
it { room.instance_variables_compare(room2).should be_empty }
|
83
|
+
it "compares instance variables" do
|
84
|
+
room2.running = !room.running
|
85
|
+
room.instance_variables_compare(room2).should_not be_empty
|
86
|
+
room.instance_variables_compare(room2).should include(:@running)
|
106
87
|
end
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
let(:room2) { BigbluebuttonRoom.last }
|
111
|
-
it { should respond_to(:instance_variables_compare) }
|
112
|
-
it { room.instance_variables_compare(room2).should be_empty }
|
113
|
-
it "compares instance variables" do
|
114
|
-
room2.running = !room.running
|
115
|
-
room.instance_variables_compare(room2).should_not be_empty
|
116
|
-
room.instance_variables_compare(room2).should include(:@running)
|
117
|
-
end
|
118
|
-
it "ignores attributes" do
|
119
|
-
room2.private = !room.private
|
120
|
-
room.instance_variables_compare(room2).should be_empty
|
121
|
-
end
|
88
|
+
it "ignores attributes" do
|
89
|
+
room2.private = !room.private
|
90
|
+
room.instance_variables_compare(room2).should be_empty
|
122
91
|
end
|
92
|
+
end
|
123
93
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
end
|
134
|
-
it "compares attributes" do
|
135
|
-
room2.private = !room.private
|
136
|
-
room.attr_equal?(room2).should be_false
|
137
|
-
end
|
138
|
-
it "compares objects" do
|
139
|
-
room2 = room.clone
|
140
|
-
room.attr_equal?(room2).should be_false
|
141
|
-
end
|
94
|
+
describe "#attr_equal?" do
|
95
|
+
before { Factory.create(:bigbluebutton_room) }
|
96
|
+
let(:room) { BigbluebuttonRoom.last }
|
97
|
+
let(:room2) { BigbluebuttonRoom.last }
|
98
|
+
it { should respond_to(:attr_equal?) }
|
99
|
+
it { room.attr_equal?(room2).should be_true }
|
100
|
+
it "compares instance variables" do
|
101
|
+
room2.running = !room.running
|
102
|
+
room.attr_equal?(room2).should be_false
|
142
103
|
end
|
104
|
+
it "compares attributes" do
|
105
|
+
room2.private = !room.private
|
106
|
+
room.attr_equal?(room2).should be_false
|
107
|
+
end
|
108
|
+
it "compares objects" do
|
109
|
+
room2 = room.clone
|
110
|
+
room.attr_equal?(room2).should be_false
|
111
|
+
end
|
112
|
+
end
|
143
113
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
114
|
+
context "initializes" do
|
115
|
+
let(:room) { BigbluebuttonRoom.new }
|
116
|
+
|
117
|
+
it "fetched attributes before they are fetched" do
|
118
|
+
room.participant_count.should == 0
|
119
|
+
room.moderator_count.should == 0
|
120
|
+
room.running.should be_false
|
121
|
+
room.has_been_forcibly_ended.should be_false
|
122
|
+
room.start_time.should be_nil
|
123
|
+
room.end_time.should be_nil
|
124
|
+
room.attendees.should == []
|
125
|
+
end
|
156
126
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
127
|
+
context "meetingid" do
|
128
|
+
it { room.meetingid.should_not be_nil }
|
129
|
+
it {
|
130
|
+
b = BigbluebuttonRoom.new(:meetingid => "user defined")
|
131
|
+
b.meetingid.should == "user defined"
|
132
|
+
}
|
133
|
+
end
|
164
134
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
end
|
135
|
+
context "voice_bridge" do
|
136
|
+
it {
|
137
|
+
b = BigbluebuttonRoom.new(:voice_bridge => "user defined")
|
138
|
+
b.voice_bridge.should == "user defined"
|
139
|
+
}
|
140
|
+
context "with a random value" do
|
141
|
+
it { room.voice_bridge.should_not be_nil }
|
142
|
+
it { room.voice_bridge.should =~ /7[0-9]{4}/ }
|
143
|
+
it "tries to randomize 10 times if voice_bridge already exists" do
|
144
|
+
room = Factory.create(:bigbluebutton_room, :voice_bridge => "70000")
|
145
|
+
BigbluebuttonRoom.stub!(:find_by_voice_bridge).and_return(room)
|
146
|
+
SecureRandom.should_receive(:random_number).exactly(10).and_return(0000)
|
147
|
+
room2 = BigbluebuttonRoom.new # triggers the random_number calls
|
148
|
+
room2.voice_bridge.should == "70000"
|
180
149
|
end
|
181
150
|
end
|
182
151
|
end
|
152
|
+
end
|
183
153
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
154
|
+
context "param format" do
|
155
|
+
let(:msg) { I18n.t('bigbluebutton_rails.rooms.errors.param_format') }
|
156
|
+
it { should validate_format_of(:param).not_with("123 321").with_message(msg) }
|
157
|
+
it { should validate_format_of(:param).not_with("").with_message(msg) }
|
158
|
+
it { should validate_format_of(:param).not_with("ab@c").with_message(msg) }
|
159
|
+
it { should validate_format_of(:param).not_with("ab#c").with_message(msg) }
|
160
|
+
it { should validate_format_of(:param).not_with("ab$c").with_message(msg) }
|
161
|
+
it { should validate_format_of(:param).not_with("ab%c").with_message(msg) }
|
162
|
+
it { should validate_format_of(:param).not_with("ábcd").with_message(msg) }
|
163
|
+
it { should validate_format_of(:param).not_with("-abc").with_message(msg) }
|
164
|
+
it { should validate_format_of(:param).not_with("abc-").with_message(msg) }
|
165
|
+
it { should validate_format_of(:param).with("_abc").with_message(msg) }
|
166
|
+
it { should validate_format_of(:param).with("abc_").with_message(msg) }
|
167
|
+
it { should validate_format_of(:param).with("abc") }
|
168
|
+
it { should validate_format_of(:param).with("123") }
|
169
|
+
it { should validate_format_of(:param).with("abc-123_d5") }
|
170
|
+
end
|
201
171
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
end
|
211
|
-
it "empty" do
|
212
|
-
@room = Factory.build(:bigbluebutton_room, :param => "",
|
213
|
-
:name => "-My Name@ _Is Odd_-")
|
214
|
-
end
|
172
|
+
context "sets param as the downcased parameterized name if param is" do
|
173
|
+
after :each do
|
174
|
+
@room.save.should be_true
|
175
|
+
@room.param.should == @room.name.downcase.parameterize
|
176
|
+
end
|
177
|
+
it "nil" do
|
178
|
+
@room = Factory.build(:bigbluebutton_room, :param => nil,
|
179
|
+
:name => "-My Name@ _Is Odd_-")
|
215
180
|
end
|
181
|
+
it "empty" do
|
182
|
+
@room = Factory.build(:bigbluebutton_room, :param => "",
|
183
|
+
:name => "-My Name@ _Is Odd_-")
|
184
|
+
end
|
185
|
+
end
|
216
186
|
|
217
|
-
|
218
|
-
|
219
|
-
|
187
|
+
context "using the api" do
|
188
|
+
before { mock_server_and_api }
|
189
|
+
let(:room) { Factory.create(:bigbluebutton_room) }
|
220
190
|
|
221
|
-
|
191
|
+
describe "#fetch_is_running?" do
|
222
192
|
|
223
|
-
|
193
|
+
it { should respond_to(:fetch_is_running?) }
|
224
194
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
it "fetches is_running? when running" do
|
234
|
-
mocked_api.should_receive(:is_meeting_running?).with(room.meetingid).and_return(true)
|
235
|
-
room.server = mocked_server
|
236
|
-
room.fetch_is_running?
|
237
|
-
room.running.should == true
|
238
|
-
room.is_running?.should == true
|
239
|
-
end
|
195
|
+
it "fetches is_running? when not running" do
|
196
|
+
mocked_api.should_receive(:is_meeting_running?).with(room.meetingid).and_return(false)
|
197
|
+
room.server = mocked_server
|
198
|
+
room.fetch_is_running?
|
199
|
+
room.running.should == false
|
200
|
+
room.is_running?.should == false
|
201
|
+
end
|
240
202
|
|
203
|
+
it "fetches is_running? when running" do
|
204
|
+
mocked_api.should_receive(:is_meeting_running?).with(room.meetingid).and_return(true)
|
205
|
+
room.server = mocked_server
|
206
|
+
room.fetch_is_running?
|
207
|
+
room.running.should == true
|
208
|
+
room.is_running?.should == true
|
241
209
|
end
|
242
210
|
|
243
|
-
|
211
|
+
end
|
244
212
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
let(:users) {
|
253
|
-
[
|
254
|
-
{:userID=>"ndw1fnaev0rj", :fullName=>"House M.D.", :role=>:moderator},
|
255
|
-
{:userID=>"gn9e22b7ynna", :fullName=>"Dexter Morgan", :role=>:moderator},
|
256
|
-
{:userID=>"llzihbndryc3", :fullName=>"Cameron Palmer", :role=>:viewer},
|
257
|
-
{:userID=>"rbepbovolsxt", :fullName=>"Trinity", :role=>:viewer}
|
258
|
-
]
|
213
|
+
describe "#fetch_meeting_info" do
|
214
|
+
|
215
|
+
# these hashes should be exactly as returned by bigbluebutton-api-ruby to be sure we are testing it right
|
216
|
+
let(:hash_info) {
|
217
|
+
{ :returncode=>true, :meetingID=>"test_id", :attendeePW=>"1234", :moderatorPW=>"4321",
|
218
|
+
:running=>false, :hasBeenForciblyEnded=>false, :startTime=>nil, :endTime=>nil,
|
219
|
+
:participantCount=>0, :moderatorCount=>0, :attendees=>[], :messageKey=>"", :message=>""
|
259
220
|
}
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
221
|
+
}
|
222
|
+
let(:users) {
|
223
|
+
[
|
224
|
+
{:userID=>"ndw1fnaev0rj", :fullName=>"House M.D.", :role=>:moderator},
|
225
|
+
{:userID=>"gn9e22b7ynna", :fullName=>"Dexter Morgan", :role=>:moderator},
|
226
|
+
{:userID=>"llzihbndryc3", :fullName=>"Cameron Palmer", :role=>:viewer},
|
227
|
+
{:userID=>"rbepbovolsxt", :fullName=>"Trinity", :role=>:viewer}
|
228
|
+
]
|
229
|
+
}
|
230
|
+
let(:hash_info2) {
|
231
|
+
{ :returncode=>true, :meetingID=>"test_id", :attendeePW=>"1234", :moderatorPW=>"4321",
|
232
|
+
:running=>true, :hasBeenForciblyEnded=>false, :startTime=>DateTime.parse("Wed Apr 06 17:09:57 UTC 2011"),
|
233
|
+
:endTime=>nil, :participantCount=>4, :moderatorCount=>2,
|
234
|
+
:attendees=>users, :messageKey=>{ }, :message=>{ }
|
266
235
|
}
|
236
|
+
}
|
267
237
|
|
268
|
-
|
269
|
-
|
270
|
-
it "fetches meeting info when the meeting is not running" do
|
271
|
-
mocked_api.should_receive(:get_meeting_info).
|
272
|
-
with(room.meetingid, room.moderator_password).and_return(hash_info)
|
273
|
-
room.server = mocked_server
|
238
|
+
it { should respond_to(:fetch_meeting_info) }
|
274
239
|
|
275
|
-
|
276
|
-
|
277
|
-
room.
|
278
|
-
|
279
|
-
room.moderator_count.should == 0
|
280
|
-
room.start_time.should == nil
|
281
|
-
room.end_time.should == nil
|
282
|
-
room.attendees.should == []
|
283
|
-
end
|
240
|
+
it "fetches meeting info when the meeting is not running" do
|
241
|
+
mocked_api.should_receive(:get_meeting_info).
|
242
|
+
with(room.meetingid, room.moderator_password).and_return(hash_info)
|
243
|
+
room.server = mocked_server
|
284
244
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
245
|
+
room.fetch_meeting_info
|
246
|
+
room.running.should == false
|
247
|
+
room.has_been_forcibly_ended.should == false
|
248
|
+
room.participant_count.should == 0
|
249
|
+
room.moderator_count.should == 0
|
250
|
+
room.start_time.should == nil
|
251
|
+
room.end_time.should == nil
|
252
|
+
room.attendees.should == []
|
253
|
+
end
|
289
254
|
|
290
|
-
|
291
|
-
|
292
|
-
room.
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
255
|
+
it "fetches meeting info when the meeting is running" do
|
256
|
+
mocked_api.should_receive(:get_meeting_info).
|
257
|
+
with(room.meetingid, room.moderator_password).and_return(hash_info2)
|
258
|
+
room.server = mocked_server
|
259
|
+
|
260
|
+
room.fetch_meeting_info
|
261
|
+
room.running.should == true
|
262
|
+
room.has_been_forcibly_ended.should == false
|
263
|
+
room.participant_count.should == 4
|
264
|
+
room.moderator_count.should == 2
|
265
|
+
room.start_time.should == DateTime.parse("Wed Apr 06 17:09:57 UTC 2011")
|
266
|
+
room.end_time.should == nil
|
267
|
+
|
268
|
+
users.each do |att|
|
269
|
+
attendee = BigbluebuttonAttendee.new
|
270
|
+
attendee.from_hash(att)
|
271
|
+
room.attendees.should include(attendee)
|
303
272
|
end
|
304
|
-
|
305
273
|
end
|
306
274
|
|
307
|
-
|
275
|
+
end
|
308
276
|
|
309
|
-
|
277
|
+
describe "#send_end" do
|
310
278
|
|
311
|
-
|
312
|
-
mocked_api.should_receive(:end_meeting).with(room.meetingid, room.moderator_password)
|
313
|
-
room.server = mocked_server
|
314
|
-
room.send_end
|
315
|
-
end
|
279
|
+
it { should respond_to(:send_end) }
|
316
280
|
|
281
|
+
it "send end_meeting" do
|
282
|
+
mocked_api.should_receive(:end_meeting).with(room.meetingid, room.moderator_password)
|
283
|
+
room.server = mocked_server
|
284
|
+
room.send_end
|
317
285
|
end
|
318
286
|
|
319
|
-
|
320
|
-
let(:attendee_password) { Forgery(:basic).password }
|
321
|
-
let(:moderator_password) { Forgery(:basic).password }
|
322
|
-
let(:hash_create) {
|
323
|
-
{
|
324
|
-
:returncode => "SUCCESS", :meetingID => "test_id",
|
325
|
-
:attendeePW => attendee_password, :moderatorPW => moderator_password,
|
326
|
-
:hasBeenForciblyEnded => "false", :messageKey => {}, :message => {}
|
327
|
-
}
|
328
|
-
}
|
329
|
-
|
330
|
-
it { should respond_to(:send_create) }
|
331
|
-
|
332
|
-
context "send create_meeting" do
|
333
|
-
|
334
|
-
context "for a stored room" do
|
335
|
-
before do
|
336
|
-
mocked_api.should_receive(:create_meeting).
|
337
|
-
with(room.name, room.meetingid, room.moderator_password,
|
338
|
-
room.attendee_password, room.welcome_msg, room.dial_number,
|
339
|
-
room.logout_url, room.max_participants, room.voice_bridge).
|
340
|
-
and_return(hash_create)
|
341
|
-
room.server = mocked_server
|
342
|
-
room.send_create
|
343
|
-
end
|
344
|
-
it { room.attendee_password.should be(attendee_password) }
|
345
|
-
it { room.moderator_password.should be(moderator_password) }
|
346
|
-
it { room.changed?.should be_false }
|
347
|
-
end
|
287
|
+
end
|
348
288
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
end
|
360
|
-
it { new_room.attendee_password.should be(attendee_password) }
|
361
|
-
it { new_room.moderator_password.should be(moderator_password) }
|
362
|
-
it("do not save the record") { new_room.new_record?.should be_true }
|
363
|
-
end
|
289
|
+
describe "#send_create" do
|
290
|
+
let(:attendee_password) { Forgery(:basic).password }
|
291
|
+
let(:moderator_password) { Forgery(:basic).password }
|
292
|
+
let(:hash_create) {
|
293
|
+
{
|
294
|
+
:returncode => "SUCCESS", :meetingID => "test_id",
|
295
|
+
:attendeePW => attendee_password, :moderatorPW => moderator_password,
|
296
|
+
:hasBeenForciblyEnded => "false", :messageKey => {}, :message => {}
|
297
|
+
}
|
298
|
+
}
|
364
299
|
|
365
|
-
|
300
|
+
it { should respond_to(:send_create) }
|
366
301
|
|
367
|
-
|
368
|
-
let(:fail_hash) { { :returncode => true, :meetingID => "new id",
|
369
|
-
:messageKey => "duplicateWarning" } }
|
370
|
-
let(:success_hash) { { :returncode => true, :meetingID => "new id",
|
371
|
-
:messageKey => "" } }
|
372
|
-
let(:new_id) { "new id" }
|
373
|
-
before {
|
374
|
-
room.randomize_meetingid = true
|
375
|
-
room.server = mocked_server
|
376
|
-
}
|
302
|
+
context "send create_meeting" do
|
377
303
|
|
378
|
-
|
379
|
-
|
304
|
+
context "for a stored room" do
|
305
|
+
before do
|
306
|
+
hash = hash_including(:moderatorPW => room.moderator_password, :attendeePW => room.attendee_password,
|
307
|
+
:welcome => room.welcome_msg, :dialNumber => room.dial_number,
|
308
|
+
:logoutURL => room.logout_url, :maxParticipants => room.max_participants,
|
309
|
+
:voiceBridge => room.voice_bridge)
|
380
310
|
mocked_api.should_receive(:create_meeting).
|
381
|
-
with(room.name,
|
382
|
-
|
383
|
-
room.logout_url, room.max_participants, room.voice_bridge)
|
384
|
-
room.send_create
|
385
|
-
end
|
386
|
-
|
387
|
-
it "and tries again on error" do
|
388
|
-
# fails twice and them succeds
|
389
|
-
room.should_receive(:random_meetingid).exactly(3).times.and_return(new_id)
|
390
|
-
mocked_api.should_receive(:create_meeting).
|
391
|
-
with(room.name, new_id, room.moderator_password,
|
392
|
-
room.attendee_password, room.welcome_msg, room.dial_number,
|
393
|
-
room.logout_url, room.max_participants, room.voice_bridge).
|
394
|
-
twice.
|
395
|
-
and_return(fail_hash)
|
396
|
-
mocked_api.should_receive(:create_meeting).
|
397
|
-
with(room.name, new_id, room.moderator_password,
|
398
|
-
room.attendee_password, room.welcome_msg, room.dial_number,
|
399
|
-
room.logout_url, room.max_participants, room.voice_bridge).
|
400
|
-
once.
|
401
|
-
and_return(success_hash)
|
311
|
+
with(room.name, room.meetingid, hash).and_return(hash_create)
|
312
|
+
room.server = mocked_server
|
402
313
|
room.send_create
|
403
314
|
end
|
315
|
+
it { room.attendee_password.should be(attendee_password) }
|
316
|
+
it { room.moderator_password.should be(moderator_password) }
|
317
|
+
it { room.changed?.should be_false }
|
318
|
+
end
|
404
319
|
|
405
|
-
|
406
|
-
|
320
|
+
context "for a new record" do
|
321
|
+
let(:new_room) { Factory.build(:bigbluebutton_room) }
|
322
|
+
before do
|
323
|
+
hash = hash_including(:moderatorPW => new_room.moderator_password, :attendeePW => new_room.attendee_password,
|
324
|
+
:welcome => new_room.welcome_msg, :dialNumber => new_room.dial_number,
|
325
|
+
:logoutURL => new_room.logout_url, :maxParticipants => new_room.max_participants,
|
326
|
+
:voiceBridge => new_room.voice_bridge)
|
407
327
|
mocked_api.should_receive(:create_meeting).
|
408
|
-
with(
|
409
|
-
|
410
|
-
|
411
|
-
exactly(10).times.
|
412
|
-
and_return(fail_hash)
|
413
|
-
room.send_create
|
328
|
+
with(new_room.name, new_room.meetingid, hash).and_return(hash_create)
|
329
|
+
new_room.server = mocked_server
|
330
|
+
new_room.send_create
|
414
331
|
end
|
332
|
+
it { new_room.attendee_password.should be(attendee_password) }
|
333
|
+
it { new_room.moderator_password.should be(moderator_password) }
|
334
|
+
it("and do not save the record") { new_room.new_record?.should be_true }
|
415
335
|
end
|
416
336
|
|
417
337
|
end
|
418
338
|
|
419
|
-
|
420
|
-
let(:
|
339
|
+
context "randomizes meetingid" do
|
340
|
+
let(:fail_hash) { { :returncode => true, :meetingID => "new id",
|
341
|
+
:messageKey => "duplicateWarning" } }
|
342
|
+
let(:success_hash) { { :returncode => true, :meetingID => "new id",
|
343
|
+
:messageKey => "" } }
|
344
|
+
let(:new_id) { "new id" }
|
345
|
+
before {
|
346
|
+
room.randomize_meetingid = true
|
347
|
+
room.server = mocked_server
|
348
|
+
}
|
421
349
|
|
422
|
-
it
|
350
|
+
it "before calling create" do
|
351
|
+
room.should_receive(:random_meetingid).and_return(new_id)
|
352
|
+
hash = hash_including(:moderatorPW => room.moderator_password, :attendeePW => room.attendee_password,
|
353
|
+
:welcome => room.welcome_msg, :dialNumber => room.dial_number,
|
354
|
+
:logoutURL => room.logout_url, :maxParticipants => room.max_participants,
|
355
|
+
:voiceBridge => room.voice_bridge)
|
356
|
+
mocked_api.should_receive(:create_meeting).with(room.name, new_id, hash)
|
357
|
+
room.send_create
|
358
|
+
end
|
423
359
|
|
424
|
-
it "
|
425
|
-
|
426
|
-
|
427
|
-
room.
|
428
|
-
|
360
|
+
it "and tries again on error" do
|
361
|
+
# fails twice and them succeds
|
362
|
+
room.should_receive(:random_meetingid).exactly(3).times.and_return(new_id)
|
363
|
+
hash = hash_including(:moderatorPW => room.moderator_password, :attendeePW => room.attendee_password,
|
364
|
+
:welcome => room.welcome_msg, :dialNumber => room.dial_number,
|
365
|
+
:logoutURL => room.logout_url, :maxParticipants => room.max_participants,
|
366
|
+
:voiceBridge => room.voice_bridge)
|
367
|
+
mocked_api.should_receive(:create_meeting).
|
368
|
+
with(room.name, new_id, hash).twice.and_return(fail_hash)
|
369
|
+
mocked_api.should_receive(:create_meeting).
|
370
|
+
with(room.name, new_id, hash).once.and_return(success_hash)
|
371
|
+
room.send_create
|
429
372
|
end
|
430
373
|
|
431
|
-
it "
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
374
|
+
it "and limits to 10 tries" do
|
375
|
+
room.should_receive(:random_meetingid).exactly(11).times.and_return(new_id)
|
376
|
+
hash = hash_including(:moderatorPW => room.moderator_password, :attendeePW => room.attendee_password,
|
377
|
+
:welcome => room.welcome_msg, :dialNumber => room.dial_number,
|
378
|
+
:logoutURL => room.logout_url, :maxParticipants => room.max_participants,
|
379
|
+
:voiceBridge => room.voice_bridge)
|
380
|
+
mocked_api.should_receive(:create_meeting).
|
381
|
+
with(room.name, new_id, hash).exactly(10).times.and_return(fail_hash)
|
382
|
+
room.send_create
|
436
383
|
end
|
384
|
+
end
|
437
385
|
|
438
|
-
|
439
|
-
|
440
|
-
|
386
|
+
context "uses #full_logout_url when set" do
|
387
|
+
before do
|
388
|
+
room.full_logout_url = "full-version-of-logout-url"
|
389
|
+
hash = hash_including(:moderatorPW => room.moderator_password, :attendeePW => room.attendee_password,
|
390
|
+
:welcome => room.welcome_msg, :dialNumber => room.dial_number,
|
391
|
+
:logoutURL => "full-version-of-logout-url", :maxParticipants => room.max_participants,
|
392
|
+
:voiceBridge => room.voice_bridge)
|
393
|
+
mocked_api.should_receive(:create_meeting).
|
394
|
+
with(room.name, room.meetingid, hash).and_return(hash_create)
|
441
395
|
room.server = mocked_server
|
442
|
-
room.join_url(username, nil, 'pass')
|
443
396
|
end
|
397
|
+
it { room.send_create }
|
444
398
|
end
|
445
399
|
|
446
400
|
end
|
447
401
|
|
448
|
-
|
449
|
-
|
450
|
-
let (:room) { Factory.build(:bigbluebutton_room, :private => true) }
|
451
|
-
it { room.should_not allow_value('').for(:moderator_password) }
|
452
|
-
it { room.should_not allow_value('').for(:attendee_password) }
|
453
|
-
end
|
402
|
+
describe "#join_url" do
|
403
|
+
let(:username) { Forgery(:name).full_name }
|
454
404
|
|
455
|
-
|
456
|
-
let (:room) { Factory.build(:bigbluebutton_room, :private => false) }
|
457
|
-
it { room.should allow_value('').for(:moderator_password) }
|
458
|
-
it { room.should allow_value('').for(:attendee_password) }
|
459
|
-
end
|
460
|
-
end
|
405
|
+
it { should respond_to(:join_url) }
|
461
406
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
407
|
+
it "with moderator role" do
|
408
|
+
mocked_api.should_receive(:join_meeting_url).
|
409
|
+
with(room.meetingid, username, room.moderator_password)
|
410
|
+
room.server = mocked_server
|
411
|
+
room.join_url(username, :moderator)
|
467
412
|
end
|
468
413
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
414
|
+
it "with attendee role" do
|
415
|
+
mocked_api.should_receive(:join_meeting_url).
|
416
|
+
with(room.meetingid, username, room.attendee_password)
|
417
|
+
room.server = mocked_server
|
418
|
+
room.join_url(username, :attendee)
|
473
419
|
end
|
474
420
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
421
|
+
it "without a role" do
|
422
|
+
mocked_api.should_receive(:join_meeting_url).
|
423
|
+
with(room.meetingid, username, 'pass')
|
424
|
+
room.server = mocked_server
|
425
|
+
room.join_url(username, nil, 'pass')
|
479
426
|
end
|
427
|
+
end
|
480
428
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
429
|
+
end
|
430
|
+
|
431
|
+
context "validates passwords" do
|
432
|
+
context "for private rooms" do
|
433
|
+
let (:room) { Factory.build(:bigbluebutton_room, :private => true) }
|
434
|
+
it { room.should_not allow_value('').for(:moderator_password) }
|
435
|
+
it { room.should_not allow_value('').for(:attendee_password) }
|
486
436
|
end
|
487
437
|
|
488
|
-
|
489
|
-
let(:room) { Factory.
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
context "when the conference is running" do
|
496
|
-
before {
|
497
|
-
room.should_receive(:is_running?).and_return(true)
|
498
|
-
room.should_receive(:join_url).with(user.name, :attendee).
|
499
|
-
and_return("http://test.com/attendee/join")
|
500
|
-
}
|
501
|
-
subject { room.perform_join(user.name, :attendee) }
|
502
|
-
it { should == "http://test.com/attendee/join" }
|
503
|
-
end
|
438
|
+
context "for public rooms" do
|
439
|
+
let (:room) { Factory.build(:bigbluebutton_room, :private => false) }
|
440
|
+
it { room.should allow_value('').for(:moderator_password) }
|
441
|
+
it { room.should allow_value('').for(:attendee_password) }
|
442
|
+
end
|
443
|
+
end
|
504
444
|
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
445
|
+
describe "#add_domain_to_logout_url" do
|
446
|
+
context "when logout_url has a path only" do
|
447
|
+
let(:room) { Factory.create(:bigbluebutton_room, :logout_url => '/only/path') }
|
448
|
+
before(:each) { room.add_domain_to_logout_url("HTTP://", "test.com:80") }
|
449
|
+
it { room.full_logout_url.should == "http://test.com:80/only/path" }
|
450
|
+
it { room.logout_url.should == "/only/path" }
|
451
|
+
it { BigbluebuttonRoom.find(room.id).logout_url.should == "/only/path" }
|
452
|
+
end
|
453
|
+
|
454
|
+
context "when logout_url has a path and domain" do
|
455
|
+
let(:room) { Factory.create(:bigbluebutton_room, :logout_url => 'other.com/only/path') }
|
456
|
+
before(:each) { room.add_domain_to_logout_url("HTTP://", "test.com:80") }
|
457
|
+
it { room.full_logout_url.should == "http://other.com/only/path" }
|
458
|
+
it { room.logout_url.should == "other.com/only/path" }
|
459
|
+
it { BigbluebuttonRoom.find(room.id).logout_url.should == "other.com/only/path" }
|
460
|
+
end
|
461
|
+
|
462
|
+
context "when logout_url has a path, domain and protocol" do
|
463
|
+
let(:room) { Factory.create(:bigbluebutton_room, :logout_url => 'HTTPS://other.com/only/path') }
|
464
|
+
before(:each) { room.add_domain_to_logout_url("HTTP://", "test.com:80") }
|
465
|
+
it { room.full_logout_url.should == "https://other.com/only/path" }
|
466
|
+
it { room.logout_url.should == "HTTPS://other.com/only/path" }
|
467
|
+
it { BigbluebuttonRoom.find(room.id).logout_url.should == "HTTPS://other.com/only/path" }
|
468
|
+
end
|
469
|
+
|
470
|
+
context "does nothing if logout_url is nil" do
|
471
|
+
let(:room) { Factory.create(:bigbluebutton_room, :logout_url => nil) }
|
472
|
+
before(:each) { room.add_domain_to_logout_url("HTTP://", "test.com:80") }
|
473
|
+
it { room.full_logout_url.should be_nil }
|
474
|
+
it { room.logout_url.should be_nil }
|
475
|
+
it { BigbluebuttonRoom.find(room.id).logout_url.should be_nil }
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
describe "#perform_join" do
|
480
|
+
let(:room) { Factory.create(:bigbluebutton_room) }
|
481
|
+
let(:user) { Factory.build(:user) }
|
482
|
+
|
483
|
+
context "for an attendee" do
|
484
|
+
before { room.should_receive(:fetch_is_running?) }
|
485
|
+
|
486
|
+
context "when the conference is running" do
|
487
|
+
before {
|
488
|
+
room.should_receive(:is_running?).and_return(true)
|
489
|
+
room.should_receive(:join_url).with(user.name, :attendee).
|
490
|
+
and_return("http://test.com/attendee/join")
|
491
|
+
}
|
492
|
+
subject { room.perform_join(user.name, :attendee) }
|
493
|
+
it { should == "http://test.com/attendee/join" }
|
510
494
|
end
|
511
495
|
|
512
|
-
context "
|
513
|
-
before { room.should_receive(:
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
room.should_receive(:join_url).with(user.name, :moderator).
|
519
|
-
and_return("http://test.com/moderator/join")
|
520
|
-
}
|
521
|
-
subject { room.perform_join(user.name, :moderator) }
|
522
|
-
it { should == "http://test.com/moderator/join" }
|
523
|
-
end
|
496
|
+
context "when the conference is not running" do
|
497
|
+
before { room.should_receive(:is_running?).and_return(false) }
|
498
|
+
subject { room.perform_join(user.name, :attendee) }
|
499
|
+
it { should be_nil }
|
500
|
+
end
|
501
|
+
end
|
524
502
|
|
525
|
-
|
526
|
-
|
527
|
-
room.should_receive(:is_running?).and_return(false)
|
528
|
-
room.should_receive(:send_create)
|
529
|
-
room.should_receive(:join_url).with(user.name, :moderator).
|
530
|
-
and_return("http://test.com/moderator/join")
|
531
|
-
}
|
532
|
-
subject { room.perform_join(user.name, :moderator) }
|
533
|
-
it { should == "http://test.com/moderator/join" }
|
534
|
-
end
|
503
|
+
context "for a moderator" do
|
504
|
+
before { room.should_receive(:fetch_is_running?) }
|
535
505
|
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
506
|
+
context "when the conference is running" do
|
507
|
+
before {
|
508
|
+
room.should_receive(:is_running?).and_return(true)
|
509
|
+
room.should_receive(:join_url).with(user.name, :moderator).
|
510
|
+
and_return("http://test.com/moderator/join")
|
511
|
+
}
|
512
|
+
subject { room.perform_join(user.name, :moderator) }
|
513
|
+
it { should == "http://test.com/moderator/join" }
|
514
|
+
end
|
515
|
+
|
516
|
+
context "when the conference is not running" do
|
517
|
+
before {
|
518
|
+
room.should_receive(:is_running?).and_return(false)
|
519
|
+
room.should_receive(:send_create)
|
520
|
+
room.should_receive(:join_url).with(user.name, :moderator).
|
521
|
+
and_return("http://test.com/moderator/join")
|
522
|
+
}
|
523
|
+
subject { room.perform_join(user.name, :moderator) }
|
524
|
+
it { should == "http://test.com/moderator/join" }
|
525
|
+
end
|
549
526
|
|
527
|
+
context "when the arg 'request' is informed" do
|
528
|
+
let(:request) { stub(ActionController::Request) }
|
529
|
+
before {
|
530
|
+
request.stub!(:protocol).and_return("HTTP://")
|
531
|
+
request.stub!(:host_with_port).and_return("test.com:80")
|
532
|
+
room.should_receive(:add_domain_to_logout_url).with("HTTP://", "test.com:80")
|
533
|
+
room.should_receive(:is_running?).and_return(true)
|
534
|
+
room.should_receive(:join_url).with(user.name, :moderator).
|
535
|
+
and_return("http://test.com/moderator/join")
|
536
|
+
}
|
537
|
+
subject { room.perform_join(user.name, :moderator, request) }
|
538
|
+
it { should == "http://test.com/moderator/join" }
|
550
539
|
end
|
551
540
|
|
552
541
|
end
|
553
542
|
|
554
543
|
end
|
555
544
|
|
545
|
+
describe "#full_logout_url" do
|
546
|
+
subject { BigbluebuttonRoom.new }
|
547
|
+
it { should respond_to(:full_logout_url) }
|
548
|
+
it { should respond_to(:"full_logout_url=") }
|
549
|
+
end
|
550
|
+
|
556
551
|
end
|