bigbluebutton_rails 2.2.0 → 2.3.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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -3
  3. data/CHANGELOG.md +34 -0
  4. data/Dockerfile +23 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +14 -5
  7. data/README.md +42 -59
  8. data/Rakefile +6 -3
  9. data/app/controllers/bigbluebutton/api/rooms_controller.rb +117 -0
  10. data/app/controllers/bigbluebutton/playback_types_controller.rb +0 -3
  11. data/app/controllers/bigbluebutton/recordings_controller.rb +37 -23
  12. data/app/controllers/bigbluebutton/rooms_controller.rb +6 -31
  13. data/app/controllers/bigbluebutton/servers_controller.rb +0 -14
  14. data/app/helpers/bigbluebutton_rails_helper.rb +4 -0
  15. data/app/models/bigbluebutton_attendee.rb +21 -0
  16. data/app/models/bigbluebutton_meeting.rb +5 -0
  17. data/app/models/bigbluebutton_playback_format.rb +3 -2
  18. data/app/models/bigbluebutton_recording.rb +164 -68
  19. data/app/models/bigbluebutton_room.rb +148 -59
  20. data/app/models/bigbluebutton_server.rb +1 -1
  21. data/app/views/bigbluebutton/api/error.rabl +10 -0
  22. data/app/views/bigbluebutton/api/rooms/index.rabl +45 -0
  23. data/app/views/bigbluebutton/api/rooms/join.rabl +6 -0
  24. data/app/views/bigbluebutton/api/rooms/running.rabl +10 -0
  25. data/app/views/bigbluebutton/recordings/play.html.erb +1 -0
  26. data/app/views/bigbluebutton/servers/_activity_list.html.erb +5 -5
  27. data/app/workers/{bigbluebutton_finish_meetings.rb → bigbluebutton_finish_meetings_worker.rb} +2 -2
  28. data/app/workers/{bigbluebutton_meeting_updater.rb → bigbluebutton_meeting_updater_worker.rb} +3 -3
  29. data/app/workers/bigbluebutton_recordings_for_room_worker.rb +27 -0
  30. data/app/workers/{bigbluebutton_update_recordings.rb → bigbluebutton_update_recordings_worker.rb} +2 -2
  31. data/app/workers/{bigbluebutton_update_server_configs.rb → bigbluebutton_update_server_configs_worker.rb} +3 -3
  32. data/bigbluebutton_rails.gemspec +1 -0
  33. data/config/locales/en.yml +21 -0
  34. data/config/locales/pt-br.yml +1 -0
  35. data/config/resque/resque.rake +7 -1
  36. data/config/resque/workers_schedule.yml +3 -3
  37. data/docker-compose.yml +68 -0
  38. data/dumb-init_1.2.0 +0 -0
  39. data/lib/bigbluebutton_rails.rb +1 -2
  40. data/lib/bigbluebutton_rails/api_controller_methods.rb +138 -0
  41. data/lib/bigbluebutton_rails/configuration.rb +33 -4
  42. data/lib/bigbluebutton_rails/controller_methods.rb +0 -31
  43. data/lib/bigbluebutton_rails/exceptions.rb +17 -0
  44. data/lib/bigbluebutton_rails/rails/routes.rb +14 -0
  45. data/lib/bigbluebutton_rails/version.rb +1 -1
  46. data/lib/generators/bigbluebutton_rails/templates/migration.rb +17 -3
  47. data/lib/generators/bigbluebutton_rails/templates/migration_2_2_0.rb +1 -1
  48. data/lib/generators/bigbluebutton_rails/templates/migration_2_2_0_b.rb +106 -0
  49. data/lib/generators/bigbluebutton_rails/templates/migration_2_3_0.rb +22 -0
  50. data/lib/tasks/bigbluebutton_rails/meetings.rake +1 -1
  51. data/spec/controllers/bigbluebutton/api/rooms_controller_spec.rb +498 -0
  52. data/spec/controllers/bigbluebutton/recordings_controller_spec.rb +84 -0
  53. data/spec/controllers/bigbluebutton/rooms_controller_exception_handling_spec.rb +2 -2
  54. data/spec/controllers/bigbluebutton/rooms_controller_spec.rb +24 -22
  55. data/spec/controllers/bigbluebutton/servers_controller_spec.rb +9 -0
  56. data/spec/factories/bigbluebutton_attendee.rb +7 -0
  57. data/spec/factories/bigbluebutton_meeting.rb +0 -1
  58. data/spec/factories/bigbluebutton_playback_type.rb +1 -0
  59. data/spec/factories/bigbluebutton_recording.rb +2 -4
  60. data/spec/helpers/bigbluebutton_rails_helper_spec.rb +10 -2
  61. data/spec/lib/bigbluebutton_rails/background_tasks_spec.rb +1 -0
  62. data/spec/lib/tasks/meetings_rake_spec.rb +1 -1
  63. data/spec/models/bigbluebutton_attendee_spec.rb +44 -0
  64. data/spec/models/bigbluebutton_meeting_db_spec.rb +0 -1
  65. data/spec/models/bigbluebutton_meeting_spec.rb +1 -0
  66. data/spec/models/bigbluebutton_playback_format_spec.rb +2 -0
  67. data/spec/models/bigbluebutton_playback_type_db_spec.rb +1 -0
  68. data/spec/models/bigbluebutton_recording_db_spec.rb +2 -2
  69. data/spec/models/bigbluebutton_recording_spec.rb +404 -72
  70. data/spec/models/bigbluebutton_room_spec.rb +435 -148
  71. data/spec/rails_app/config/database.yml.example +10 -11
  72. data/spec/rails_app/config/initializers/resque.rb +33 -0
  73. data/spec/rails_app/config/routes.rb +2 -0
  74. data/spec/rails_app/lib/tasks/db/populate.rake +73 -55
  75. data/spec/support/mocked_server.rb +1 -1
  76. data/spec/workers/{bigbluebutton_finish_meetings_spec.rb → bigbluebutton_finish_meetings_worker_spec.rb} +3 -3
  77. data/spec/workers/{bigbluebutton_meeting_updater_spec.rb → bigbluebutton_meeting_updater_worker_spec.rb} +8 -7
  78. data/spec/workers/bigbluebutton_recordings_for_room_worker_spec.rb +49 -0
  79. data/spec/workers/{bigbluebutton_update_recordings_spec.rb → bigbluebutton_update_recordings_worker_spec.rb} +3 -3
  80. data/spec/workers/{bigbluebutton_update_server_configs_spec.rb → bigbluebutton_update_server_configs_worker_spec.rb} +4 -4
  81. metadata +42 -14
  82. data/lib/classes/bigbluebutton_attendee.rb +0 -21
  83. data/spec/classes/bigbluebutton_attendee_spec.rb +0 -76
  84. data/spec/controllers/bigbluebutton/recordings_controller_json_responses_spec.rb +0 -111
  85. data/spec/controllers/bigbluebutton/rooms_controller_json_responses_spec.rb +0 -203
  86. data/spec/controllers/bigbluebutton/servers_controller_json_responses_spec.rb +0 -162
