Add ability to import tool profiles

fixes PLAT-2598

Test plan:
* Install the plagiarism detection tool in a course locally using docker
* Get a zip file from me with a bunch of exports
* In the course that has the plagiarism detection tool installed
  * Import 'import.imscc'
  * Ensure that the import completes without warnings or errors
  * Import 'different_version.imscc'
  * Ensure that the import completes with a warning about finding a
    different version of the tool
  * Import 'missing_data.imscc'
  * Ensure that the import completes with a warning that has a link to
    an error report
* In a course where the plagiarism detection tool is not installed
  * Import 'registration_url.imscc'
  * Ensure that the import completes with a warning that tells the user
    the registration url they can use to install the missing tool
  * Import 'import.imscc'
  * Ensure that the import completes with a warning that tells the user
    they need to install the tool but without a registration url

Change-Id: I3836c2caf602487de135b5a8732bba00b96b9342
Reviewed-on: https://gerrit.instructure.com/114139
Tested-by: Jenkins
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
Reviewed-by: Brad Humphrey <brad@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Karl Lloyd <karl@instructure.com>
This commit is contained in:
Andrew Butterfield 2017-06-02 15:38:02 -07:00
parent b308b24a29
commit 9f028d3d6d
21 changed files with 903 additions and 1 deletions

View File

@ -133,6 +133,7 @@ module Importers
Importers::ExternalFeedImporter.process_migration(data, migration); migration.update_import_progress(56) Importers::ExternalFeedImporter.process_migration(data, migration); migration.update_import_progress(56)
Importers::GradingStandardImporter.process_migration(data, migration); migration.update_import_progress(58) Importers::GradingStandardImporter.process_migration(data, migration); migration.update_import_progress(58)
Importers::ContextExternalToolImporter.process_migration(data, migration); migration.update_import_progress(60) Importers::ContextExternalToolImporter.process_migration(data, migration); migration.update_import_progress(60)
Importers::ToolProfileImporter.process_migration(data, migration); migration.update_import_progress(61)
Importers::QuizImporter.process_migration(data, migration, question_data); migration.update_import_progress(65) Importers::QuizImporter.process_migration(data, migration, question_data); migration.update_import_progress(65)
Importers::DiscussionTopicImporter.process_migration(data, migration); migration.update_import_progress(70) Importers::DiscussionTopicImporter.process_migration(data, migration); migration.update_import_progress(70)
Importers::WikiPageImporter.process_migration(data, migration); migration.update_import_progress(75) Importers::WikiPageImporter.process_migration(data, migration); migration.update_import_progress(75)

View File

@ -0,0 +1,70 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module Importers
class MissingRequiredToolProfileValuesError < StandardError; end
class ToolProfileImporter
class << self
def process_migration(data, migration)
tool_profiles = data['tool_profiles'] || []
tool_profiles.each do |tool_profile|
begin
values = tease_out_required_values!(tool_profile)
tool_proxies = Lti::ToolProxy.find_active_proxies_for_context_by_vendor_code_and_product_code(
context: migration.context,
vendor_code: values[:vendor_code],
product_code: values[:product_code]
)
if tool_proxies.empty?
if values[:registration_url].blank?
migration.add_warning(I18n.t("We were unable to find a tool profile match for \"%{product_name}\".", product_name: values[:product_name]))
else
migration.add_warning(I18n.t("We were unable to find a tool profile match for \"%{product_name}\". If you would like to use this tool please install it using the following registration url: %{registration_url}", product_name: values[:product_name], registration_url: values[:registration_url]))
end
elsif tool_proxies.none? { |tool_proxy| tool_proxy.matching_tool_profile?(tool_profile['tool_profile']) }
migration.add_warning(I18n.t("We found a different version of \"%{product_name}\" installed for your course. If this tool fails to work as intended, try reregistering or reinstalling it.", product_name: values[:product_name]))
end
rescue MissingRequiredToolProfileValuesError => e
migration.add_import_warning('tool_profile', tool_profile['resource_href'], e)
end
end
end
private
def tease_out_required_values!(tool_profile)
values = {
vendor_code: tool_profile.dig('tool_profile', 'product_instance', 'product_info', 'product_family', 'vendor', 'code'),
product_code: tool_profile.dig('tool_profile', 'product_instance', 'product_info', 'product_family', 'code'),
registration_url: tool_profile.dig('meta', 'registration_url'),
product_name: tool_profile.dig('tool_profile', 'product_instance', 'product_info', 'product_name', 'default_value'),
}
missing_keys = values.select { |_, v| v.nil? }.keys
if missing_keys.present?
fail MissingRequiredToolProfileValuesError, I18n.t("Missing required values: %{missing_values}", missing_values: missing_keys.join(','))
else
values
end
end
end
end
end

View File

@ -38,6 +38,12 @@ module Lti
self.class.find_active_proxies_for_context(context).include?(self) self.class.find_active_proxies_for_context(context).include?(self)
end end
def self.find_active_proxies_for_context_by_vendor_code_and_product_code(context:, vendor_code:, product_code:)
find_active_proxies_for_context(context)
.eager_load(:product_family)
.where('lti_product_families.vendor_code = ? AND lti_product_families.product_code = ?', vendor_code, product_code)
end
def self.find_active_proxies_for_context(context) def self.find_active_proxies_for_context(context)
find_all_proxies_for_context(context).where('lti_tool_proxies.workflow_state = ?', 'active') find_all_proxies_for_context(context).where('lti_tool_proxies.workflow_state = ?', 'active')
end end
@ -80,5 +86,23 @@ module Lti
ims_tool_proxy.enabled_capabilities ims_tool_proxy.enabled_capabilities
end end
def matching_tool_profile?(other_profile)
profile = raw_data['tool_profile']
return false if profile.dig('product_instance', 'product_info', 'product_family', 'vendor', 'code') !=
other_profile.dig('product_instance', 'product_info', 'product_family', 'vendor', 'code')
return false if profile.dig('product_instance', 'product_info', 'product_family', 'code') !=
other_profile.dig('product_instance', 'product_info', 'product_family', 'code')
resource_handlers = profile['resource_handler']
other_resource_handlers = other_profile['resource_handler']
rh_names = resource_handlers.map { |rh| rh.dig('resource_type', 'code') }
other_rh_names = other_resource_handlers.map { |rh| rh.dig('resource_type', 'code') }
return false if rh_names.sort != other_rh_names.sort
true
end
end end
end end

View File

@ -22,6 +22,7 @@ module CC::Importer::Canvas
include WikiConverter include WikiConverter
include AssignmentConverter include AssignmentConverter
include TopicConverter include TopicConverter
include ToolProfileConverter
include WebcontentConverter include WebcontentConverter
include QuizConverter include QuizConverter
include MediaTrackConverter include MediaTrackConverter
@ -58,6 +59,8 @@ module CC::Importer::Canvas
res = lti.get_blti_resources(@manifest) res = lti.get_blti_resources(@manifest)
@course[:external_tools] = lti.convert_blti_links(res, self) @course[:external_tools] = lti.convert_blti_links(res, self)
set_progress(50) set_progress(50)
@course[:tool_profiles] = convert_tool_profiles
set_progress(52)
@course[:file_map] = create_file_map @course[:file_map] = create_file_map
set_progress(60) set_progress(60)
@course[:all_files_zip] = package_course_files @course[:all_files_zip] = package_course_files

View File

@ -0,0 +1,37 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module CC::Importer::Canvas
module ToolProfileConverter
include CC::Importer
def convert_tool_profiles
tool_profiles = []
@manifest.css('resource[type=tool_profile]').each do |res|
file = res.at_css('file')
next unless file
file_path = File.join @unzipped_file_path, file['href']
json = JSON.parse(File.read(file_path))
json['resource_href'] = file['href']
tool_profiles << json
end
tool_profiles
end
end
end

View File

@ -0,0 +1,65 @@
{
"tool_profiles": [
{
"meta": {
"registration_url": "https://www.samplelaunch.com/register"
},
"resource_href": "href",
"tool_profile": {
"lti_version": "LTI-2p0",
"product_instance": {
"guid": "be42ae52-23fe-48f5-a783-40ecc7ef6d5c",
"product_info": {
"product_version": "1.0",
"product_family": {
"code": "abc",
"vendor": {
"code": "123",
"vendor_name": {
"default_value": "acme"
},
"description": {
"default_value": "example vendor"
}
}
},
"description": {
"default_value": "example product"
},
"product_name": {
"default_value": "learn abc's"
}
}
},
"base_url_choice": [
{
"default_base_url": "https://www.samplelaunch.com",
"selector": {
"applies_to": [
"MessageHandler"
]
}
}
],
"resource_handler": [
{
"resource_type": {
"code": "code"
},
"resource_name": {
"default_value": "resource name",
"key": ""
},
"message": [
{
"message_type": "message_type",
"path": "https://www.samplelaunch.com/blti"
}
]
}
],
"service_offered": []
}
}
]
}

View File

@ -0,0 +1,65 @@
{
"tool_profiles": [
{
"meta": {
"registration_url": "https://www.samplelaunch.com/register"
},
"resource_href": "href",
"tool_profile": {
"lti_version": "LTI-2p0",
"product_instance": {
"guid": "be42ae52-23fe-48f5-a783-40ecc7ef6d5c",
"product_info": {
"product_version": "1.0",
"product_family": {
"code": "abc",
"vendor": {
"code": "123",
"vendor_name": {
"default_value": "acme"
},
"description": {
"default_value": "example vendor"
}
}
},
"description": {
"default_value": "example product"
},
"product_name": {
"default_value": "learn abc's"
}
}
},
"base_url_choice": [
{
"default_base_url": "https://www.samplelaunch.com",
"selector": {
"applies_to": [
"MessageHandler"
]
}
}
],
"resource_handler": [
{
"resource_type": {
"code": "different_code"
},
"resource_name": {
"default_value": "resource name",
"key": ""
},
"message": [
{
"message_type": "message_type",
"path": "https://www.samplelaunch.com/blti"
}
]
}
],
"service_offered": []
}
}
]
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<assignmentGroups xmlns="http://canvas.instructure.com/xsd/cccv1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://canvas.instructure.com/xsd/cccv1p0 http://canvas.instructure.com/xsd/cccv1p0.xsd">
<assignmentGroup identifier="i9b9077e9f08ef701b2b956092c195f8b">
<title>Assignments</title>
<position>1</position>
<group_weight>0.0</group_weight>
</assignmentGroup>
</assignmentGroups>

View File

@ -0,0 +1,2 @@
Q: What did the panda say when he was forced out of his natural habitat?
A: This is un-BEAR-able

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<course identifier="i15448fbdf54a2fc2013e32f908227288" xmlns="http://canvas.instructure.com/xsd/cccv1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://canvas.instructure.com/xsd/cccv1p0 http://canvas.instructure.com/xsd/cccv1p0.xsd">
<title>Import/Export Test</title>
<course_code>Import/Export</course_code>
<start_at>2017-06-12T22:35:22</start_at>
<is_public>true</is_public>
<allow_student_wiki_edits>false</allow_student_wiki_edits>
<allow_student_forum_attachments>false</allow_student_forum_attachments>
<lock_all_announcements>false</lock_all_announcements>
<allow_student_organized_groups>true</allow_student_organized_groups>
<default_view>feed</default_view>
<license>public_domain</license>
<lock_all_announcements>false</lock_all_announcements>
<grading_standard_enabled>false</grading_standard_enabled>
<storage_quota>524288000</storage_quota>
<root_account_uuid>pd3ANgn1DnNxn04Gqwi2FGBdRNiULEbEC8L1ltGc</root_account_uuid>
</course>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<fileMeta xmlns="http://canvas.instructure.com/xsd/cccv1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://canvas.instructure.com/xsd/cccv1p0 http://canvas.instructure.com/xsd/cccv1p0.xsd">
</fileMeta>

View File

@ -0,0 +1 @@
{"tool_profile":{"lti_version":"LTI-2p0","product_instance":{"guid":"be42ae52-23fe-48f5-a783-40ecc7ef6d5c","product_info":{"product_version":"1.0","product_family":{"code":"similarity detection reference tool","vendor":{"code":"Instructure.com","vendor_name":{"default_value":"Instructure"},"description":{"default_value":"Canvas Learning Management System"}}},"description":{"default_value":"LTI 2.1 tool provider reference implementation"},"product_name":{"default_value":"similarity detection reference tool"}}},"base_url_choice":[{"default_base_url":"http://originality.docker","selector":{"applies_to":["MessageHandler"]}}],"resource_handler":[{"resource_type":{"code":"sumbissionsz"},"resource_name":{"default_value":"Similarity Detection Tool","key":""},"message":[{"message_type":"basic-lti-launch-request","path":"/submission/index","enabled_capability":["Canvas.placements.accountNavigation","Canvas.placements.courseNavigation"]}]},{"resource_type":{"code":"placements"},"resource_name":{"default_value":"Similarity Detection Tool","key":""},"message":[{"message_type":"basic-lti-launch-request","path":"/assignments/configure","enabled_capability":["Canvas.placements.similarityDetection"]}]}],"service_offered":[{"endpoint":"http://originality.docker/event/submission","action":["POST"],"@id":"http://originality.docker/lti/v2/services#vnd.Canvas.SubmissionEvent","@type":"RestService"}]},"meta":{"registration_url":"https://register.me/register"}}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<manifest identifier="ie6041ef31d35509f367c4b8464cc131e" xmlns="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1" xmlns:lom="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource" xmlns:lomimscc="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1 http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imscp_v1p2_v1p0.xsd http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd">
<metadata>
<schema>IMS Common Cartridge</schema>
<schemaversion>1.1.0</schemaversion>
<lomimscc:lom>
<lomimscc:general>
<lomimscc:title>
<lomimscc:string>Import/Export Test</lomimscc:string>
</lomimscc:title>
</lomimscc:general>
<lomimscc:lifeCycle>
<lomimscc:contribute>
<lomimscc:date>
<lomimscc:dateTime>2017-06-12</lomimscc:dateTime>
</lomimscc:date>
</lomimscc:contribute>
</lomimscc:lifeCycle>
<lomimscc:rights>
<lomimscc:copyrightAndOtherRestrictions>
<lomimscc:value>yes</lomimscc:value>
</lomimscc:copyrightAndOtherRestrictions>
<lomimscc:description>
<lomimscc:string>Public Domain - http://en.wikipedia.org/wiki/Public_domain</lomimscc:string>
</lomimscc:description>
</lomimscc:rights>
</lomimscc:lom>
</metadata>
<organizations>
<organization identifier="org_1" structure="rooted-hierarchy">
<item identifier="LearningModules">
</item>
</organization>
</organizations>
<resources>
<resource identifier="i15448fbdf54a2fc2013e32f908227288" type="associatedcontent/imscc_xmlv1p1/learning-application-resource" href="course_settings/canvas_export.txt">
<file href="course_settings/course_settings.xml"/>
<file href="course_settings/assignment_groups.xml"/>
<file href="course_settings/files_meta.xml"/>
<file href="course_settings/media_tracks.xml"/>
<file href="course_settings/canvas_export.txt"/>
</resource>
<resource identifier="i964fd8107ac2c2e75e9a142971693976" type="tool_profile">
<file href="i964fd8107ac2c2e75e9a142971693976.json"/>
</resource>
</resources>
</manifest>

View File

@ -67,3 +67,9 @@ def get_import_context(system=nil)
context context
end end
class ImportHelper
def self.get_import_data_xml(sub_folder, file_name)
File.open(File.join(IMPORT_JSON_DIR, sub_folder, "#{file_name}.xml")) { |f| Nokogiri::XML(f) }
end
end

View File

@ -0,0 +1,132 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require File.expand_path(File.dirname(__FILE__) + '/../../../../import_helper')
describe CC::Importer::Canvas::ToolProfileConverter do
let(:manifest) { ImportHelper.get_import_data_xml('unzipped', 'imsmanifest') }
let(:path) { File.expand_path(File.dirname(__FILE__) + '/../../../../fixtures/importer/unzipped') }
let(:converter) do
Class.new do
include CC::Importer::Canvas::ToolProfileConverter
def initialize(manifest, path)
@manifest = manifest
@unzipped_file_path = path
end
end.new(manifest, path)
end
describe '#convert_tool_profiles' do
it 'unpacks tool profiles in the common cartridge' do
tool_profiles = converter.convert_tool_profiles
expect(tool_profiles.size).to eq(1)
expect(tool_profiles.first).to eq({
"tool_profile" => {
"lti_version" => "LTI-2p0",
"product_instance" => {
"guid" => "be42ae52-23fe-48f5-a783-40ecc7ef6d5c",
"product_info" => {
"product_version" => "1.0",
"product_family" => {
"code" => "similarity detection reference tool",
"vendor" => {
"code" => "Instructure.com",
"vendor_name" => {
"default_value" => "Instructure"
},
"description" => {
"default_value" => "Canvas Learning Management System"
}
}
},
"description" => {
"default_value" => "LTI 2.1 tool provider reference implementation"
},
"product_name" => {
"default_value" => "similarity detection reference tool"
}
}
},
"base_url_choice" => [
{
"default_base_url" => "http://originality.docker",
"selector" => {
"applies_to" => [
"MessageHandler"
]
}
}
],
"resource_handler" => [
{
"resource_type" => {
"code" => "sumbissionsz"
},
"resource_name" => {
"default_value" => "Similarity Detection Tool",
"key" => ""
},
"message" => [
{
"message_type" => "basic-lti-launch-request",
"path" => "/submission/index",
"enabled_capability" => [
"Canvas.placements.accountNavigation",
"Canvas.placements.courseNavigation"
]
}
]
},
{
"resource_type" => {
"code" => "placements"
},
"resource_name" => {
"default_value" => "Similarity Detection Tool",
"key" => ""
},
"message" => [
{
"message_type" => "basic-lti-launch-request",
"path" => "/assignments/configure",
"enabled_capability" => [
"Canvas.placements.similarityDetection"
]
}
]
}
],
"service_offered" => [
{
"endpoint" => "http://originality.docker/event/submission",
"action" => [
"POST"
],
"@id" => "http://originality.docker/lti/v2/services#vnd.Canvas.SubmissionEvent",
"@type" => "RestService"
}
]
},
"meta" => {
"registration_url" => "https://register.me/register"
},
"resource_href" => "i964fd8107ac2c2e75e9a142971693976.json"
})
end
end
end

View File

@ -0,0 +1,125 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require File.expand_path(File.dirname(__FILE__) + '/spec_helper.rb')
RSpec.shared_context "lti2_course_spec_helper", :shared_context => :metadata do
let(:account) { Account.create! }
let(:course) { Course.create!(account: account) }
let(:developer_key) {DeveloperKey.create!(redirect_uri: 'http://www.example.com/redirect')}
let(:product_family) do
Lti::ProductFamily.create!(
vendor_code: '123',
product_code: 'abc',
vendor_name: 'acme',
root_account: account,
developer_key: developer_key
)
end
let(:tool_proxy) do
tp = Lti::ToolProxy.create!(
context: course,
guid: SecureRandom.uuid,
shared_secret: 'abc',
product_family: product_family,
product_version: '1',
workflow_state: 'active',
raw_data: {
'enabled_capability' => ['Security.splitSecret'],
'tool_profile' => {
'lti_version' => 'LTI-2p0',
'product_instance' => {
'guid' => 'be42ae52-23fe-48f5-a783-40ecc7ef6d5c',
'product_info' => {
'product_version' => '1.0',
'product_family' => {
'code' => 'abc',
'vendor' => {
'code' => '123',
'vendor_name' => {
'default_value' => 'acme'
},
'description' => {
'default_value' => 'example vendor'
}
}
},
'description' => {
'default_value' => 'example product'
},
'product_name' => {
'default_value' => "learn abc's"
}
}
},
'base_url_choice' => [
{
'default_base_url' => 'https://www.samplelaunch.com',
'selector' => {
'applies_to' => [
'MessageHandler'
]
}
}
],
'resource_handler' => [
{
'resource_type' => {
'code' => 'code'
},
'resource_name' => {
'default_value' => 'resource name',
'key' => ''
},
'message' => [
{
'message_type' => 'message_type',
'path' => 'https://www.samplelaunch.com/blti'
}
]
}
],
'service_offered' => []
}
},
lti_version: '1'
)
Lti::ToolProxyBinding.where(context_id: account, context_type: account.class.to_s,
tool_proxy_id: tp).first_or_create!
tp
end
let(:resource_handler) do
Lti::ResourceHandler.create!(
resource_type_code: 'code',
name: 'resource name',
tool_proxy: tool_proxy
)
end
let(:message_handler) do
Lti::MessageHandler.create!(
message_type: 'message_type',
launch_path: 'https://www.samplelaunch.com/blti',
resource_handler: resource_handler,
tool_proxy: tool_proxy
)
end
let(:tool_proxy_binding) do
Lti::ToolProxyBinding.where(context_id: account, context_type: account.class.to_s,
tool_proxy_id: tool_proxy).first_or_create!
end
end

View File

@ -39,7 +39,64 @@ RSpec.shared_context "lti2_spec_helper", :shared_context => :metadata do
product_family: product_family, product_family: product_family,
product_version: '1', product_version: '1',
workflow_state: 'active', workflow_state: 'active',
raw_data: {'enabled_capability' => ['Security.splitSecret']}, raw_data: {
'enabled_capability' => ['Security.splitSecret'],
'tool_profile' => {
'lti_version' => 'LTI-2p0',
'product_instance' => {
'guid' => 'be42ae52-23fe-48f5-a783-40ecc7ef6d5c',
'product_info' => {
'product_version' => '1.0',
'product_family' => {
'code' => 'abc',
'vendor' => {
'code' => '123',
'vendor_name' => {
'default_value' => 'acme'
},
'description' => {
'default_value' => 'example vendor'
}
}
},
'description' => {
'default_value' => 'example product'
},
'product_name' => {
'default_value' => "learn abc's"
}
}
},
'base_url_choice' => [
{
'default_base_url' => 'https://www.samplelaunch.com',
'selector' => {
'applies_to' => [
'MessageHandler'
]
}
}
],
'resource_handler' => [
{
'resource_type' => {
'code' => 'code'
},
'resource_name' => {
'default_value' => 'resource name',
'key' => ''
},
'message' => [
{
'message_type' => 'message_type',
'path' => 'https://www.samplelaunch.com/blti'
}
]
},
],
'service_offered' => []
}
},
lti_version: '1' lti_version: '1'
) )
Lti::ToolProxyBinding.where(context_id: account, context_type: account.class.to_s, Lti::ToolProxyBinding.where(context_id: account, context_type: account.class.to_s,

View File

@ -60,6 +60,9 @@ describe Course do
}}.with_indifferent_access }}.with_indifferent_access
migration.migration_ids_to_import = params migration.migration_ids_to_import = params
# tool profile tests
Importers::ToolProfileImporter.expects(:process_migration)
Importers::CourseContentImporter.import_content(@course, data, params, migration) Importers::CourseContentImporter.import_content(@course, data, params, migration)
@course.reload @course.reload

View File

@ -0,0 +1,86 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '../../../import_helper')
require File.expand_path(File.dirname(__FILE__) + '../../../lti2_course_spec_helper')
describe Importers::ToolProfileImporter do
describe '#process_migration' do
context 'no tool profiles' do
let(:data) { {} }
let(:migration) { double }
it 'does nothing' do
expect { Importers::ToolProfileImporter.process_migration(data, migration) }.not_to raise_error
end
end
context 'malformed tool profile' do
let(:data) { { "tool_profiles" => [{ 'resource_href' => 'href' }] } }
let(:migration) { double }
it 'adds an import warning' do
expect(migration).to receive(:add_import_warning).with('tool_profile', 'href', instance_of(Importers::MissingRequiredToolProfileValuesError))
Importers::ToolProfileImporter.process_migration(data, migration)
end
end
context 'with tool profile and no tool proxies' do
let(:data) { get_import_data('', 'matching_tool_profiles') }
let(:context) { get_import_context }
let(:migration) { context.content_migrations.create! }
it 'adds a warning to the migration' do
expect(migration).to receive(:add_warning).with("We were unable to find a tool profile match for \"learn abc's\". If you would like to use this tool please install it using the following registration url: https://www.samplelaunch.com/register")
Importers::ToolProfileImporter.process_migration(data, migration)
end
it 'adds a warning to the migration without registration url' do
data['tool_profiles'].first['meta']['registration_url'] = ''
expect(migration).to receive(:add_warning).with("We were unable to find a tool profile match for \"learn abc's\".")
Importers::ToolProfileImporter.process_migration(data, migration)
end
end
context 'with tool profile and different version tool proxies' do
include_context 'lti2_course_spec_helper'
let(:data) { get_import_data('', 'nonmatching_tool_profiles') }
let(:migration) { double(context: course) }
it 'does nothing' do
tool_proxy # necessary to instantiate tool_proxy
expect(migration).to receive(:add_warning).with("We found a different version of \"learn abc's\" installed for your course. If this tool fails to work as intended, try reregistering or reinstalling it.")
Importers::ToolProfileImporter.process_migration(data, migration)
end
end
context 'with tool profile and matching tool proxies' do
include_context 'lti2_course_spec_helper'
let(:data) { get_import_data('', 'matching_tool_profiles') }
let(:migration) { double(context: course) }
it 'does nothing' do
tool_proxy # necessary to instantiate tool_proxy
expect { Importers::ToolProfileImporter.process_migration(data, migration) }.not_to raise_error
end
end
end
end

