diff --git a/.gitignore b/.gitignore index d6a504d..27ab372 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ converter/ logs/*.log tmp node_modules/ +coverage diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/Gemfile b/Gemfile index dd038c9..7d9e92d 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,9 @@ group :development, :test do end group :test do - gem 'database_cleaner' + gem 'database_cleaner', '~> 1.6', '>= 1.6.2' + gem 'nokogiri', '~> 1.8', '>= 1.8.2' gem 'rack-test', '~> 0.8.2' gem 'rspec', '~> 3.7' + gem 'simplecov', '~> 0.15.1' end diff --git a/Gemfile.lock b/Gemfile.lock index b2a815b..2e735ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,6 +29,7 @@ GEM declarative (0.0.10) declarative-option (0.1.0) diff-lcs (1.3) + docile (1.1.5) dotenv (2.2.1) execjs (2.7.0) faraday (0.14.0) @@ -61,6 +62,7 @@ GEM faraday_middleware (~> 0.8) json-jwt (~> 1.7) simple_oauth (~> 0.3.1) + json (2.1.0) json-jwt (1.8.3) activesupport bindata @@ -76,10 +78,13 @@ GEM mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) + mini_portile2 (2.3.0) minitest (5.11.3) multi_json (1.13.1) multipart-post (2.0.0) mustermann (1.0.2) + nokogiri (1.8.2) + mini_portile2 (~> 2.3.0) os (0.9.6) pg (0.21.0) pry (0.11.3) @@ -134,6 +139,11 @@ GEM jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simple_oauth (0.3.1) + simplecov (0.15.1) + docile (~> 1.1.0) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) sinatra (2.0.1) mustermann (~> 1.0) rack (~> 2.0) @@ -176,11 +186,12 @@ PLATFORMS DEPENDENCIES activerecord (~> 5.1, >= 5.1.4) byebug - database_cleaner + database_cleaner (~> 1.6, >= 1.6.2) dotenv (~> 2.2, >= 2.2.1) google-api-client (~> 0.19.6) googleauth (~> 0.6.2) ims-lti (~> 2.2, >= 2.2.3) + nokogiri (~> 1.8, >= 1.8.2) pg (~> 0.18) pry-byebug puma @@ -191,6 +202,7 @@ DEPENDENCIES require_all (~> 1.5) rspec (~> 3.7) sass (~> 3.5, >= 3.5.5) + simplecov (~> 0.15.1) sinatra (~> 2.0.1) sinatra-activerecord (~> 2.0, >= 2.0.13) sinatra-asset-pipeline (~> 2.0) diff --git a/lib/app_helpers.rb b/lib/app_helpers.rb index 1f5107b..cc71ff5 100644 --- a/lib/app_helpers.rb +++ b/lib/app_helpers.rb @@ -11,7 +11,7 @@ def authenticate_lti session[:return_url] = params['content_item_return_url'] else logger.warn("LTI Authentication error: #{lti_auth.error}") - error 401 + error 401, lti_auth.error end end diff --git a/lib/gdrive_service.rb b/lib/gdrive_service.rb index 4ff2837..0d4296f 100644 --- a/lib/gdrive_service.rb +++ b/lib/gdrive_service.rb @@ -19,7 +19,6 @@ def initialize(credentials) end def list(folder = 'root') - puts "list for '#{folder}'" gdrive_files = service.fetch_all(items: :files) do |token| service.list_files( q: "'#{folder}' in parents and trashed = false", diff --git a/spec/app_spec.rb b/spec/app_spec.rb index faefbba..1839f5e 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -12,20 +12,79 @@ before { get '/config.xml' } it { expect(last_response).to be_ok } - it { expect(last_response.content_type).to match(%r{application/xml}) } - it { expect(last_response).to match(//) } + it { expect(last_response.content_type).to include('application/xml') } + it 'has a launch_url' do + config = Nokogiri::XML(last_response.body) + expect(config.at_xpath('//blti:launch_url')).to be_present + end end describe 'credentials' do it 'renders a generate credentials buttons' do get '/credentials/new' - expect(last_response).to match(/class="credentials-generate-btn"/) + expect(last_response).to have_css('.credentials-generate-btn') end it 'generates new credentials' do - creds_count = AuthCredential.count - post '/credentials' - expect(AuthCredential.count).to eq(creds_count + 1) - expect(last_response).to match(%r{
#{AuthCredential.last.key}
}) + expect { post '/credentials' }.to change { AuthCredential.count }.by(1) + credential = Nokogiri::HTML(last_response.body).css('.credential-value').first + expect(credential).to be_present + expect(credential.text).to eq AuthCredential.last.key + end +end + +describe 'LTI authentication' do + it 'requires a valid key/secret pair' do + post '/lti/course-navigation' + expect(last_response.status).to eq 401 + expect(last_response.body).to include('No key/pair credentials for') + end + + it 'requires an valid oauth signature' do + lti_request '/lti/course-navigation', signature: 'wrong-signature' + expect(last_response.status).to eq 401 + expect(last_response.body).to include('Invalid Signature') + end + + it 'expires the timestamp in 5 minutes' do + lti_request '/lti/course-navigation', timestamp: (Time.current - 5.minutes).to_i + expect(last_response.status).to eq 401 + expect(last_response.body).to include('Timestamp expired') + end + + it 'set session user when request is valid' do + lti_request '/lti/course-navigation' + expect(last_response).to be_ok + expect(session['user_id']).to eq 'user-id' + end +end + +describe 'course-navigation' do + it 'authenticates oauth LTI requests' do + lti_request '/lti/course-navigation' + expect(last_response).to be_ok + end + + it 'render file-browser component when is authorized on google' do + allow_any_instance_of(GoogleAuth).to receive(:credentials).and_return(OpenStruct.new) + lti_request '/lti/course-navigation' + expect(last_response).to have_css('.file-browser') + component = Nokogiri::HTML(last_response.body).css('.file-browser').first + expect(component.attr('data-browser-type')).to eq 'navigation' + end +end + +describe 'editor-selection' do + it 'authenticates oauth LTI requests' do + lti_request '/lti/editor-selection' + expect(last_response).to be_ok + end + + it 'render file-browser component when is authorized on google' do + allow_any_instance_of(GoogleAuth).to receive(:credentials).and_return(OpenStruct.new) + lti_request '/lti/editor-selection' + expect(last_response).to have_css('.file-browser') + component = Nokogiri::HTML(last_response.body).css('.file-browser').first + expect(component.attr('data-browser-type')).to eq 'selection' end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8612352..d8adbaa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,10 @@ require 'rack/test' require 'rspec' require 'database_cleaner' +require 'simplecov' + +SimpleCov.start + require_relative '../app' DatabaseCleaner.strategy = :transaction @@ -15,6 +19,47 @@ module RSpecMixin def app Sinatra::Application end + + def lti_request(path, credential: nil, timestamp: nil, signature: nil) + credential ||= AuthCredential.generate + url = 'http://example.org' + path + params = { + oauth_consumer_key: credential.key, + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: timestamp || Time.current.to_i.to_s, + oauth_nonce: 'XxmJ35LrLDc4vFR1JHHFroTC2ty02DWUZP98mKQ', + oauth_version: '1.0', + context_id: '4dde05e8ca1973bcca9bffc13e1548820eee93a3', + context_label: 'GDrive', + context_title: 'GDrive test', + custom_user_id: 'user-id', + launch_presentation_document_target: 'iframe', + launch_presentation_locale: 'en', + lti_message_type: 'ContentItemSelectionRequest', + lti_version: 'LTI-1p0', + roles: 'Instructor,urn:lti:instrole:ims/lis/Administrator', + tool_consumer_instance_guid: '794d72b707af6ea82cfe3d5d473f16888a8366c7.canvas.docker', + user_id: '535fa085f22b4655f48cd5a36a9215f64c062838' + } + unless signature + authenticator = IMS::LTI::Services::MessageAuthenticator.new(url, params, credential.secret) + signature = authenticator.simple_oauth_header.send(:signature) + end + post path, params.merge(oauth_signature: signature) + end + + def session + last_request.env['rack.session'] + end +end + +RSpec::Matchers.define :have_css do |selector| + match do |resp| + Nokogiri::HTML(resp.body).css(selector).first + end + failure_message do |_resp| + "expected the page to have css '#{selector}'" + end end RSpec.configure do |config|