Outcomes Service content migration service
closes OUT-3655 flag=outcome_alignments_course_migration test plan: - start up canvas, quiz_api and outcomes - for an account, provision outcomes service and sync some course outcomes (see g/235665) - enable the outcome_alignments_course_migration and outcome_alignment_non_scoring_content feature options in the root account of a course - in a course, create a canvas page and align an outcome - export a copy of the course - create a new course - import the course content into the new course - re-sync course outcomes and contexts - re-import the course content into the new course - confirm that the new course contains the canvas page with the outcome aligned to it Change-Id: Ie5cce9e1f8829736c03fd5f6bc2c7a8c000c8333 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/238973 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Pat Renner <prenner@instructure.com> Reviewed-by: Michael Brewer-Davis <mbd@instructure.com> QA-Review: Michael Brewer-Davis <mbd@instructure.com> Product-Review: Michael Brewer-Davis <mbd@instructure.com>
This commit is contained in:
parent
f33b0c2662
commit
aaf457bc94
|
@ -0,0 +1,151 @@
|
||||||
|
#
|
||||||
|
# Copyright (C) 2020 - 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 OutcomesService
|
||||||
|
class MigrationService
|
||||||
|
class << self
|
||||||
|
def applies_to_course?(course)
|
||||||
|
course.root_account.feature_enabled?(:outcome_alignments_course_migration) &&
|
||||||
|
OutcomesService::Service.enabled_in_context?(course)
|
||||||
|
end
|
||||||
|
|
||||||
|
def begin_export(course, opts)
|
||||||
|
artifacts = export_artifacts(course, opts)
|
||||||
|
return nil if artifacts.empty?
|
||||||
|
data = {
|
||||||
|
context_type: 'course',
|
||||||
|
context_id: course.id.to_s,
|
||||||
|
export_settings: {
|
||||||
|
format: 'canvas',
|
||||||
|
artifacts: artifacts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content_exports_url = "#{OutcomesService::Service.url(course)}/api/content_exports"
|
||||||
|
response = CanvasHttp.post(
|
||||||
|
content_exports_url,
|
||||||
|
headers_for(course, 'content_migration.export', context_type: 'course', context_id: course.id.to_s),
|
||||||
|
body: data.to_json,
|
||||||
|
content_type: 'application/json'
|
||||||
|
)
|
||||||
|
if response.code =~ /^2/
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
{ export_id: json['id'], course: course }
|
||||||
|
else
|
||||||
|
raise "Error queueing export for Outcomes Service: #{response.body}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_completed?(export_data)
|
||||||
|
content_export_url = "#{OutcomesService::Service.url(export_data[:course])}/api/content_exports/#{export_data[:export_id]}"
|
||||||
|
response = CanvasHttp.get(
|
||||||
|
content_export_url,
|
||||||
|
headers_for(export_data[:course], 'content_migration.export', id: export_data[:export_id])
|
||||||
|
)
|
||||||
|
if response.code =~ /^2/
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
case json['state']
|
||||||
|
when 'completed'
|
||||||
|
true
|
||||||
|
when 'failed'
|
||||||
|
raise "Content Export for Outcomes Service failed"
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise "Error retrieving export state for Outcomes Service: #{response.body}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def retrieve_export(export_data)
|
||||||
|
content_export_url = "#{OutcomesService::Service.url(export_data[:course])}/api/content_exports/#{export_data[:export_id]}"
|
||||||
|
response = CanvasHttp.get(
|
||||||
|
content_export_url,
|
||||||
|
headers_for(export_data[:course], 'content_migration.export', id: export_data[:export_id])
|
||||||
|
)
|
||||||
|
if response.code =~ /^2/
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
json['data']
|
||||||
|
else
|
||||||
|
raise "Error retrieving export for Outcomes Service: #{response.body}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_imported_content(course, content_migration, imported_content)
|
||||||
|
content_imports_url = "#{OutcomesService::Service.url(course)}/api/content_imports"
|
||||||
|
data = imported_content.merge(
|
||||||
|
context_type: 'course',
|
||||||
|
context_id: course.id.to_s,
|
||||||
|
external_migration_id: content_migration.id
|
||||||
|
)
|
||||||
|
response = CanvasHttp.post(
|
||||||
|
content_imports_url,
|
||||||
|
headers_for(course, 'content_migration.import', context_type: 'course', context_id: course.id.to_s),
|
||||||
|
body: data.to_json,
|
||||||
|
content_type: 'application/json'
|
||||||
|
)
|
||||||
|
if response.code =~ /^2/
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
{ import_id: json['id'], course: course }
|
||||||
|
else
|
||||||
|
raise "Error sending import for Outcomes Service: #{response.body}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_completed?(import_data)
|
||||||
|
content_import_url = "#{OutcomesService::Service.url(import_data[:course])}/api/content_imports/#{import_data[:import_id]}"
|
||||||
|
response = CanvasHttp.get(
|
||||||
|
content_import_url,
|
||||||
|
headers_for(import_data[:course], 'content_migration.import', id: import_data[:import_id])
|
||||||
|
)
|
||||||
|
if response.code =~ /^2/
|
||||||
|
json = JSON.parse(response.body)
|
||||||
|
case json['state']
|
||||||
|
when 'completed'
|
||||||
|
true
|
||||||
|
when 'failed'
|
||||||
|
raise 'Content Import for Outcomes Service failed'
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise "Error retrieving import state for Outcomes Service: #{response.body}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def headers_for(course, scope, overrides = {})
|
||||||
|
{
|
||||||
|
'Authorization' => OutcomesService::Service.jwt(course, scope, overrides: overrides)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_artifacts(course, opts)
|
||||||
|
page_ids = if opts[:selective]
|
||||||
|
opts[:exported_assets].map{|asset| (match = asset.match(/wiki_page_(\d+)/)) && match[1]}.compact
|
||||||
|
else
|
||||||
|
course.wiki_pages.pluck(:id)
|
||||||
|
end
|
||||||
|
return [] unless page_ids.any?
|
||||||
|
[{
|
||||||
|
external_type: 'canvas.page',
|
||||||
|
external_id: page_ids
|
||||||
|
}]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,58 @@
|
||||||
|
#
|
||||||
|
# Copyright (C) 2020 - 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 OutcomesService
|
||||||
|
class Service
|
||||||
|
class << self
|
||||||
|
def url(context)
|
||||||
|
settings = settings(context)
|
||||||
|
protocol = ENV.fetch('OUTCOMES_SERVICE_PROTOCOL', Rails.env.production? ? 'https' : 'http')
|
||||||
|
domain = settings[:domain]
|
||||||
|
"#{protocol}://#{domain}" if domain.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled_in_context?(context)
|
||||||
|
settings = settings(context)
|
||||||
|
settings[:consumer_key].present? && settings[:jwt_secret].present? && settings[:domain].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def jwt(context, scope, expiration = 1.day.from_now.to_i, overrides: {})
|
||||||
|
settings = settings(context)
|
||||||
|
if settings.key?(:consumer_key) && settings.key?(:jwt_secret) && settings.key?(:domain)
|
||||||
|
consumer_key = settings[:consumer_key]
|
||||||
|
jwt_secret = settings[:jwt_secret]
|
||||||
|
domain = settings[:domain]
|
||||||
|
payload = {
|
||||||
|
host: domain,
|
||||||
|
consumer_key: consumer_key,
|
||||||
|
scope: scope,
|
||||||
|
exp: expiration
|
||||||
|
}.merge(overrides)
|
||||||
|
JWT.encode(payload, jwt_secret, 'HS512')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def settings(context)
|
||||||
|
context.root_account.settings.dig(:provision, 'outcomes') || {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -41,3 +41,9 @@ limit_section_visibility_in_lmgb:
|
||||||
display_name: Restricted view for teachers in LMGB
|
display_name: Restricted view for teachers in LMGB
|
||||||
description: Hides certain students and sections in the LMGB if the teacher only
|
description: Hides certain students and sections in the LMGB if the teacher only
|
||||||
has access to a particular section
|
has access to a particular section
|
||||||
|
outcome_alignments_course_migration:
|
||||||
|
state: hidden
|
||||||
|
applies_to: RootAccount
|
||||||
|
display_name: Outcomes Service Alignment Migration
|
||||||
|
description: Includes Outcomes Service alignments when exporting and importing
|
||||||
|
course content
|
||||||
|
|
|
@ -16,4 +16,5 @@
|
||||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Canvas::Migration::ExternalContent::Migrator.register_service('conditional_release', ConditionalRelease::MigrationService)
|
Canvas::Migration::ExternalContent::Migrator.register_service('conditional_release', ConditionalRelease::MigrationService)
|
||||||
|
Canvas::Migration::ExternalContent::Migrator.register_service('outcomes_service', OutcomesService::MigrationService)
|
||||||
Canvas::Migration::ExternalContent::Migrator.register_service('quizzes_next_export', QuizzesNext::ExportService)
|
Canvas::Migration::ExternalContent::Migrator.register_service('quizzes_next_export', QuizzesNext::ExportService)
|
||||||
|
|
|
@ -0,0 +1,277 @@
|
||||||
|
#
|
||||||
|
# Copyright (C) 2020 - 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_relative '../../spec_helper'
|
||||||
|
require_relative '../../sharding_spec_helper'
|
||||||
|
require 'webmock/rspec'
|
||||||
|
|
||||||
|
describe OutcomesService::MigrationService do
|
||||||
|
around(:example) do |example|
|
||||||
|
WebMock.disable_net_connect!(allow_localhost: true)
|
||||||
|
example.run
|
||||||
|
WebMock.enable_net_connect!
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:root_account) { account_model }
|
||||||
|
let(:course) { course_model(root_account: root_account) }
|
||||||
|
|
||||||
|
context 'without settings' do
|
||||||
|
describe '.applies_to_course?' do
|
||||||
|
it 'returns false' do
|
||||||
|
expect(described_class.applies_to_course?(course)).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with settings' do
|
||||||
|
before do
|
||||||
|
root_account.settings[:provision] = { 'outcomes' => {
|
||||||
|
domain: 'canvas.test',
|
||||||
|
consumer_key: 'blah',
|
||||||
|
jwt_secret: 'woo'
|
||||||
|
}}
|
||||||
|
root_account.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag disabled' do
|
||||||
|
before do
|
||||||
|
root_account.disable_feature!(:outcome_alignments_course_migration)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.applies_to_course?' do
|
||||||
|
it 'returns false' do
|
||||||
|
expect(described_class.applies_to_course?(course)).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag enabled' do
|
||||||
|
before do
|
||||||
|
root_account.enable_feature!(:outcome_alignments_course_migration)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.applies_to_course?' do
|
||||||
|
it 'returns true' do
|
||||||
|
expect(described_class.applies_to_course?(course)).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.begin_export' do
|
||||||
|
before do
|
||||||
|
course.wiki_pages.create(
|
||||||
|
title: 'Some random wiki page',
|
||||||
|
body: 'wiki page content'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_post_content_export(external_ids = [course.wiki_pages.first.id])
|
||||||
|
stub_request(:post, 'http://canvas.test/api/content_exports').with({
|
||||||
|
body: {
|
||||||
|
context_type: 'course',
|
||||||
|
context_id: course.id.to_s,
|
||||||
|
export_settings: {
|
||||||
|
format: 'canvas',
|
||||||
|
artifacts: [{
|
||||||
|
external_type: 'canvas.page',
|
||||||
|
external_id: external_ids
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: /.+/
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should request course wiki page' do
|
||||||
|
stub_post_content_export.to_return(status: 200, body: '{"id":123}')
|
||||||
|
expect(described_class.begin_export(course, {})).to eq({
|
||||||
|
export_id: 123,
|
||||||
|
course: course
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should raise error on non 2xx response' do
|
||||||
|
stub_post_content_export.to_return(status: 401, body: '{"valid_jwt":false}')
|
||||||
|
expect { described_class.begin_export(course, {}) }.to raise_error(/Error queueing export for Outcomes Service/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should request selective course wiki page' do
|
||||||
|
stub_post_content_export(['2']).to_return(status: 200, body: '{"id":123}')
|
||||||
|
expect(described_class.begin_export(course, {
|
||||||
|
selective: true,
|
||||||
|
exported_assets: ['wiki_page_2']
|
||||||
|
})).to eq({
|
||||||
|
export_id: 123,
|
||||||
|
course: course
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return no export if no artifacts requested' do
|
||||||
|
expect(described_class.begin_export(course, {
|
||||||
|
selective: true,
|
||||||
|
exported_assets: []
|
||||||
|
})).to eq nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.export_completed?' do
|
||||||
|
let(:export_data) do
|
||||||
|
{
|
||||||
|
course: course,
|
||||||
|
export_id: 1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_get_content_export
|
||||||
|
stub_request(:get, 'http://canvas.test/api/content_exports/1').with({
|
||||||
|
headers: {
|
||||||
|
Authorization: /\+*/
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on completed' do
|
||||||
|
stub_get_content_export.to_return(status: 200, body: '{"state":"completed"}')
|
||||||
|
expect(described_class.export_completed?(export_data)).to eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false on pending' do
|
||||||
|
stub_get_content_export.to_return(status: 200, body: '{"state":"in_progress"}')
|
||||||
|
expect(described_class.export_completed?(export_data)).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error on failed' do
|
||||||
|
stub_get_content_export.to_return(status: 200, body: '{"state":"failed"}')
|
||||||
|
expect { described_class.export_completed?(export_data) }.to raise_error('Content Export for Outcomes Service failed')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error on non 2xx response' do
|
||||||
|
stub_request(:get, 'http://canvas.test/api/content_exports/1').to_return(status: 401, body: '{"valid_jwt":false}')
|
||||||
|
export_data = {
|
||||||
|
course: course,
|
||||||
|
export_id: 1
|
||||||
|
}
|
||||||
|
expect { described_class.export_completed?(export_data) }.to raise_error('Error retrieving export state for Outcomes Service: {"valid_jwt":false}')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.retrieve_export' do
|
||||||
|
let(:export_data) do
|
||||||
|
{
|
||||||
|
course: course,
|
||||||
|
export_id: 1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_get_content_export
|
||||||
|
stub_request(:get, 'http://canvas.test/api/content_exports/1').with({
|
||||||
|
headers: {
|
||||||
|
Authorization: /\+*/
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns export data when complete' do
|
||||||
|
stub_get_content_export.to_return(status: 200, body: '{"data":"stuff"}')
|
||||||
|
expect(described_class.retrieve_export(export_data)).to eq 'stuff'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error on non 2xx response' do
|
||||||
|
stub_get_content_export.to_return(status: 401, body: '{"valid_jwt":false}')
|
||||||
|
expect { described_class.retrieve_export(export_data) }.to raise_error('Error retrieving export for Outcomes Service: {"valid_jwt":false}')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.send_imported_content' do
|
||||||
|
let(:content_migration) { ContentMigration.create!(context: course) }
|
||||||
|
let(:imported_content) do
|
||||||
|
{
|
||||||
|
data: 'stuff'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_post_content_import
|
||||||
|
stub_request(:post, 'http://canvas.test/api/content_imports').with(
|
||||||
|
body: {
|
||||||
|
context_type: 'course',
|
||||||
|
context_id: course.id.to_s,
|
||||||
|
external_migration_id: content_migration.id,
|
||||||
|
data: 'stuff'
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: /\+*/
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns import id on import creation' do
|
||||||
|
stub_post_content_import.to_return(status: 200, body: '{"id":123}')
|
||||||
|
expect(described_class.send_imported_content(course, content_migration, imported_content)). to eq({
|
||||||
|
import_id: 123,
|
||||||
|
course: course
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error on non 2xx response' do
|
||||||
|
stub_post_content_import.to_return(status: 401, body: '{"valid_jwt":false}')
|
||||||
|
expect { described_class.send_imported_content(course, content_migration, imported_content) }.to raise_error(
|
||||||
|
'Error sending import for Outcomes Service: {"valid_jwt":false}'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.import_completed?' do
|
||||||
|
let(:import_data) do
|
||||||
|
{
|
||||||
|
course: course,
|
||||||
|
import_id: 1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_get_content_import
|
||||||
|
stub_request(:get, 'http://canvas.test/api/content_imports/1').with({
|
||||||
|
headers: {
|
||||||
|
Authorization: /\+*/
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true on completed' do
|
||||||
|
stub_get_content_import.to_return(status: 200, body: '{"state":"completed"}')
|
||||||
|
expect(described_class.import_completed?(import_data)).to eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false on pending' do
|
||||||
|
stub_get_content_import.to_return(status: 200, body: '{"state":"in_progress"}')
|
||||||
|
expect(described_class.import_completed?(import_data)).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error on failed' do
|
||||||
|
stub_get_content_import.to_return(status: 200, body: '{"state":"failed"}')
|
||||||
|
expect { described_class.import_completed?(import_data) }.to raise_error('Content Import for Outcomes Service failed')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises error on non 2xx response' do
|
||||||
|
stub_get_content_import.to_return(status: 401, body: '{"valid_jwt":false}')
|
||||||
|
expect { described_class.import_completed?(import_data) }.to raise_error('Error retrieving import state for Outcomes Service: {"valid_jwt":false}')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,87 @@
|
||||||
|
#
|
||||||
|
# Copyright (C) 2020 - 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_relative '../../spec_helper'
|
||||||
|
require_relative '../../sharding_spec_helper'
|
||||||
|
|
||||||
|
describe OutcomesService::Service do
|
||||||
|
Service = OutcomesService::Service
|
||||||
|
|
||||||
|
let(:root_account) { account_model }
|
||||||
|
let(:course) { course_model(root_account: root_account) }
|
||||||
|
|
||||||
|
context 'without settings' do
|
||||||
|
describe '.url' do
|
||||||
|
it 'returns nil url' do
|
||||||
|
expect(Service.url(course)).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.enabled_in_context?' do
|
||||||
|
it 'returns not enabled' do
|
||||||
|
expect(Service.enabled_in_context?(course)).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.jwt' do
|
||||||
|
it 'returns nil jwt' do
|
||||||
|
expect(Service.jwt(course, 'outcomes.show')).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with settings' do
|
||||||
|
before do
|
||||||
|
root_account.settings[:provision] = { 'outcomes' => {
|
||||||
|
domain: 'canvas.test',
|
||||||
|
consumer_key: 'blah',
|
||||||
|
jwt_secret: 'woo'
|
||||||
|
}}
|
||||||
|
root_account.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.url' do
|
||||||
|
it 'returns url' do
|
||||||
|
expect(Service.url(course)).to eq 'http://canvas.test'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.enabled_in_context?' do
|
||||||
|
it 'returns enabled' do
|
||||||
|
expect(Service.enabled_in_context?(course)).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.jwt' do
|
||||||
|
it 'returns valid jwt' do
|
||||||
|
expect(Service.jwt(course, 'outcomes.show')).not_to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes overrides' do
|
||||||
|
token = Service.jwt(course, 'outcomes.list', overrides: { context_uuid: 'xyz' })
|
||||||
|
decoded = JWT.decode(token, 'woo', true, algorithm: 'HS512')
|
||||||
|
expect(decoded[0]).to include(
|
||||||
|
'host' => 'canvas.test',
|
||||||
|
'consumer_key' => 'blah',
|
||||||
|
'scope' => 'outcomes.list',
|
||||||
|
'context_uuid' => 'xyz'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue