diff --git a/.env.sample b/.env.sample index 93c5cb8..bd7317f 100644 --- a/.env.sample +++ b/.env.sample @@ -32,3 +32,10 @@ CUSTOM_API_TOKEN= # Required for simulcasting to Millicast for director mode MILLICAST_API_SECRET= MILLICAST_ACCOUNT_ID= + +# Required for Microsoft Azure AD Auth +AZURE_CLIENT_ID = Client App ID +AZURE_CLIENT_SECRET = Client App Secret +AZURE_TENANT_ID = Client App Tenant ID +AZURE_REDIRECT_URI = where microsoft will redirect after login, eg. http://localhost:3000/auth/azure_ad/callback +AZURE_SCOPES = Scopes required for Application, eg. 'openid email profile User.Read offline_access OnlineMeetings.ReadWrite' \ No newline at end of file diff --git a/Gemfile b/Gemfile index 5edcd45..e98f6e0 100644 --- a/Gemfile +++ b/Gemfile @@ -139,6 +139,11 @@ gem 'rack-cors' # Ruby wrappers for the HubSpot REST API gem "hubspot-ruby" +# OAuth +gem 'omniauth-oauth2', '~> 1.6' +# OmniAuth CSRF protection +gem 'omniauth-rails_csrf_protection', '~> 0.1.2' + group :development, :test, :review do # Call "byebug" anywhere in the code to stop execution and get a debugger console gem "byebug", "~> 11.0.1", platforms: [:mri, :mingw, :x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index 6998497..34bba69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -220,6 +220,7 @@ GEM activesupport (>= 4.2.0) hashdiff (1.0.1) hashery (2.1.2) + hashie (4.1.0) hexapdf (0.9.3) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.2) @@ -297,6 +298,7 @@ GEM money (~> 6.13.2) railties (>= 3.0) msgpack (1.3.1) + multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) nio4r (2.5.2) @@ -308,6 +310,21 @@ GEM warden oath-generators (1.0.1) oath (>= 0.0.12) + oauth2 (1.4.4) + faraday (>= 0.8, < 2.0) + jwt (>= 1.0, < 3.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.9.1) + hashie (>= 3.4.6) + rack (>= 1.6.2, < 3) + omniauth-oauth2 (1.6.0) + oauth2 (~> 1.1) + omniauth (~> 1.9) + omniauth-rails_csrf_protection (0.1.2) + actionpack (>= 4.2) + omniauth (>= 1.3.1) parallel (1.19.1) parity (3.2.0) parser (2.6.5.0) @@ -552,6 +569,8 @@ DEPENDENCIES mux_ruby! oath (~> 1.1.0) oath-generators (~> 1.0.1) + omniauth-oauth2 (~> 1.6) + omniauth-rails_csrf_protection (~> 0.1.2) parity (~> 3.2.0) pdf-reader (~> 2.1.0) pdfkit (~> 0.8.2) diff --git a/app/controllers/broadcasts_controller.rb b/app/controllers/broadcasts_controller.rb index e6249cc..8b33b6b 100644 --- a/app/controllers/broadcasts_controller.rb +++ b/app/controllers/broadcasts_controller.rb @@ -75,7 +75,7 @@ class BroadcastsController < ApplicationController end def broadcast_params - params.require(:broadcast).permit(:name, :shoot_location_time_zone, files: []) + params.require(:broadcast).permit(:name, :shoot_location_time_zone, :conference_option, files: []) end def set_project @@ -113,7 +113,7 @@ class BroadcastsController < ApplicationController end def conference_url_for(broadcast) - broadcast.video_conference_url_override || url_for([broadcast.project, broadcast, :zoom_meeting]) + broadcast.video_conference_url_override || url_for([broadcast.project, broadcast, :conference_meeting]) end def log_create_analytics diff --git a/app/controllers/callbacks_controller.rb b/app/controllers/callbacks_controller.rb new file mode 100644 index 0000000..c10f19b --- /dev/null +++ b/app/controllers/callbacks_controller.rb @@ -0,0 +1,22 @@ +class CallbacksController < ApplicationController + skip_before_action :require_login + skip_after_action :verify_authorized, except: :index + skip_after_action :verify_policy_scoped, only: :index + skip_before_action :verify_authenticity_token + + def create + uid = request.env['omniauth.auth'][:uid] + token_data = request.env['omniauth.auth'][:credentials] + + current_user&.tap do |user| + user.microsoft_user_id = uid + user.microsoft_access_token = token_data.token + user.microsoft_refresh_token = token_data.refresh_token + user.microsoft_token_expires_at = token_data.expires_at # Expiration time is returned in seconds + user.save + end + + redirect_to profile_path + end + +end \ No newline at end of file diff --git a/app/controllers/conference_meetings_controller.rb b/app/controllers/conference_meetings_controller.rb new file mode 100644 index 0000000..42c96c0 --- /dev/null +++ b/app/controllers/conference_meetings_controller.rb @@ -0,0 +1,45 @@ +class ConferenceMeetingsController < ApplicationController + require 'microsoft_graph' + + def show + authorize broadcast = Broadcast.find(params[:broadcast_id]) + case broadcast.conference_option + when 'zoom' + redirect_to broadcast.zoom_meeting_url + when 'ms_teams' + if broadcast.conference_join_url.nil? + begin + graph_api = MicrosoftGraph.new( + current_user, + ENV['AZURE_CLIENT_ID'], + ENV['AZURE_CLIENT_SECRET'], + ENV['AZURE_TENANT_ID'], + ENV['AZURE_SCOPES'] + ) + + subject = "#{broadcast.name} Online Meeting" + teams_meeting = graph_api.create_teams_meeting(subject) + join_url = teams_meeting['joinUrl'] + + if join_url.present? + broadcast.conference_join_url = join_url + broadcast.save + else + raise StandardError, 'Failed to read teams meeting join URL' + end + rescue ActionController::InvalidAuthenticityToken => e + Rails.logger.error(e.message) + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: t('.alerts.not_authenticated') + return + rescue StandardError => e + Rails.logger.error(e.message) + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: t('.alerts.failed_to_join') + return + end + end + redirect_to broadcast.conference_join_url + else + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: t('.alerts.unknown_conference_option') + end + end +end \ No newline at end of file diff --git a/app/controllers/zoom_meetings_controller.rb b/app/controllers/zoom_meetings_controller.rb deleted file mode 100644 index 22e3d56..0000000 --- a/app/controllers/zoom_meetings_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -class ZoomMeetingsController < ApplicationController - def show - authorize broadcast = Broadcast.find(params[:broadcast_id]) - redirect_to broadcast.zoom_meeting_url - end -end \ No newline at end of file diff --git a/app/helpers/broadcast_conferences_helper.rb b/app/helpers/broadcast_conferences_helper.rb new file mode 100644 index 0000000..05ab097 --- /dev/null +++ b/app/helpers/broadcast_conferences_helper.rb @@ -0,0 +1,17 @@ +module BroadcastConferencesHelper + def options_for_conference_select + [ + ['Zoom', 'zoom'], + ['MS Teams', 'ms_teams'] + ] + end + + def conference_option_name_from_key(key) + option = options_for_conference_select.find { |option| option[1] == key } + if option.present? + option.first + else + 'Unknown conference option' + end + end +end \ No newline at end of file diff --git a/app/views/broadcasts/_form.html.erb b/app/views/broadcasts/_form.html.erb index 2efc4bd..b60665d 100644 --- a/app/views/broadcasts/_form.html.erb +++ b/app/views/broadcasts/_form.html.erb @@ -2,6 +2,7 @@ <%= bootstrap_form_with model: model, local: true do |form| %> <%= form.text_field :name %> + <%= form.select :conference_option, options_for_conference_select, { label: t('.labels.conference_option') }, class: "form-control custom-select" %> <%= form.time_zone_select(:shoot_location_time_zone, nil, label: "Time zone of shoot location") %>
diff --git a/app/views/broadcasts/show.html.erb b/app/views/broadcasts/show.html.erb index a63ac4f..6d2f6b8 100644 --- a/app/views/broadcasts/show.html.erb +++ b/app/views/broadcasts/show.html.erb @@ -127,8 +127,8 @@