@@ -11,12 +11,17 @@ module BigbluebuttonRails
11
11
  attr_accessor :user_attr_name
12
12
  attr_accessor :user_attr_id
13
13
  attr_accessor :use_local_voice_bridges
14
+ attr_accessor :api_secret
15
+ attr_accessor :playback_url_authentication
16
+ attr_accessor :playback_iframe
17
+ attr_accessor :downloadable_playback_types
14
18
 
15
19
  # methods
16
20
  attr_accessor :select_server
17
21
  attr_accessor :match_room_recording
18
22
  attr_accessor :get_invitation_url
19
- attr_accessor :get_dynamic_metadata
23
+ attr_accessor :get_create_options
24
+ attr_accessor :get_join_options
20
25
 
21
26
  def initialize
22
27
  @controllers = {
@@ -40,6 +45,17 @@ module BigbluebuttonRails
40
45
  @use_local_voice_bridges = false
41
46
  @guest_support = false
42
47
 
48
+ # Flag to use authentication on playback url
49
+ @playback_url_authentication = false
50
+
51
+ # If true, show all non downloadable playbacks inside an iframe instead of
52
+ # redirecting to their URL
53
+ @playback_iframe = false
54
+
55
+ # Name of the playback formats that can be downloaded. They are treated differently
56
+ # from those that play in a web page, for example.
57
+ @downloadable_playback_types = ['presentation_video', 'presentation_export']
58
+
43
59
  # How to find the room of a recording using the `data` returned by
44
60
  # a `getRecordings`.
45
61
  @match_room_recording = Proc.new do |data, *args|
@@ -50,9 +66,13 @@ module BigbluebuttonRails
50
66
  # returns nil (disable the feature).
51
67
  @get_invitation_url = Proc.new{ |room| nil }
52
68
 
53
- # Default method to get the dynamic metadata to use when creating a
54
- # conference in a room.
55
- @get_dynamic_metadata = Proc.new{ |room| nil }
69
+ # Define this method to return extra parameters to be passed to a `create` call.
70
+ # `user` is the user creating the meeting, if any.
71
+ @get_create_options = Proc.new{ |room, user| nil }
72
+
73
+ # Define this method to return extra parameters to be passed to a `join` call.
74
+ # `user` is the user joining the meeting, if any.
75
+ @get_join_options = Proc.new{ |room, user| nil }
56
76
 
57
77
  # Selects a server to be used by `room` whenever it needs to make API calls.
58
78
  # By default, if no servers are available an exception is raised.
@@ -68,6 +88,15 @@ module BigbluebuttonRails
68
88
  @select_server = Proc.new do |room, api_method=nil|
69
89
  room.select_server(api_method)
70
90
  end
91
+
92
+ # Secret used to authenticate API calls. API calls received are expected to set
93
+ # the HTTP header "Authorization" with a value "Bearer 123123123123", where
94
+ # "123123123123" is the secret set in `api_secret`.
95
+ # Notice that this header has the secret in clear text in the HTTP request, so
96
+ # only use it if your application is hosted with HTTPS.
97
+ # Set it to an empty string to disable authentication. Set it to nil to deny all
98
+ # requests (disable the API).
99
+ @api_secret = nil
71
100
  end
72
101
 
73
102
  def set_controllers(options)
@@ -106,37 +106,6 @@ module BigbluebuttonRails
106
106
  role == :moderator
107
107
  end
108
108
 
109
- # Method called right before a meeting is created to get options that should receive
110
- # priority over the options saved in the database when sending the <tt>create</tt>
111
- # API call.
112
- # The parameter 'room' is the BigbluebuttonRoom where the meeting is about
113
- # to be created.
114
- #
115
- # By default, all options sent in a 'create' API call are taken from the
116
- # options saved in the database for the target room. This includes, for example,
117
- # the meeting's name, dial number, welcome message, if it should be recorded or not.
118
- # This method can return a hash of options to force some of these options to be
119
- # different from what is stored in the database. For example, if a room should *not* be
120
- # recorded in case unprivileged users create it, this method can return <tt>{ record: false }</tt>
121
- # to force the <tt>record</tt> flag to be false.
122
- # The keys used in this hash should correspond to the attributes sent in the <tt>CREATE</tt>
123
- # API call, not the the attributes stored in the database! See the method
124
- # <tt>create_meeting</tt> in the gem <tt>bigbluebutton-api-ruby</tt> for examples.
125
- # (https://github.com/mconf/bigbluebutton-api-ruby/blob/master/lib/bigbluebutton_api.rb#L156)
126
- #
127
- # You may want to override this in your ApplicationController to implement your own
128
- # own logic, if needed. For example:
129
- #
130
- # def bigbluebutton_create_options(room)
131
- # can_record = room.record && bigbluebutton_user.can_record?
132
- # { record: can_record }
133
- # end
134
- # end
135
- #
136
- def bigbluebutton_create_options(room)
137
- {}
138
- end
139
-
140
109
  end
141
110
  end
142
111
 
@@ -7,4 +7,21 @@ module BigbluebuttonRails
7
7
  # room that does not have a server associated
8
8
  class ServerRequired < StandardError; end
9
9
 
10
+ # To help create responses for API errors
11
+ class APIError < StandardError
12
+ def initialize(msg, code=500, title=nil)
13
+ @title = title || msg
14
+ @code = code
15
+ super(msg)
16
+ end
17
+
18
+ def code
19
+ @code
20
+ end
21
+
22
+ def title
23
+ @title
24
+ end
25
+ end
26
+
10
27
  end
@@ -79,6 +79,20 @@ module ActionDispatch::Routing
79
79
  send("bigbluebutton_routes_#{params[0].to_s}", options)
80
80
  end
81
81
 
82
+ def bigbluebutton_api_routes(api_scope='/api/conference')
83
+ scope api_scope do
84
+ resources :rooms, only: [] do
85
+ collection do
86
+ get :index, to: 'bigbluebutton/api/rooms#index'
87
+ end
88
+ member do
89
+ get :running, to: 'bigbluebutton/api/rooms#running'
90
+ post :join, to: 'bigbluebutton/api/rooms#join'
91
+ end
92
+ end
93
+ end
94
+ end
95
+
82
96
  protected
83
97
 
84
98
  def bigbluebutton_routes_default(*params) #:nodoc:
@@ -1,3 +1,3 @@
1
1
  module BigbluebuttonRails
2
- VERSION = "2.2.0".freeze
2
+ VERSION = "2.3.0".freeze
3
3
  end
@@ -56,11 +56,12 @@ class CreateBigbluebuttonRails < ActiveRecord::Migration
56
56
  t.string :meetingid
57
57
  t.string :name
58
58
  t.boolean :published, :default => false
59
- t.datetime :start_time
60
- t.datetime :end_time
59
+ t.decimal :start_time, precision: 14, scale: 0
60
+ t.decimal :end_time, precision: 14, scale: 0
61
61
  t.boolean :available, :default => true
62
62
  t.string :description
63
63
  t.integer :size, limit: 8, default: 0
64
+ t.text :recording_users
64
65
  t.timestamps
65
66
  end
66
67
  add_index :bigbluebutton_recordings, :room_id
@@ -86,6 +87,7 @@ class CreateBigbluebuttonRails < ActiveRecord::Migration
86
87
  t.string :identifier
87
88
  t.boolean :visible, :default => false
88
89
  t.boolean :default, :default => false
90
+ t.boolean :downloadable, :default => false
89
91
  t.timestamps
90
92
  end
91
93
 
@@ -95,13 +97,14 @@ class CreateBigbluebuttonRails < ActiveRecord::Migration
95
97
  t.integer :room_id
96
98
  t.string :meetingid
97
99
  t.string :name
98
- t.datetime :start_time
99
100
  t.decimal :create_time, precision: 14, scale: 0
101
+ t.decimal :finish_time, precision: 14, scale: 0
100
102
  t.boolean :running, :default => false
101
103
  t.boolean :recorded, :default => false
102
104
  t.integer :creator_id
103
105
  t.string :creator_name
104
106
  t.boolean :ended, :default => false
107
+ t.string :got_stats
105
108
  t.timestamps
106
109
  end
107
110
  add_index :bigbluebutton_meetings, [:meetingid, :create_time], :unique => true
@@ -111,6 +114,17 @@ class CreateBigbluebuttonRails < ActiveRecord::Migration
111
114
  t.text :available_layouts
112
115
  t.timestamps
113
116
  end
117
+
118
+ create_table :bigbluebutton_attendees do |t|
119
+ t.string :user_id
120
+ t.string :external_user_id
121
+ t.string :user_name
122
+ t.decimal :join_time, precision: 14, scale: 0
123
+ t.decimal :left_time, precision: 14, scale: 0
124
+ t.integer :bigbluebutton_meeting_id
125
+ t.timestamps
126
+ end
127
+
114
128
  end
115
129
 
116
130
  def self.down
@@ -1,8 +1,8 @@
1
1
  class BigbluebuttonRailsTo220 < ActiveRecord::Migration
2
2
  def self.up
3
3
  remove_column :bigbluebutton_meetings, :server_id
4
- remove_column :bigbluebutton_rooms, :server_id
5
4
  remove_index :bigbluebutton_rooms, :server_id
5
+ remove_column :bigbluebutton_rooms, :server_id
6
6
  end
7
7
 
8
8
  def self.down
@@ -0,0 +1,106 @@
1
+ class BigbluebuttonRailsTo220B < ActiveRecord::Migration
2
+ def up
3
+ BigbluebuttonMeeting.where(create_time: nil).find_each do |meeting|
4
+ meeting.update_attribute(create_time, meeting.start_time.to_i)
5
+ end
6
+
7
+ remove_column :bigbluebutton_meetings, :start_time
8
+
9
+ add_column :bigbluebutton_recordings, :temp_start_time, :decimal, precision: 14, scale: 0
10
+ add_column :bigbluebutton_recordings, :temp_end_time, :decimal, precision: 14, scale: 0
11
+
12
+ BigbluebuttonRecording.find_each do |rec|
13
+ rec.update_attributes(
14
+ temp_start_time: rec.start_time.to_i,
15
+ temp_end_time: rec.end_time.to_i
16
+ )
17
+ end
18
+
19
+ remove_column :bigbluebutton_recordings, :start_time
20
+ remove_column :bigbluebutton_recordings, :end_time
21
+
22
+ rename_column :bigbluebutton_recordings, :temp_start_time, :start_time
23
+ rename_column :bigbluebutton_recordings, :temp_end_time, :end_time
24
+
25
+ BigbluebuttonRecording.find_each do |rec|
26
+ rec.update_attributes(
27
+ meeting_id: BigbluebuttonRecording.find_matching_meeting(rec).try(:id)
28
+ )
29
+ end
30
+
31
+ BigbluebuttonRecording.where(meeting_id: nil).find_each do |rec|
32
+ rec.update_attributes(
33
+ meeting_id: find_matching_meeting_closer_create_time(rec)
34
+ )
35
+ end
36
+
37
+ BigbluebuttonRecording.where(meeting_id: nil).where.not(room_id: nil).find_each do |rec|
38
+ meeting = BigbluebuttonMeeting.create do |m|
39
+ creator_data = find_creator_data(rec)
40
+ m.server_id = rec.server_id
41
+ m.room_id = rec.room_id
42
+ m.meetingid = rec.meetingid
43
+ m.name = rec.name
44
+ m.running = false
45
+ m.recorded = true
46
+ m.creator_id = creator_data[0]
47
+ m.creator_name = creator_data[1]
48
+ m.server_url = nil
49
+ m.server_secret = nil
50
+ m.create_time = rec.start_time
51
+ m.ended = true
52
+ end
53
+ rec.update_attributes(meeting_id: meeting.id)
54
+ puts "Created a meeting for the recording id-#{rec.id}: Meeting id-#{meeting.id}"
55
+ end
56
+
57
+ create_table :bigbluebutton_attendees do |t|
58
+ t.string :user_id
59
+ t.string :external_user_id
60
+ t.string :user_name
61
+ t.decimal :join_time, precision: 14, scale: 0
62
+ t.decimal :left_time, precision: 14, scale: 0
63
+ t.integer :bigbluebutton_meeting_id
64
+ t.timestamps
65
+ end
66
+
67
+ add_column :bigbluebutton_meetings, :finish_time, :decimal, precision: 14, scale: 0
68
+ add_column :bigbluebutton_meetings, :got_stats, :string
69
+ end
70
+
71
+ def find_matching_meeting_closer_create_time(recording)
72
+ meeting_id = recording.meeting_id
73
+ if meeting_id.nil?
74
+ meeting = BigbluebuttonMeeting.where("meetingid = ? AND created_at > ? AND created_at < ?",
75
+ recording.meetingid, Time.at(recording.start_time)-2.minutes, Time.at(recording.start_time)+2.minutes).first
76
+
77
+ unless meeting.nil?
78
+ unless BigbluebuttonRecording.find_by(meeting_id: meeting.id).present?
79
+ meeting_id = meeting.id
80
+ end
81
+ end
82
+
83
+ unless meeting_id.nil?
84
+ puts "Meeting found for recording id-#{recording.id}: Meeting id-#{meeting.id}"
85
+ end
86
+ end
87
+
88
+ meeting_id
89
+ end
90
+
91
+ def find_creator_data(recording)
92
+ creator_data = []
93
+ if recording.room.owner.is_a?(User)
94
+ creator_id = recording.room.try(:owner).try(:id)
95
+ creator_name = recording.room.try(:owner).try(:name)
96
+ elsif recording.room.owner.is_a?(Space)
97
+ creator_id = recording.room.try(:owner).try(:admins).try(:first).try(:id)
98
+ creator_name = recording.room.try(:owner).try(:admins).try(:first).try(:name)
99
+ end
100
+ creator_data = [creator_id, creator_name]
101
+ end
102
+
103
+ def down
104
+ raise ActiveRecord::IrreversibleMigration, "Can't undo due to loss of values during migration"
105
+ end
106
+ end
@@ -0,0 +1,22 @@
1
+ class BigbluebuttonRailsTo230 < ActiveRecord::Migration
2
+ def self.up
3
+ rename_column :bigbluebutton_rooms, :param, :slug
4
+ rename_column :bigbluebutton_servers, :param, :slug
5
+ add_column :bigbluebutton_recordings, :recording_users, :text
6
+ add_column :bigbluebutton_playback_types, :downloadable, :boolean, default: false
7
+ remove_column :bigbluebutton_meetings, :got_stats
8
+
9
+ BigbluebuttonPlaybackType.find_each do |type|
10
+ downloadable = BigbluebuttonRails.configuration.downloadable_playback_types.include?(type.identifier)
11
+ type.update_attributes(downloadable: downloadable)
12
+ end
13
+ end
14
+
15
+ def self.down
16
+ remove_column :bigbluebutton_playback_types, :downloadable
17
+ remove_column :bigbluebutton_recordings, :recording_users
18
+ rename_column :bigbluebutton_servers, :slug, :param
19
+ rename_column :bigbluebutton_rooms, :slug, :param
20
+ add_column :bigbluebutton_meetings, :got_stats, :string
21
+ end
22
+ end
@@ -6,5 +6,5 @@ namespace :bigbluebutton_rails do
6
6
  task :finish => :environment do
7
7
  BigbluebuttonRails::BackgroundTasks.finish_meetings
8
8
  end
9
- end
9
+ end
10
10
  end
@@ -0,0 +1,498 @@
1
+ require 'spec_helper'
2
+ require 'bigbluebutton_api'
3
+
4
+ describe Bigbluebutton::Api::RoomsController do
5
+ render_views
6
+ let!(:room) { FactoryGirl.create(:bigbluebutton_room) }
7
+
8
+ before {
9
+ @previous = BigbluebuttonRails.configuration.api_secret
10
+ BigbluebuttonRails.configuration.api_secret = "" # all allowed
11
+ }
12
+ after {
13
+ BigbluebuttonRails.configuration.api_secret = @previous
14
+ }
15
+
16
+ shared_examples "an authenticated API call" do
17
+
18
+ context "when the server has a secret" do
19
+ before {
20
+ @previous = BigbluebuttonRails.configuration.api_secret
21
+ BigbluebuttonRails.configuration.api_secret = "123123"
22
+ }
23
+ after {
24
+ BigbluebuttonRails.configuration.api_secret = @previous
25
+ }
26
+
27
+ [nil, "WRONG", '', "bearer 123123"].each do |auth_header|
28
+ context "forbids the authorization header #{auth_header.inspect}" do
29
+ before(:each) {
30
+ request.headers['Authorization'] = auth_header
31
+ action
32
+ }
33
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('403') }
34
+ it {
35
+ title = JSON.parse(response.body)['errors'][0]['title']
36
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.forbidden.title'))
37
+ }
38
+ it {
39
+ detail = JSON.parse(response.body)['errors'][0]['detail']
40
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.forbidden.msg'))
41
+ }
42
+ end
43
+ end
44
+
45
+ context "when the valid secret is informed" do
46
+ before(:each) {
47
+ request.headers['Authorization'] = "Bearer 123123"
48
+ action
49
+ }
50
+ it { JSON.parse(response.body)['errors'].should be_nil }
51
+ it { JSON.parse(response.body)['data'].should_not be_nil }
52
+ end
53
+ end
54
+
55
+ context "when the server has a nil secret" do
56
+ before {
57
+ @previous = BigbluebuttonRails.configuration.api_secret
58
+ BigbluebuttonRails.configuration.api_secret = nil
59
+ }
60
+ after {
61
+ BigbluebuttonRails.configuration.api_secret = @previous
62
+ }
63
+
64
+ [nil, "WRONG", '', "bearer 123123"].each do |auth_header|
65
+ context "forbids the authorization header #{auth_header.inspect}" do
66
+ before(:each) {
67
+ request.headers['Authorization'] = auth_header
68
+ action
69
+ }
70
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('403') }
71
+ it {
72
+ title = JSON.parse(response.body)['errors'][0]['title']
73
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.forbidden.title'))
74
+ }
75
+ it {
76
+ detail = JSON.parse(response.body)['errors'][0]['detail']
77
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.forbidden.msg'))
78
+ }
79
+ end
80
+ end
81
+ end
82
+
83
+ context "when the server has an empty string as secret" do
84
+ before {
85
+ @previous = BigbluebuttonRails.configuration.api_secret
86
+ BigbluebuttonRails.configuration.api_secret = ''
87
+ }
88
+ after {
89
+ BigbluebuttonRails.configuration.api_secret = @previous
90
+ }
91
+
92
+ [nil, "WRONG", '', "bearer 123123"].each do |auth_header|
93
+ context "forbids the authorization header #{auth_header.inspect}" do
94
+ before(:each) {
95
+ request.headers['Authorization'] = auth_header
96
+ action
97
+ }
98
+ it { JSON.parse(response.body)['errors'].should be_nil }
99
+ it { JSON.parse(response.body)['data'].should_not be_nil }
100
+ end
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ describe "#index" do
107
+ context "authenticates" do
108
+ let(:action) { get :index, format: :json }
109
+ it_should_behave_like "an authenticated API call"
110
+ end
111
+
112
+ context "basic" do
113
+ before(:each) { get :index, format: :json }
114
+ it { should respond_with(:success) }
115
+ it { should respond_with_content_type('json') }
116
+ it { should assign_to(:rooms).with([room]) }
117
+ it { JSON.parse(response.body)['data'].should be_an_instance_of(Array) }
118
+ it { JSON.parse(response.body)['data'][0]['type'].should eql('room') }
119
+ it { JSON.parse(response.body)['data'][0]['id'].should eql(room.to_param) }
120
+ end
121
+
122
+ context "content" do
123
+ let(:owner) { FactoryGirl.create(:bigbluebutton_server) } # could be any model
124
+ before { room.update_attributes(owner: owner) }
125
+ before(:each) { get :index, format: :json }
126
+
127
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(room.name) }
128
+ it { JSON.parse(response.body)['data'][0]['attributes']['private'].should eql(room.private) }
129
+ it { JSON.parse(response.body)['data'][0]['links']['self'].should eql(room.short_path) }
130
+
131
+ context "includes the owner in the response" do
132
+ it { JSON.parse(response.body)['data'][0]['relationships']['owner']['data']['type'].should eql('server') }
133
+ it { JSON.parse(response.body)['data'][0]['relationships']['owner']['data']['id'].should eql(owner.to_param) }
134
+ it { JSON.parse(response.body)['data'][0]['relationships']['owner']['data']['attributes']['name'].should eql(owner.name) }
135
+ end
136
+ end
137
+
138
+ context "empty response" do
139
+ before { room.destroy }
140
+ before(:each) { get :index, format: :json }
141
+ it { JSON.parse(response.body)['data'].should be_empty }
142
+ end
143
+
144
+ context "filtering" do
145
+ before { room.update_attributes(name: "La Lo", param: "lalo-1") }
146
+ let!(:room2) { FactoryGirl.create(:bigbluebutton_room, name: "La Le", param: "lale-2") }
147
+ let!(:room3) { FactoryGirl.create(:bigbluebutton_room, name: "Li Lo", param: "lilo") }
148
+
149
+ context "filters by terms" do
150
+ before(:each) { get :index, filter: { terms: 'la' }, format: :json }
151
+ it { JSON.parse(response.body)['data'].length.should be(2) }
152
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql("La Le") }
153
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql("La Lo") }
154
+ end
155
+
156
+ context "orders by number of matches" do
157
+ before(:each) { get :index, filter: { terms: 'la,1' }, format: :json }
158
+ it { JSON.parse(response.body)['data'].length.should be(2) }
159
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql("La Lo") }
160
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql("La Le") }
161
+ end
162
+
163
+ context "strips the terms" do
164
+ before(:each) { get :index, filter: { terms: ' la ' }, format: :json }
165
+ it { JSON.parse(response.body)['data'].length.should be(2) }
166
+ end
167
+ end
168
+
169
+ context "sorting" do
170
+ let!(:room2) { FactoryGirl.create(:bigbluebutton_room, name: room.name + "-2") }
171
+ let!(:room3) { FactoryGirl.create(:bigbluebutton_room, name: room.name + "-3") }
172
+
173
+ context "orders by name" do
174
+ before(:each) { get :index, sort: 'name', format: :json }
175
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(room.name) }
176
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(room2.name) }
177
+ it { JSON.parse(response.body)['data'][2]['attributes']['name'].should eql(room3.name) }
178
+ end
179
+
180
+ context "orders by name DESC" do
181
+ before(:each) { get :index, sort: '-name', format: :json }
182
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(room3.name) }
183
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(room2.name) }
184
+ it { JSON.parse(response.body)['data'][2]['attributes']['name'].should eql(room.name) }
185
+ end
186
+
187
+ context "orders by name by default" do
188
+ before(:each) { get :index, format: :json }
189
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(room.name) }
190
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(room2.name) }
191
+ it { JSON.parse(response.body)['data'][2]['attributes']['name'].should eql(room3.name) }
192
+ end
193
+
194
+ context "orders by activity" do
195
+ before {
196
+ FactoryGirl.create(:bigbluebutton_meeting, create_time: Time.now - 2.hours, room: room)
197
+ FactoryGirl.create(:bigbluebutton_meeting, create_time: Time.now, room: room2)
198
+ FactoryGirl.create(:bigbluebutton_meeting, create_time: Time.now - 1.hour, room: room3)
199
+ }
200
+ before(:each) { get :index, sort: 'activity', format: :json }
201
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(room2.name) }
202
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(room3.name) }
203
+ it { JSON.parse(response.body)['data'][2]['attributes']['name'].should eql(room.name) }
204
+ end
205
+
206
+ context "orders by activity DESC" do
207
+ before {
208
+ FactoryGirl.create(:bigbluebutton_meeting, create_time: Time.now - 2.hours, room: room)
209
+ FactoryGirl.create(:bigbluebutton_meeting, create_time: Time.now, room: room2)
210
+ FactoryGirl.create(:bigbluebutton_meeting, create_time: Time.now - 1.hour, room: room3)
211
+ }
212
+ before(:each) { get :index, sort: '-activity', format: :json }
213
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(room.name) }
214
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(room3.name) }
215
+ it { JSON.parse(response.body)['data'][2]['attributes']['name'].should eql(room2.name) }
216
+ end
217
+
218
+ context "doesn't order by anything else" do
219
+ before {
220
+ FactoryGirl.create(:bigbluebutton_room, attendee_key: "2")
221
+ FactoryGirl.create(:bigbluebutton_room, attendee_key: "1")
222
+ FactoryGirl.create(:bigbluebutton_room, attendee_key: "0")
223
+ }
224
+ before(:each) { get :index, sort: 'attendee_key', format: :json }
225
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(room.name) }
226
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(room2.name) }
227
+ it { JSON.parse(response.body)['data'][2]['attributes']['name'].should eql(room3.name) }
228
+ end
229
+ end
230
+
231
+ context "pagination" do
232
+ context "limits to 10 by default" do
233
+ before {
234
+ 15.times { FactoryGirl.create(:bigbluebutton_room) }
235
+ }
236
+ before(:each) { get :index, format: :json }
237
+ it { JSON.parse(response.body)['data'].length.should be(10) }
238
+ end
239
+
240
+ context "paginates" do
241
+ before {
242
+ 9.times { FactoryGirl.create(:bigbluebutton_room) }
243
+ @rooms = BigbluebuttonRoom.order('name').all
244
+ }
245
+
246
+ context "returns the selected page" do
247
+ before(:each) { get :index, page: { size: 2, number: 3 }, format: :json }
248
+ it { JSON.parse(response.body)['data'].length.should be(2) }
249
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(@rooms[4].name) }
250
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(@rooms[5].name) }
251
+ end
252
+
253
+ context "returns the first page by default" do
254
+ before(:each) { get :index, page: { size: 3 }, format: :json }
255
+ it { JSON.parse(response.body)['data'].length.should be(3) }
256
+ it { JSON.parse(response.body)['data'][0]['attributes']['name'].should eql(@rooms[0].name) }
257
+ it { JSON.parse(response.body)['data'][1]['attributes']['name'].should eql(@rooms[1].name) }
258
+ it { JSON.parse(response.body)['data'][2]['attributes']['name'].should eql(@rooms[2].name) }
259
+ end
260
+
261
+ context "includes the pagination links in the response" do
262
+ before(:each) { get :index, page: { size: 2, number: 3 }, format: :json }
263
+ it { JSON.parse(response.body)['links']['self'].should eql(request.original_url) }
264
+ it {
265
+ uri = URI.parse(request.original_url)
266
+ query = Rack::Utils.parse_query(uri.query)
267
+ query["page[number]"] = 4
268
+ uri.query = Rack::Utils.build_query(query)
269
+ JSON.parse(response.body)['links']['next'].should eql(uri.to_s)
270
+ }
271
+ it {
272
+ uri = URI.parse(request.original_url)
273
+ query = Rack::Utils.parse_query(uri.query)
274
+ query["page[number]"] = 2
275
+ uri.query = Rack::Utils.build_query(query)
276
+ JSON.parse(response.body)['links']['prev'].should eql(uri.to_s)
277
+ }
278
+ it {
279
+ uri = URI.parse(request.original_url)
280
+ query = Rack::Utils.parse_query(uri.query)
281
+ query["page[number]"] = 1
282
+ uri.query = Rack::Utils.build_query(query)
283
+ JSON.parse(response.body)['links']['first'].should eql(uri.to_s)
284
+ }
285
+
286
+ context "doesn't include 'prev' when in the first page" do
287
+ before(:each) { get :index, page: { size: 2, number: 1 }, format: :json }
288
+ it { JSON.parse(response.body)['links']['prev'].should be_nil }
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ describe "#running" do
296
+ before {
297
+ mock_server_and_api
298
+ @api_mock.stub(:is_meeting_running?)
299
+ }
300
+
301
+ context "authenticates" do
302
+ let(:action) { get :running, id: room.to_param, format: :json }
303
+ it_should_behave_like "an authenticated API call"
304
+ end
305
+
306
+ context "basic" do
307
+ before(:each) { get :running, id: room.to_param, format: :json }
308
+ it { should respond_with(:success) }
309
+ it { should respond_with_content_type('json') }
310
+ it { should assign_to(:room).with(room) }
311
+ it { JSON.parse(response.body)['data']['type'].should eql('room') }
312
+ it { JSON.parse(response.body)['data']['id'].should eql(room.to_param) }
313
+ it { room.request_headers["x-forwarded-for"].should eql(request.remote_ip) }
314
+ end
315
+
316
+ context "room is running" do
317
+ before { @api_mock.stub(:is_meeting_running?).and_return(true) }
318
+ before(:each) { get :running, id: room.to_param, format: :json }
319
+ it { JSON.parse(response.body)['data']['attributes']['running'].should be(true) }
320
+ end
321
+
322
+ context "room is running" do
323
+ before { @api_mock.should_receive(:is_meeting_running?).and_return(false) }
324
+ before(:each) { get :running, id: room.to_param, format: :json }
325
+ it { JSON.parse(response.body)['data']['attributes']['running'].should be(false) }
326
+ end
327
+
328
+ context "when the room is not found" do
329
+ before { BigbluebuttonRoom.stub(:find_by).and_return(nil) }
330
+ before(:each) { post :running, id: room.to_param + '-2', format: :json }
331
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('404') }
332
+ it {
333
+ title = JSON.parse(response.body)['errors'][0]['title']
334
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.room_not_found.title'))
335
+ }
336
+ it {
337
+ detail = JSON.parse(response.body)['errors'][0]['detail']
338
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.room_not_found.msg'))
339
+ }
340
+ end
341
+ end
342
+
343
+ describe "#join" do
344
+ let(:expected_url) { 'https://fake-url.no/join?anything=1' }
345
+ before {
346
+ mock_server_and_api
347
+ @api_mock.stub(:is_meeting_running?).and_return(true)
348
+ @api_mock.stub(:join_meeting_url).and_return(expected_url)
349
+ }
350
+
351
+ context "authenticates" do
352
+ let(:action) { post :join, id: room.to_param, format: :json, name: 'User 1' }
353
+ it_should_behave_like "an authenticated API call"
354
+ end
355
+
356
+ context "basic" do
357
+ before { room.update_attributes(private: false) }
358
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1' }
359
+ it { should respond_with(:success) }
360
+ it { should respond_with_content_type('json') }
361
+ it { should assign_to(:room).with(room) }
362
+ it { should assign_to(:url).with(expected_url) }
363
+ it { JSON.parse(response.body)['data']['type'].should eql('join-url') }
364
+ it { JSON.parse(response.body)['data']['id'].should eql(expected_url) }
365
+ it { room.request_headers["x-forwarded-for"].should eql(request.remote_ip) }
366
+ end
367
+
368
+ context "generates the correct join url" do
369
+ let(:expected_url) { 'https://another-fake-url.no/join?anything=1' }
370
+
371
+ context "attendee in a public room" do
372
+ before {
373
+ room.should_receive(:parameterized_join_url).with('User 1', :attendee, nil, {}).and_return(expected_url)
374
+ }
375
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1' }
376
+ it { JSON.parse(response.body)['data']['id'].should eql(expected_url) }
377
+ end
378
+
379
+ context "attendee in a private room" do
380
+ before {
381
+ room.update_attributes(private: true)
382
+ room.should_receive(:parameterized_join_url).with('User 1', :attendee, nil, {}).and_return(expected_url)
383
+ }
384
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1', key: room.attendee_key }
385
+ it { JSON.parse(response.body)['data']['id'].should eql(expected_url) }
386
+ end
387
+
388
+ context "moderator in a private room" do
389
+ before {
390
+ room.update_attributes(private: true)
391
+ room.should_receive(:parameterized_join_url).with('User 1', :moderator, nil, {}).and_return(expected_url)
392
+ }
393
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1', key: room.moderator_key }
394
+ it { JSON.parse(response.body)['data']['id'].should eql(expected_url) }
395
+ end
396
+
397
+ context "with metadata" do
398
+ let(:expected_meta) {
399
+ { 'userdata-param-1' => 1, 'userdata-param_2' => 'string-2' }
400
+ }
401
+ before {
402
+ room.should_receive(:parameterized_join_url).with('User 1', :attendee, nil, expected_meta).and_return(expected_url)
403
+ }
404
+ before(:each) {
405
+ post :join, id: room.to_param, format: :json, name: 'User 1', key: room.moderator_key,
406
+ 'meta-param-1': 1, meta_param_2: 'string-2'
407
+ }
408
+ it { JSON.parse(response.body)['data']['id'].should eql(expected_url) }
409
+ end
410
+ end
411
+
412
+ context "when the room is not found" do
413
+ before { BigbluebuttonRoom.stub(:find_by).and_return(nil) }
414
+ before(:each) { post :join, id: room.to_param + '-2', format: :json, name: 'User 1' }
415
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('404') }
416
+ it {
417
+ title = JSON.parse(response.body)['errors'][0]['title']
418
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.room_not_found.title'))
419
+ }
420
+ it {
421
+ detail = JSON.parse(response.body)['errors'][0]['detail']
422
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.room_not_found.msg'))
423
+ }
424
+ end
425
+
426
+ context "when the room is not running" do
427
+ before { @api_mock.stub(:is_meeting_running?).and_return(false) }
428
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1' }
429
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('400') }
430
+ it {
431
+ title = JSON.parse(response.body)['errors'][0]['title']
432
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.room_not_running.title'))
433
+ }
434
+ it {
435
+ detail = JSON.parse(response.body)['errors'][0]['detail']
436
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.room_not_running.msg'))
437
+ }
438
+ end
439
+
440
+ context "when a name is not informed" do
441
+ before(:each) { post :join, id: room.to_param, format: :json }
442
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('400') }
443
+ it {
444
+ title = JSON.parse(response.body)['errors'][0]['title']
445
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.missing_params.title'))
446
+ }
447
+ it {
448
+ detail = JSON.parse(response.body)['errors'][0]['detail']
449
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.missing_params.msg'))
450
+ }
451
+ end
452
+
453
+ context "when a key is not informed and the room is private" do
454
+ before { room.update_attributes(private: true) }
455
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1' }
456
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('400') }
457
+ it {
458
+ title = JSON.parse(response.body)['errors'][0]['title']
459
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.missing_params.title'))
460
+ }
461
+ it {
462
+ detail = JSON.parse(response.body)['errors'][0]['detail']
463
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.missing_params.msg'))
464
+ }
465
+ end
466
+
467
+ context "attendee in a private room with wrong key" do
468
+ before { room.update_attributes(private: true) }
469
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1', key: 'WRONG' }
470
+ it { JSON.parse(response.body)['errors'][0]['status'].should eql('403') }
471
+ it {
472
+ title = JSON.parse(response.body)['errors'][0]['title']
473
+ title.should eql(I18n.t('bigbluebutton_rails.api.errors.invalid_key.title'))
474
+ }
475
+ it {
476
+ detail = JSON.parse(response.body)['errors'][0]['detail']
477
+ detail.should eql(I18n.t('bigbluebutton_rails.api.errors.invalid_key.msg'))
478
+ }
479
+ end
480
+
481
+ context "when guest support is on" do
482
+ let(:expected_url) { 'https://another-fake-url.no/join?anything=1' }
483
+ before {
484
+ @guest_support = BigbluebuttonRails.configuration.guest_support
485
+ BigbluebuttonRails.configuration.guest_support = true
486
+ }
487
+ after {
488
+ BigbluebuttonRails.configuration.guest_support = @guest_support
489
+ }
490
+
491
+ before {
492
+ room.should_receive(:parameterized_join_url).with('User 1', :guest, nil, {}).and_return(expected_url)
493
+ }
494
+ before(:each) { post :join, id: room.to_param, format: :json, name: 'User 1' }
495
+ it { JSON.parse(response.body)['data']['id'].should eql(expected_url) }
496
+ end
497
+ end
498
+ end