bigbluebutton-api-ruby 0.0.11 → 0.1.0.rc1

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.
Files changed (51) hide show
  1. data/.gitignore +5 -2
  2. data/.rvmrc +1 -1
  3. data/.travis.yml +6 -0
  4. data/CHANGELOG.rdoc +19 -8
  5. data/Gemfile +9 -1
  6. data/Gemfile.lock +62 -9
  7. data/LICENSE +5 -7
  8. data/LICENSE_003 +20 -0
  9. data/README.rdoc +42 -19
  10. data/Rakefile +31 -19
  11. data/bigbluebutton-api-ruby.gemspec +5 -5
  12. data/examples/get_version_example.rb +18 -0
  13. data/examples/join_example.rb +59 -0
  14. data/examples/overall_0.7_example.rb +92 -0
  15. data/examples/prepare.rb +38 -0
  16. data/extras/bigbluebutton_bot.rb +64 -0
  17. data/extras/download_bot_from.txt +1 -0
  18. data/extras/test-presentation.pdf +0 -0
  19. data/features/check_status.feature +45 -0
  20. data/features/config.yml.example +21 -0
  21. data/features/create_meetings.feature +29 -0
  22. data/features/end_meetings.feature +27 -0
  23. data/features/join_meetings.feature +29 -0
  24. data/features/pre_upload_slides.feature +14 -0
  25. data/features/recordings.feature +34 -0
  26. data/features/step_definitions/check_status_steps.rb +119 -0
  27. data/features/step_definitions/common_steps.rb +122 -0
  28. data/features/step_definitions/create_meetings_steps.rb +54 -0
  29. data/features/step_definitions/end_meetings_steps.rb +49 -0
  30. data/features/step_definitions/join_meetings_steps.rb +39 -0
  31. data/features/step_definitions/pre_upload_slides_steps.rb +13 -0
  32. data/features/step_definitions/recordings_steps.rb +38 -0
  33. data/features/support/api_tests/configs.rb +51 -0
  34. data/features/support/env.rb +7 -0
  35. data/features/support/hooks.rb +11 -0
  36. data/lib/bigbluebutton_api.rb +301 -97
  37. data/lib/bigbluebutton_formatter.rb +105 -19
  38. data/lib/bigbluebutton_modules.rb +92 -0
  39. data/lib/hash_to_xml.rb +22 -51
  40. data/spec/bigbluebutton_api_0.8_spec.rb +273 -0
  41. data/spec/bigbluebutton_api_spec.rb +211 -117
  42. data/spec/bigbluebutton_formatter_spec.rb +178 -29
  43. data/spec/bigbluebutton_modules_spec.rb +95 -0
  44. data/spec/data/hash_to_xml_complex.xml +45 -0
  45. data/spec/hash_to_xml_spec.rb +143 -0
  46. data/spec/spec_helper.rb +4 -2
  47. data/spec/support/forgery/forgeries/random_name.rb +7 -0
  48. data/spec/support/forgery/forgeries/url.rb +5 -0
  49. metadata +47 -12
  50. data/test/config.yml.example +0 -9
  51. data/test/test.rb +0 -154
@@ -0,0 +1,92 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')
3
+
4
+ require 'bigbluebutton_api'
5
+ require 'prepare'
6
+ require 'securerandom'
7
+
8
+ begin
9
+ prepare
10
+
11
+ puts
12
+ puts "---------------------------------------------------"
13
+ if @api.test_connection
14
+ puts "Connection successful! continuing..."
15
+ else
16
+ puts "Connection failed! The server might be unreachable. Exiting..."
17
+ Kernel.exit!
18
+ end
19
+
20
+ puts
21
+ puts "---------------------------------------------------"
22
+ version = @api.get_api_version
23
+ puts "The API version of your server is #{version}"
24
+
25
+ puts
26
+ puts "---------------------------------------------------"
27
+ response = @api.get_meetings
28
+ puts "Existent meetings in your server:"
29
+ response[:meetings].each do |m|
30
+ puts " " + m[:meetingID] + ": " + m.inspect
31
+ end
32
+
33
+ puts
34
+ puts "---------------------------------------------------"
35
+ meeting_id = SecureRandom.hex(4)
36
+ meeting_name = meeting_id
37
+ moderator_name = "House"
38
+ attendee_name = "Cameron"
39
+ options = { :moderatorPW => "54321",
40
+ :attendeePW => "12345",
41
+ :welcome => 'Welcome to my meeting',
42
+ :dialNumber => '1-800-000-0000x00000#',
43
+ :voiceBridge => 70000 + rand(9999),
44
+ :webVoice => SecureRandom.hex(4),
45
+ :logoutURL => 'https://github.com/mconf/bigbluebutton-api-ruby',
46
+ :maxParticipants => 25 }
47
+
48
+ @api.create_meeting(meeting_name, meeting_id, options)
49
+ puts "The meeting has been created. Please open a web browser and enter the meeting using either of the URLs below."
50
+
51
+ puts
52
+ puts "---------------------------------------------------"
53
+ url = @api.join_meeting_url(meeting_id, moderator_name, options[:moderatorPW])
54
+ puts "1) Moderator URL = #{url}"
55
+ puts ""
56
+ url = @api.join_meeting_url(meeting_id, attendee_name, options[:attendeePW])
57
+ puts "2) Attendee URL = #{url}"
58
+
59
+ puts
60
+ puts "---------------------------------------------------"
61
+ puts "Waiting 30 seconds for you to enter via browser"
62
+ sleep(30)
63
+
64
+ unless @api.is_meeting_running?(meeting_id)
65
+ puts "You have NOT entered the meeting"
66
+ Kernel.exit!
67
+ end
68
+ puts "You have successfully entered the meeting"
69
+
70
+ puts
71
+ puts "---------------------------------------------------"
72
+ response = @api.get_meeting_info(meeting_id, options[:moderatorPW])
73
+ puts "Meeting info:"
74
+ puts response.inspect
75
+
76
+ puts
77
+ puts "---------------------------------------------------"
78
+ puts "Attendees:"
79
+ response[:attendees].each do |m|
80
+ puts " " + m[:fullName] + " (" + m[:userID] + "): " + m.inspect
81
+ end
82
+
83
+
84
+ puts
85
+ puts "---------------------------------------------------"
86
+ @api.end_meeting(meeting_id, options[:moderatorPW])
87
+ puts "The meeting has been ended"
88
+
89
+ rescue Exception => ex
90
+ puts "Failed with error #{ex.message}"
91
+ puts ex.backtrace
92
+ end
@@ -0,0 +1,38 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib')
3
+
4
+ require 'bigbluebutton_api'
5
+ require 'yaml'
6
+
7
+ def prepare
8
+ config_file = File.join(File.dirname(__FILE__), '..', 'features', 'config.yml')
9
+ unless File.exist? config_file
10
+ puts config_file + " does not exists. Copy the example and configure your server."
11
+ puts "cp features/config.yml.example features/config.yml"
12
+ puts
13
+ Kernel.exit!
14
+ end
15
+ @config = YAML.load_file(config_file)
16
+
17
+ puts "** Config:"
18
+ @config.each do |k,v|
19
+ puts k + ": " + v.to_s
20
+ end
21
+ puts
22
+
23
+ if ARGV.size > 0
24
+ unless @config['servers'].has_key?(ARGV[0])
25
+ throw Exception.new("Server #{ARGV[0]} does not exists in your configuration file.")
26
+ end
27
+ server = @config['servers'][ARGV[0]]
28
+ else
29
+ key = @config['servers'].keys.first
30
+ server = @config['servers'][key]
31
+ end
32
+
33
+ puts "** Using the server:"
34
+ puts server.inspect
35
+ puts
36
+
37
+ @api = BigBlueButton::BigBlueButtonApi.new(server['url'], server['salt'], server['version'].to_s, true)
38
+ end
@@ -0,0 +1,64 @@
1
+ class BigBlueButtonBot
2
+ BOT_FILENAME = "bbb-bot.jar"
3
+ @@pids = []
4
+
5
+ def initialize(api, meeting, salt="", count=1, timeout=20)
6
+ bot_file = File.join(File.dirname(__FILE__), BOT_FILENAME)
7
+ unless File.exist?(bot_file)
8
+ throw Exception.new(bot_file + " does not exists. See download_bot_from.txt and download the bot file.")
9
+ end
10
+
11
+ server = parse_server_url(api.url)
12
+
13
+ # note: fork + exec with these parameters was the only solution found to run the command in background
14
+ # and be able to wait for it (kill it) later on (see BigBlueButtonBot.finalize)
15
+ pid = Process.fork do
16
+ exec("java", "-jar", "#{bot_file}", "-s", "#{server}", "-p", "#{salt}", "-m", "#{meeting}", "-n", "#{count}")
17
+
18
+ # other options that didn't work:
19
+ # IO::popen("java -jar #{bot_file} -s \"#{server}\" -m \"#{meeting}\" -n #{count} >/dev/null")
20
+ # exec(["java", "-jar #{bot_file} -s \"#{server}\" -m \"#{meeting}\" -n #{count} >/dev/null"])
21
+ # exec("java -jar #{bot_file} -s \"#{server}\" -m \"#{meeting}\" -n #{count} >/dev/null")
22
+ # Process.exit!
23
+ end
24
+ @@pids << pid
25
+
26
+ wait_bot_startup(api, meeting, count, timeout)
27
+ end
28
+
29
+ def self.finalize
30
+ @@pids.each do |pid|
31
+ p = Process.kill("TERM", pid)
32
+ Process.detach(pid)
33
+ end
34
+ @@pids.clear
35
+ end
36
+
37
+ def parse_server_url(full_url)
38
+ uri = URI.parse(full_url)
39
+ uri_s = uri.scheme + "://" + uri.host
40
+ uri_s = uri_s + ":" + uri.port.to_s if uri.port != uri.default_port
41
+ uri_s
42
+ end
43
+
44
+ # wait until the meeting is running with a certain number of participants
45
+ def wait_bot_startup(api, meeting, participants, timeout=20)
46
+ Timeout::timeout(timeout) do
47
+ stop_wait = false
48
+ while !stop_wait
49
+ sleep 1
50
+
51
+ # find the meeting and hope it is running
52
+ response = api.get_meetings
53
+ selected = response[:meetings].reject!{ |m| m[:meetingID] != meeting }
54
+ if selected and selected.size > 0 and selected[0][:running]
55
+
56
+ # check how many participants are in the meeting
57
+ pass = selected[0][:moderatorPW]
58
+ response = api.get_meeting_info(meeting, pass)
59
+ stop_wait = response[:participantCount] >= participants
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1 @@
1
+ https://github.com/daronco/mconf-mobile/blob/879f38e1be127f46b379c57a1933d3867b6bf14b/bbb-bot/bbb-bot.jar?raw=true
Binary file
@@ -0,0 +1,45 @@
1
+ Feature: Check meeting configurations and status
2
+ To be able to monitor BigBlueButton
3
+ One needs to check the current meetings
4
+ and the status and configurations of a meeting
5
+
6
+ @version-all @need-bot
7
+ Scenario: Check that a meeting is running
8
+ Given that a meeting was created
9
+ And the meeting is running
10
+ Then the method isMeetingRunning informs that the meeting is running
11
+
12
+ @version-all
13
+ Scenario: Check that a meeting is NOT running
14
+ Given that a meeting was created
15
+ Then the method isMeetingRunning informs that the meeting is NOT running
16
+
17
+ @version-all
18
+ Scenario: Check the information of a meeting
19
+ Given that a meeting was created
20
+ When calling the method get_meeting_info
21
+ Then the response is successful with no messages
22
+ And it shows all the information of the meeting that was created
23
+
24
+ # to make sure that getMeetingInfo is returning the proper info used in create
25
+ @version-all
26
+ Scenario: Check the information of a meeting created with optional parameters
27
+ Given that a meeting was created with ALL the optional arguments
28
+ When calling the method get_meeting_info
29
+ Then the response is successful with no messages
30
+ And it shows all the information of the meeting that was created
31
+
32
+ @version-all @need-bot
33
+ Scenario: Check the information of a meeting that is running and has attendees
34
+ Given that a meeting was created
35
+ And the meeting is running with 2 attendees
36
+ When calling the method get_meeting_info
37
+ Then the response is successful with no messages
38
+ And it shows the 2 attendees in the list
39
+
40
+ @version-all
41
+ Scenario: List the meetings in a server
42
+ Given that a meeting was created
43
+ When calling the method get_meetings
44
+ Then the response is successful with no messages
45
+ And the created meeting should be listed in the response with proper information
@@ -0,0 +1,21 @@
1
+ debug: false
2
+
3
+ # maximum wait for a response to any API request (secs)
4
+ timeout_req: 60
5
+
6
+ # maximum wait for a meeting to be ended after end_meeting is called (secs)
7
+ timeout_ending: 30
8
+
9
+ # maximum wait until the bot starts (secs)
10
+ timeout_bot_start: 60
11
+
12
+ servers:
13
+ bbb-dev-07:
14
+ salt: 'your-salt'
15
+ url: 'http://your-server/bigbluebutton/api'
16
+ version: '0.7'
17
+ test-install-08:
18
+ salt: '8cd8ef52e8e101574e400365b55e11a6'
19
+ url: 'http://test-install.blindsidenetworks.com/bigbluebutton/api'
20
+ version: '0.8'
21
+ mobile_salt: '03b07' # needed for >= 0.8
@@ -0,0 +1,29 @@
1
+ Feature: Create rooms
2
+ To be able to use BigBlueButton
3
+ One needs to create a webconference room first
4
+
5
+ @version-all
6
+ Scenario: Create a new room
7
+ When the create method is called with ALL the optional arguments
8
+ Then the response is successful and well formatted
9
+ And the meeting exists in the server
10
+
11
+ @version-all
12
+ Scenario: Create a new room with default parameters
13
+ When the create method is called with NO optional arguments
14
+ Then the response is successful and well formatted
15
+ And the meeting exists in the server
16
+
17
+ @version-all
18
+ Scenario: Try to create a room with a duplicated meeting id
19
+ When the create method is called with a duplicated meeting id
20
+ Then the response is an error with the key "idNotUnique"
21
+
22
+ @version-all @need-bot
23
+ Scenario: Try to recreate a previously ended meeting
24
+ Given the create method is called
25
+ # note: meeting needs to be running to be ended in 0.7
26
+ And the meeting is running
27
+ And the meeting is forcibly ended
28
+ When the create method is called again with the same meeting id
29
+ Then the response is an error with the key "idNotUnique"
@@ -0,0 +1,27 @@
1
+ Feature: End rooms
2
+ To stop a meeting using the API
3
+ One needs to be able to call 'end' to this meeting
4
+
5
+ @version-all @need-bot
6
+ Scenario: End a meeting
7
+ Given that a meeting was created
8
+ And the meeting is running
9
+ When the method to end the meeting is called
10
+ Then the response is successful and well formatted
11
+ And the meeting should be ended
12
+ And the information returned by get_meeting_info is correct
13
+
14
+ # in 0.7 ending a meeting that is not running generates an error
15
+ @version-07
16
+ Scenario: Try to end a meeting that is not running in 0.7
17
+ Given that a meeting was created
18
+ When the method to end the meeting is called
19
+ Then the response is an error with the key "notFound"
20
+
21
+ # in 0.8 ending a meeting that is not running is ok
22
+ @version-08
23
+ Scenario: Try to end a meeting that is not running in 0.8
24
+ Given that a meeting was created
25
+ When the method to end the meeting is called
26
+ Then the response is successful
27
+ And the response has the messageKey "sentEndMeetingRequest"
@@ -0,0 +1,29 @@
1
+ Feature: Join meeting
2
+ To participate in a meeting
3
+ The user needs to be able to join a created meeting
4
+
5
+ @version-all
6
+ Scenario: Join a meeting as moderator
7
+ Given that a meeting was created
8
+ When the user tries to access the link to join the meeting as moderator
9
+ Then he is redirected to the BigBlueButton client
10
+ # can't really check if the user is in the session because in bbb he will
11
+ # only be listed as an attendee after stabilishing a rtmp connection
12
+
13
+ @version-all
14
+ Scenario: Join a meeting as attendee
15
+ Given that a meeting was created
16
+ When the user tries to access the link to join the meeting as attendee
17
+ Then he is redirected to the BigBlueButton client
18
+
19
+ @version-all
20
+ Scenario: Join a non created meeting
21
+ Given the default BigBlueButton server
22
+ When the user tries to access the link to join a meeting that was not created
23
+ Then the response is an xml with the error "invalidMeetingIdentifier"
24
+
25
+ @version-all
26
+ Scenario: Try to join with the wrong password
27
+ Given that a meeting was created
28
+ When the user tries to access the link to join the meeting using a wrong password
29
+ Then the response is an xml with the error "invalidPassword"
@@ -0,0 +1,14 @@
1
+ @version-08
2
+ Feature: Pre-upload slides
3
+ To have presentations ready in the meeting when the users join
4
+ One needs to pre-upload these presentations when the meeting is created
5
+
6
+ Scenario: Pre-upload presentations
7
+ Given the default BigBlueButton server
8
+ When the user creates a meeting pre-uploading the following presentations:
9
+ | type | presentation |
10
+ | url | http://www.samplepdf.com/sample.pdf |
11
+ | file | extras/test-presentation.pdf |
12
+ Then the response is successful and well formatted
13
+ # OPTIMIZE: There's no way to check if the presentation is really in the meeting
14
+ # And these presentations should be available in the meeting as it begins
@@ -0,0 +1,34 @@
1
+ @version-08
2
+ Feature: Record a meeting and manage recordings
3
+ To record a meeting or manage the recorded meeting
4
+ One needs be able to list the recordings, publish and unpublish them
5
+
6
+ # We don't check if meetings will really be recorded
7
+ # To record a meeting we need at least audio in the session
8
+ # And also it would probably that a long time to record and process test meetings
9
+ # For now we'll have only basic tests in this feature
10
+
11
+ Scenario: Set a meeting to be recorded
12
+ Given the default BigBlueButton server
13
+ When the user creates a meeting with the record flag
14
+ Then the response is successful and well formatted
15
+ And the meeting is set to be recorded
16
+
17
+ Scenario: By default a meeting will not be recorded
18
+ Given the default BigBlueButton server
19
+ When the user creates a meeting without the record flag
20
+ Then the response is successful and well formatted
21
+ And the meeting is NOT set to be recorded
22
+
23
+ Scenario: List the available recordings in a server with no recordings
24
+ Given the default BigBlueButton server
25
+ When the user calls the get_recordings method
26
+ Then the response is successful and well formatted
27
+ And the response has the messageKey "noRecordings"
28
+
29
+ # Possible scenarios to test in the future
30
+ # Scenario: Record a meeting # not only set to be recorded
31
+ # Scenario: List the available recordings
32
+ # Scenario: Publish a recording
33
+ # Scenario: Unpublish a recording
34
+ # Scenario: Remove a recording
@@ -0,0 +1,119 @@
1
+ When /^the method isMeetingRunning informs that the meeting is running$/ do
2
+ @req.response = @api.is_meeting_running?(@req.id)
3
+ @req.response.should be_true
4
+ end
5
+
6
+ When /^the method isMeetingRunning informs that the meeting is not running$/i do
7
+ @req.response = @api.is_meeting_running?(@req.id)
8
+ @req.response.should be_false
9
+ end
10
+
11
+ When /^calling the method get_meetings$/ do
12
+ @req.response = @api.get_meetings
13
+ end
14
+
15
+ When /^calling the method get_meeting_info$/ do
16
+ @req.response = @api.get_meeting_info(@req.id, @req.mod_pass)
17
+ end
18
+
19
+ When /^the created meeting should be listed in the response with proper information$/ do
20
+ @req.response[:meetings].size.should >= 1
21
+
22
+ # the created meeting is in the list and has only 1 occurance
23
+ found = @req.response[:meetings].reject{ |m| m[:meetingID] != @req.id }
24
+ found.should_not be_nil
25
+ found.size.should == 1
26
+
27
+ # proper information in the meeting hash
28
+ found = found[0]
29
+ found[:attendeePW].should be_a(String)
30
+ found[:attendeePW].should_not be_empty
31
+ found[:moderatorPW].should == @req.mod_pass
32
+ found[:hasBeenForciblyEnded].should be_false
33
+ found[:running].should be_false
34
+ if @api.version >= "0.8"
35
+ found[:meetingName].should == @req.id
36
+ found[:createTime].should be_a(Numeric)
37
+ end
38
+ end
39
+
40
+ When /^it shows all the information of the meeting that was created$/ do
41
+ @req.response = @api.get_meeting_info(@req.id, @req.mod_pass)
42
+ @req.response[:meetingID].should == @req.id
43
+ @req.response[:running].should be_false
44
+ @req.response[:hasBeenForciblyEnded].should be_false
45
+ @req.response[:startTime].should be_nil
46
+ @req.response[:endTime].should be_nil
47
+ @req.response[:participantCount].should == 0
48
+ @req.response[:moderatorCount].should == 0
49
+ @req.response[:attendees].should == []
50
+ @req.response[:messageKey].should == ""
51
+ @req.response[:message].should == ""
52
+ if @req.opts.has_key?(:attendeePW)
53
+ @req.response[:attendeePW].should == @req.opts[:attendeePW]
54
+ else # auto generated password
55
+ @req.response[:attendeePW].should be_a(String)
56
+ @req.response[:attendeePW].should_not be_empty
57
+ @req.opts[:attendeePW] = @req.response[:attendeePW]
58
+ end
59
+ if @req.opts.has_key?(:moderatorPW)
60
+ @req.response[:moderatorPW].should == @req.opts[:moderatorPW]
61
+ else # auto generated password
62
+ @req.response[:moderatorPW].should be_a(String)
63
+ @req.response[:moderatorPW].should_not be_empty
64
+ @req.opts[:moderatorPW] = @req.response[:moderatorPW]
65
+ end
66
+
67
+ if @api.version >= "0.8"
68
+ @req.response[:meetingName].should == @req.id
69
+ @req.response[:createTime].should be_a(Numeric)
70
+
71
+ @req.opts.has_key?(:record) ?
72
+ (@req.response[:recording].should == @req.opts[:record]) :
73
+ (@req.response[:recording].should be_false)
74
+ @req.opts.has_key?(:maxParticipants) ?
75
+ (@req.response[:maxUsers].should == @req.opts[:maxParticipants]) :
76
+ (@req.response[:maxUsers].should == 20)
77
+ @req.opts.has_key?(:voiceBridge) ?
78
+ (@req.response[:voiceBridge].should == @req.opts[:voiceBridge]) :
79
+ (@req.response[:voiceBridge].should be_a(Numeric))
80
+
81
+ if @req.opts.has_key?(:meta_one)
82
+ @req.response[:metadata].size.should == 2
83
+ @req.response[:metadata].should be_a(Hash)
84
+ @req.response[:metadata].should include(:one => "one")
85
+ @req.response[:metadata].should include(:two => "TWO")
86
+ else
87
+ @req.response[:metadata].should == {}
88
+ end
89
+
90
+ # note: the duration passed in the api call is not returned (so it won't be checked)
91
+ end
92
+ end
93
+
94
+ Then /^it shows the (\d+) attendees in the list$/ do |count|
95
+ # check only what is different in a meeting that is RUNNING
96
+ # the rest is checked in other scenarios
97
+
98
+ @req.response = @api.get_meeting_info(@req.id, @req.mod_pass)
99
+ participants = count.to_i
100
+
101
+ @req.response[:running].should be_true
102
+ @req.response[:moderatorCount].should > 0
103
+ @req.response[:hasBeenForciblyEnded].should be_false
104
+ @req.response[:participantCount].should == participants
105
+ @req.response[:attendees].size.should == 2
106
+
107
+ # check the start time that should be within 2 hours from now
108
+ @req.response[:startTime].should be_a(DateTime)
109
+ @req.response[:startTime].should < DateTime.now
110
+ @req.response[:startTime].should >= DateTime.now - (2/24.0)
111
+ @req.response[:endTime].should be_nil
112
+
113
+ # in the bot being used, bots are always moderators with these names
114
+ @req.response[:attendees].sort! { |h1,h2| h1[:fullName] <=> h2[:fullName] }
115
+ @req.response[:attendees][0][:fullName].should == "BOT0"
116
+ @req.response[:attendees][0][:role].should == :moderator
117
+ @req.response[:attendees][1][:fullName].should == "BOT1"
118
+ @req.response[:attendees][1][:role].should == :moderator
119
+ end