View File

@ -17,6 +17,7 @@
# #
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
require File.expand_path(File.dirname(__FILE__) + '/../../lti2_spec_helper.rb')
require_dependency "lti/tool_proxy" require_dependency "lti/tool_proxy"
module Lti module Lti
@ -173,6 +174,37 @@ module Lti
end end
end end
describe "#find_active_proxies_for_context_by_vendor_code_and_product_code" do
it "doesn't return tool_proxies that are disabled" do
tool_proxy = create_tool_proxy(context: sub_account_2_1, workflow_state: 'disabled')
tool_proxy.bindings.create!(context: sub_account_2_1)
proxies = described_class.find_active_proxies_for_context_by_vendor_code_and_product_code(context: sub_account_2_1, vendor_code: '123', product_code: 'abc')
expect(proxies.count).to eq 0
end
it "doesn't return tool_proxies that don't have a matching vendor_code" do
tool_proxy = create_tool_proxy(context: sub_account_2_1)
tool_proxy.bindings.create!(context: sub_account_2_1)
proxies = described_class.find_active_proxies_for_context_by_vendor_code_and_product_code(context: sub_account_2_1, vendor_code: '1234', product_code: 'abc')
expect(proxies.count).to eq 0
end
it "doesn't return tool_proxies that don't have a matching product_code" do
tool_proxy = create_tool_proxy(context: sub_account_2_1)
tool_proxy.bindings.create!(context: sub_account_2_1)
proxies = described_class.find_active_proxies_for_context_by_vendor_code_and_product_code(context: sub_account_2_1, vendor_code: '123', product_code: 'abcd')
expect(proxies.count).to eq 0
end
it "returns tool proxies that match" do
tool_proxy = create_tool_proxy(context: sub_account_2_1)
tool_proxy.bindings.create!(context: sub_account_2_1)
proxies = described_class.find_active_proxies_for_context_by_vendor_code_and_product_code(context: sub_account_2_1, vendor_code: '123', product_code: 'abc')
expect(proxies.count).to eq 1
end
end
describe "#find_active_proxies_for_context" do describe "#find_active_proxies_for_context" do
it "doesn't return tool_proxies that are disabled" do it "doesn't return tool_proxies that are disabled" do
tool_proxy = create_tool_proxy(context: sub_account_2_1, workflow_state: 'disabled') tool_proxy = create_tool_proxy(context: sub_account_2_1, workflow_state: 'disabled')
@ -282,5 +314,123 @@ module Lti
end end
end end
describe "#matching_tool_profile?" do
include_context 'lti2_spec_helper'
it 'returns true when there is a match' do
expect(tool_proxy.matching_tool_profile?({
"product_instance" => {
"product_info" => {
"product_family" => {
"vendor" => {
"code" => "123"
},
"code" => "abc"
},
}
},
"resource_handler" => [
{
"resource_type" => {
"code" => "code"
}
}
]
})).to eq(true)
end
it "returns false when the vendor_code doesn't match" do
expect(tool_proxy.matching_tool_profile?({
"product_instance" => {
"product_info" => {
"product_family" => {
"vendor" => {
"code" => "1234"
},
"code" => "abc"
},
}
},
"resource_handler" => [
{
"resource_type" => {
"code" => "code"
}
}
]
})).to eq(false)
end
it "returns false when the product_code doesn't match" do
expect(tool_proxy.matching_tool_profile?({
"product_instance" => {
"product_info" => {
"product_family" => {
"vendor" => {
"code" => "123"
},
"code" => "abcd"
},
}
},
"resource_handler" => [
{
"resource_type" => {
"code" => "code"
}
}
]
})).to eq(false)
end
it "returns false when the resource type codes do not match" do
expect(tool_proxy.matching_tool_profile?({
"product_instance" => {
"product_info" => {
"product_family" => {
"vendor" => {
"code" => "123"
},
"code" => "abc"
},
}
},
"resource_handler" => [
{
"resource_type" => {
"code" => "different_code"
}
}
]
})).to eq(false)
end
it "returns false when the resource handlers differ in number" do
expect(tool_proxy.matching_tool_profile?({
"product_instance" => {
"product_info" => {
"product_family" => {
"vendor" => {
"code" => "123"
},
"code" => "abc"
},
}
},
"resource_handler" => [
{
"resource_type" => {
"code" => "different_code"
}
},
{
"resource_type" => {
"code" => "code"
}
}
]
})).to eq(false)
end
end
end end
end end