<% end %> -

If you want to join the ZOOM meeting dedicated to this broadcast, follow the link below.

- <%= link_to 'Video Conference', @conference_url, class: 'btn btn-primary btn-block', target: '_blank' %> +

<%= "If you want to join the #{conference_option_name_from_key(@broadcast.conference_option)} meeting dedicated to this broadcast, follow the link below." %>

+ <%= link_to 'Video Conference', @conference_url, class: "btn btn-primary btn-block", target: '_blank' %>
params[:active_tab] == 'recordings') %>" id="recordings">
diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 0e989e8..d677cab 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -17,6 +17,7 @@ <%= @user.role_for(Current.account).to_s.titleize %> <% end %>

+ <%= link_to 'Auth to Microsoft', '/auth/azure_ad', method: :post, class: "btn btn-primary" %>
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 0000000..87f4e6a --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,13 @@ +require 'azure_ad' + +Rails.application.config.middleware.use OmniAuth::Builder do + provider :azure_ad, + client_id: ENV['AZURE_CLIENT_ID'], + client_secret: ENV['AZURE_CLIENT_SECRET'], + redirect_uri: ENV['AZURE_REDIRECT_URI'], + client_options: { + token_url: "#{ENV['AZURE_TENANT_ID']}/oauth2/v2.0/token", + authorize_url: "#{ENV['AZURE_TENANT_ID']}/oauth2/v2.0/authorize" + }, + scope: ENV['AZURE_SCOPES'] +end diff --git a/config/locales/en.yml b/config/locales/en.yml index ac5c824..5389ebe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -275,6 +275,9 @@ en: stream_multiple_cameras: Stream multiple cameras at one time update: reset_notice: The Share URL has been reset, and the previous URL will no longer work. Please click "Copy URL" and share it again with those who you want to have access to this live stream + form: + labels: + conference_option: Conference Option bulk_taggings: new_bulk_tag_modal: submit: Add @@ -1642,3 +1645,9 @@ en: edit: Edit report: Report generating: Generating... + conference_meetings: + show: + alerts: + not_authenticated: You are not authenticated via Microsoft, please authenticate and try again + failed_to_join: Failed to join conference + unknown_conference_option: Unknown conference option diff --git a/config/locales/es.yml b/config/locales/es.yml index 3383633..0479da8 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -131,6 +131,9 @@ es: stream_multiple_cameras: Stream multiple cameras at one time update: reset_notice: The Share URL has been reset, and the previous URL will no longer work. Please click "Copy URL" and share it again with those who you want to have access to this live stream + form: + labels: + conference_option: Conference Option (ES) contract_templates: blank_contracts: create: @@ -705,3 +708,9 @@ es: production_elements_logs: Production Elements Logs, and more (ES) reduces_labor_cost: Reduces labor costs (ES) simplifies_cue_sheets: Simplifies Music Cue Sheets, Graphic Cue Sheets (ES) + conference_meetings: + show: + alerts: + not_authenticated: You are not authenticated via Microsoft, please authenticate and try again (ES) + failed_to_join: Failed to join conference (ES) + unknown_conference_option: Unknown conference option (ES) diff --git a/config/routes.rb b/config/routes.rb index 79bb52d..b89da54 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,8 @@ require 'sidekiq/web' Rails.application.routes.draw do AVAILABLE_LOCALES_REGEX = /#{I18n.available_locales.join("|")}/.freeze + get 'auth/azure_ad/callback', to: 'callbacks#create' + concern :confirmable do resources :video_release_confirmations, only: [:new, :create, :destroy] end @@ -100,7 +102,7 @@ Rails.application.routes.draw do member do delete :destroy_file end - resource :zoom_meeting, only: [:show] + resource :conference_meeting, only: [:show] resources :broadcast_recordings, only: :destroy end resources :directories, except: [:index] do diff --git a/db/migrate/20200820081251_add_microsoft_info_to_users.rb b/db/migrate/20200820081251_add_microsoft_info_to_users.rb new file mode 100644 index 0000000..0fd69db --- /dev/null +++ b/db/migrate/20200820081251_add_microsoft_info_to_users.rb @@ -0,0 +1,8 @@ +class AddMicrosoftInfoToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :microsoft_user_id, :string + add_column :users, :microsoft_access_token, :string + add_column :users, :microsoft_refresh_token, :string + add_column :users, :microsoft_token_expires_at, :integer + end +end \ No newline at end of file diff --git a/db/migrate/20200820081524_add_conference_details_to_broadcasts.rb b/db/migrate/20200820081524_add_conference_details_to_broadcasts.rb new file mode 100644 index 0000000..72a5d9e --- /dev/null +++ b/db/migrate/20200820081524_add_conference_details_to_broadcasts.rb @@ -0,0 +1,6 @@ +class AddConferenceDetailsToBroadcasts < ActiveRecord::Migration[6.0] + def change + add_column :broadcasts, :conference_option, :string + add_column :broadcasts, :conference_join_url, :string + end +end \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9c06dc7..1948cc0 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -586,7 +586,9 @@ CREATE TABLE public.broadcasts ( stream_key_override character varying, director_mode_video_embed text, simulcast_uid character varying, - video_conference_url_override character varying + video_conference_url_override character varying, + conference_option character varying, + conference_join_url character varying ); @@ -1804,7 +1806,11 @@ CREATE TABLE public.users ( remember_created_at timestamp without time zone, first_name character varying, last_name character varying, - time_zone character varying DEFAULT 'UTC'::character varying NOT NULL + time_zone character varying DEFAULT 'UTC'::character varying NOT NULL, + microsoft_user_id character varying, + microsoft_access_token character varying, + microsoft_refresh_token character varying, + microsoft_token_expires_at integer ); @@ -4025,6 +4031,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200807190607'), ('20200811102720'), ('20200812060406'), -('20200819070738'); +('20200819070738'), +('20200820081251'), +('20200820081524'); diff --git a/lib/azure_ad.rb b/lib/azure_ad.rb new file mode 100644 index 0000000..04ae4cc --- /dev/null +++ b/lib/azure_ad.rb @@ -0,0 +1,124 @@ +require 'omniauth-oauth2' + +# This file is from omniauth-microsoft_graph lib (not installed) +# It is modified to make auth work + +module OmniAuth + module Strategies + class AzureAd < OmniAuth::Strategies::OAuth2 + BASE_SCOPE_URL = 'https://graph.microsoft.com/' + BASE_SCOPES = %w[offline_access openid email profile].freeze + DEFAULT_SCOPE = 'offline_access openid email profile User.Read'.freeze + + option :name, :azure_ad + + option :client_options, + site: 'https://login.microsoftonline.com/' + + option :authorize_options, %i[state callback_url scope response_mode] + + option :token_params, {} + + option :scope, DEFAULT_SCOPE + option :authorized_client_ids, [] + + uid { raw_info["id"] } + + info do + { + # 'email' => raw_info["mail"], + # 'first_name' => raw_info["givenName"], + # 'last_name' => raw_info["surname"], + # 'name' => [raw_info["givenName"], raw_info["surname"]].join(' '), + # 'nickname' => raw_info["displayName"], + } + end + + extra do + { + # 'raw_info' => raw_info, + # 'params' => access_token.params, + # 'aud' => options.client_id + } + end + + def authorize_params + super.tap do |params| + options[:authorize_options].each do |k| + params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s]) + end + + params[:scope] = get_scope(params) + + session['omniauth.state'] = params[:state] if params[:state] + end + end + + def raw_info + @raw_info ||= access_token.get('https://graph.microsoft.com/v1.0/me').parsed + end + + def callback_url + options[:callback_url] || full_host + script_name + callback_path + end + + def custom_build_access_token + token_response = get_access_token(request) + session[:microsoft_graph_api_token] = token_response.token + token_response + end + + alias build_access_token custom_build_access_token + + private + + def get_access_token(request) + verifier = request.params['code'] + redirect_uri = request.params['redirect_uri'] || request.params['callback_url'] + if verifier && request.xhr? + client_get_token(verifier, redirect_uri || '/auth/azure_ad/callback') + elsif verifier + client_get_token(verifier, redirect_uri || callback_url) + elsif verify_token(request.params['access_token']) + ::OAuth2::AccessToken.from_hash(client, request.params.dup) + elsif request.content_type =~ /json/i + begin + body = JSON.parse(request.body.read) + request.body.rewind # rewind request body for downstream middlewares + verifier = body && body['code'] + client_get_token(verifier, '/auth/azure_ad/callback') if verifier + rescue JSON::ParserError => e + warn "[omniauth google-oauth2] JSON parse error=#{e}" + end + end + end + + def client_get_token(verifier, redirect_uri) + client.auth_code.get_token(verifier, get_token_options(redirect_uri), get_token_params) + end + + def get_token_params + deep_symbolize(options.auth_token_params || {}) + end + + def get_token_options(redirect_uri = '') + { redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true)) + end + + def get_scope(params) + raw_scope = params[:scope] || DEFAULT_SCOPE + scope_list = raw_scope.split(' ').map { |item| item.split(',') }.flatten + scope_list.map! { |s| s =~ %r{^https?://} || BASE_SCOPES.include?(s) ? s : "#{BASE_SCOPE_URL}#{s}" } + scope_list.join(' ') + end + + def verify_token(access_token) + return false unless access_token + # access_token.get('https://graph.microsoft.com/v1.0/me').parsed + raw_response = client.request(:get, 'https://graph.microsoft.com/v1.0/me', + params: { access_token: access_token }).parsed + (raw_response['aud'] == options.client_id) || options.authorized_client_ids.include?(raw_response['aud']) + end + end + end +end \ No newline at end of file diff --git a/lib/microsoft_graph.rb b/lib/microsoft_graph.rb new file mode 100644 index 0000000..160b164 --- /dev/null +++ b/lib/microsoft_graph.rb @@ -0,0 +1,107 @@ +require 'httparty' + +class MicrosoftGraph + BASE_URL = 'https://graph.microsoft.com/v1.0'.freeze + + def initialize(current_user, client_id, client_secret, tenant_id, scopes) + @current_user = current_user + @uid = current_user.microsoft_user_id + @token = current_user.microsoft_access_token + @refresh_token = current_user.microsoft_refresh_token + @token_expires_at = current_user.microsoft_token_expires_at + + @client_id = client_id + @client_secret = client_secret + @tenant_id = tenant_id + @scopes = scopes + end + + def create_teams_meeting(subject) + if @refresh_token.nil? || @token_expires_at.nil? + raise ActionController::InvalidAuthenticityToken, 'Missing refresh token / token expiration' + return + end + + # Obtain new token if token is expired or will expire in less than 5 minutes + if 5.minutes.from_now.to_i > @token_expires_at.seconds + refresh_access_token + end + + if @token.nil? + raise ActionController::InvalidAuthenticityToken, 'Missing access token' + return + end + + response = HTTParty.post( + "#{BASE_URL}/me/onlineMeetings", + body: { + subject: subject, + participants: { + organizer: { + identity: { + user: { + id: @uid + } + } + } + } + }.to_json, + headers: { + Authorization: "Bearer #{@token}", + 'Content-Type': 'application/json' + } + ) + + raise StandardError, 'Authenticated user does not have a permission to create Teams Online Meeting' if response.code == 403 + + if response.code != 201 + Rails.logger.error('[Microsoft Graph Error]') + Rails.logger.error(response.inspect) + raise StandardError, "Failed to create teams meeting [#{response.code}]" + else + JSON.parse(response.body) + end + end + + private + + def refresh_token_url + "https://login.microsoftonline.com/#{@tenant_id}/oauth2/v2.0/token" + end + + def refresh_access_token + response = HTTParty.post(refresh_token_url, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: { + client_id: @client_id, + client_secret: @client_secret, + refresh_token: @refresh_token, + grant_type: 'refresh_token', + scope: @scopes + }) + + if response.code != 200 + Rails.logger.error '[Microsoft Graph Error] Failed to obtain new access token using refresh token' + Rails.logger.error(response.inspect) + raise StandardError, 'Failed to obtain new access token' + end + + parsed_response = JSON.parse(response.body) + + new_access_token = parsed_response['access_token'] + new_refresh_token = parsed_response['refresh_token'] + token_expires_in = parsed_response['expires_in'] # For how long access token is valid (in seconds) + token_new_expiration_time = Time.now.to_i + token_expires_in + + @current_user.microsoft_access_token = new_access_token + @current_user.microsoft_refresh_token = new_refresh_token + @current_user.microsoft_token_expires_at = token_new_expiration_time + @current_user.save! + + @token = new_access_token + @refresh_token = new_refresh_token + @token_expires_at = token_new_expiration_time + end +end \ No newline at end of file diff --git a/spec/controllers/broadcasts_controller_spec.rb b/spec/controllers/broadcasts_controller_spec.rb index fc14c94..b844283 100644 --- a/spec/controllers/broadcasts_controller_spec.rb +++ b/spec/controllers/broadcasts_controller_spec.rb @@ -157,16 +157,24 @@ RSpec.describe BroadcastsController, type: :controller do expect(response.body).to have_xpath "//input[@readonly][@value='#{broadcast_url(broadcast.token)}']" end - it "displays zoom meeting button" do + it "displays zoom meeting button for zoom conference option" do get :show, params: { project_id: project.id, id: broadcast.id } - expect(response.body).to have_link("Video Conference", href: project_broadcast_zoom_meeting_url(project, broadcast)) + expect(response.body).to have_link("Video Conference", href: project_broadcast_conference_meeting_url(project, broadcast)) + end + + it "displays microsoft teams meeting button for MS Teams conference option" do + ms_teams_broadcast = create(:broadcast, :ms_teams_conference, project: project ) + get :show, params: { project_id: project.id, id: ms_teams_broadcast.id } + + expect(response.body).to have_content 'MS Teams' + expect(response.body).to have_link 'Video Conference', href: project_broadcast_conference_meeting_url(project, ms_teams_broadcast) end it "assigns required variables" do get :show, params: { project_id: project.id, id: broadcast.id } - expect(assigns(:conference_url)).to eq project_broadcast_zoom_meeting_url(project, broadcast) + expect(assigns(:conference_url)).to eq project_broadcast_conference_meeting_url(project, broadcast) expect(assigns(:broadcast)).to eq broadcast end diff --git a/spec/controllers/conference_meetings_controller_spec.rb b/spec/controllers/conference_meetings_controller_spec.rb new file mode 100644 index 0000000..6257ce2 --- /dev/null +++ b/spec/controllers/conference_meetings_controller_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +RSpec.describe ConferenceMeetingsController, type: :controller do + let(:user) { create(:user) } + let(:account) { user.primary_account } + let(:project) { create(:project, account: user.primary_account) } + let(:broadcast) { create(:broadcast, name: "Broadcast", project: project) } + let(:ms_teams_broadcast) { create(:broadcast, :ms_teams_conference, project: project) } + let(:unknown_option_broadcast) { create(:broadcast, project: project, conference_option: 'google') } + + let(:meeting_start_url) { "http://meeting_start_url" } + let(:meeting_hash) { HashWithIndifferentAccess.new(start_url: meeting_start_url) } + let(:user_create_response) { {"id" => "new_host_id"} } + let(:roles_assign_response) { {"ids" => ["new_host_id"]} } + let(:roles_list_response) { {"roles" => [{"name" => "directme-host"}]} } + + + before :each do + allow_any_instance_of(ZoomGateway).to receive(:find_meeting).and_return(meeting_hash) + allow_any_instance_of(ZoomGateway).to receive(:create_meeting).and_return("meeting_id") + allow_any_instance_of(ZoomGateway).to receive(:create_host).and_return("host_id") + stub_mux_live_stream + end + + describe "#show" do + before { sign_in user } + + it "redirects to meeting start url with Zoom conference option" do + get :show, params: { project_id: project.id, broadcast_id: broadcast.id } + expect(response).to redirect_to(meeting_start_url) + end + + it "redirects to the broadcast show page with alert if user is not authenticated via microsoft and tries to create MS Teams meeting" do + get :show, params: { project_id: project.id, broadcast_id: ms_teams_broadcast.id } + expect(response).to redirect_to project_broadcast_path(project, ms_teams_broadcast) + expect(flash.alert).to eq not_authenticated_alert + end + + it "redirects to the broadcast show page with alert if user is authenticated via microsoft and tries to create MS Teams meeting but Graph API fails to create meeting" do + allow_any_instance_of(MicrosoftGraph).to receive(:create_teams_meeting).and_return(nil) + get :show, params: { project_id: project.id, broadcast_id: ms_teams_broadcast.id } + expect(response).to redirect_to project_broadcast_path(project, ms_teams_broadcast) + expect(flash.alert).to eq failed_to_join_alert + end + + it "redirects to the broadcast show page with alert if conference option is not reckognized" do + get :show, params: { project_id: project.id, broadcast_id: unknown_option_broadcast.id } + expect(response).to redirect_to project_broadcast_path(project, unknown_option_broadcast) + expect(flash.alert).to eq unknown_conference_option_alert + end + + it "redirects to meeting start url with MS Teams conference option" do + new_ms_teams_meeting = JSON.parse({ joinUrl: meeting_start_url }.to_json) + + allow_any_instance_of(MicrosoftGraph).to receive(:create_teams_meeting).and_return(new_ms_teams_meeting) + + get :show, params: { project_id: project.id, broadcast_id: ms_teams_broadcast.id } + expect(response).to redirect_to(meeting_start_url) + end + end + + private + + def not_authenticated_alert + t 'conference_meetings.show.alerts.not_authenticated' + end + + def failed_to_join_alert + t 'conference_meetings.show.alerts.failed_to_join' + end + + def unknown_conference_option_alert + t 'conference_meetings.show.alerts.unknown_conference_option' + end +end diff --git a/spec/controllers/zoom_meetings_controller_spec.rb b/spec/controllers/zoom_meetings_controller_spec.rb deleted file mode 100644 index 66092b4..0000000 --- a/spec/controllers/zoom_meetings_controller_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' - -RSpec.describe ZoomMeetingsController, type: :controller do - let(:user) { create(:user) } - let(:account) { user.primary_account } - let(:project) { create(:project, account: user.primary_account) } - let(:broadcast) { create(:broadcast, name: "Broadcast", project: project) } - - let(:meeting_start_url) { "http://meeting_start_url" } - let(:meeting_hash) { HashWithIndifferentAccess.new(start_url: meeting_start_url) } - let(:user_create_response) { {"id" => "new_host_id"} } - let(:roles_assign_response) { {"ids" => ["new_host_id"]} } - let(:roles_list_response) { {"roles" => [{"name" => "directme-host"}]} } - - before :each do - allow_any_instance_of(ZoomGateway).to receive(:find_meeting).and_return(meeting_hash) - allow_any_instance_of(ZoomGateway).to receive(:create_meeting).and_return("meeting_id") - allow_any_instance_of(ZoomGateway).to receive(:create_host).and_return("host_id") - stub_mux_live_stream - end - - describe "#show" do - before { sign_in user } - - it "redirects to meeting start url" do - get :show, params: { project_id: project.id, broadcast_id: broadcast.id } - expect(response).to redirect_to(meeting_start_url) - end - end -end diff --git a/spec/factories/broadcasts.rb b/spec/factories/broadcasts.rb index d077ba8..28780d8 100644 --- a/spec/factories/broadcasts.rb +++ b/spec/factories/broadcasts.rb @@ -2,11 +2,16 @@ FactoryBot.define do factory :broadcast do association :project name "My Live Stream" + conference_option "zoom" transient do skip_create_callback false end + trait :ms_teams_conference do + conference_option "ms_teams" + end + trait :with_stream do stream_uid "mux_stream" stream_key "mux_key" diff --git a/spec/features/user_managing_broadcasts_spec.rb b/spec/features/user_managing_broadcasts_spec.rb index 9c40b71..36766ce 100644 --- a/spec/features/user_managing_broadcasts_spec.rb +++ b/spec/features/user_managing_broadcasts_spec.rb @@ -25,6 +25,7 @@ feature 'User managing broadcasts' do by 'filling out the form' do fill_in broadcast_name_field, with: 'My Broadcast' + select_conference_option('Zoom') select_time_zone("New Delhi") end @@ -352,5 +353,10 @@ feature 'User managing broadcasts' do t 'broadcasts.file.actions.delete_file' end + def select_conference_option(value) + if value.present? + select value, from: "broadcast[conference_option]" + end + end end