Render simplified launch params into NRPS v2 responses

- Output a smaller version of a LTI 1.3 launch payload for
  each NRPS v2 member when the NRPS request includes a `rlid`
  query parameter.
  - Most claims and custom params from the launch payload are
  excluded either because:
    - They describe the context and would thus be redundant, or
    - They pose performance problems (N+1 queries, usually), or
    - They are absent from the spec example, e.g.
    `https://purl.imsglobal.org/spec/lti/claim/version`, or
    - They require additional development and thus need to be
    handled as a separate task.
  - See `memberships_provider.rb` ~line 68 for list of
  custom params supported in this commit. (More coming later.)
  - Vast majority of the patch has to do with tests against
  `JwtMessage`, which was modified to allow claims to be turned
  on/off via a new white/blacklist mechanism in `AppUtil`.
  - Custom param white/blacklisting is handled directly in
  `VariableExpander` to satisfy the LTI rule that unsupported
  params should just be echoed as-is. This (instead of keeping
  all the white/blacklist support in `JwtMessage` ensures
  consistent behavior w/r/t `VariableExpander`'s more sophisticated
  features, specifically its suport for expanding variables embedded
  into larger strings.

Closes LTIA-40

Test Plan

  - Configure a LTI 1.3/Advantage tool with the supported set of
  custom params listed in `memberships_provider.rb` starting ~line 68.
  If using the POST
  `/api/lti/accounts/:account_id/developer_keys/tool_configuration`
  API, this is done by setting
  `tool_configuration.settings.custom_fields` to a JSON object where
  keys are the param name to be rendered into LTI payloads and values
  are the $-prefixed custom param names themselves. Include several
  nonsense entries as well as unsupported entries e.g.:

  ```
  // ... snip ...
  "tool_configuration": {
    "settings": {
      // ... snip ...
      "custom_fields": {
        "person_name_full": "$Person.name.full",
        "person_name_display": "$Person.name.display",
        "person_name_family": "$Person.name.family",
        "person_name_given": "$Person.name.given",
        "canvas_user_isrootaccountadmin": "$Canvas.user.isRootAccountAdmin"
        "unsupported_param_1": "$unsupported.param.1",
        "unsupported_param_2": "$unsupported.param.2"
      }
      // ... snip ...
    }
    // ... snip ...
  }
  // ... snip ...
  ```
  - Place this tool into a course, ensure the course has several active
  members.
  - Launch the tool in order to observe the course context's LTI
  identifier. Use that identifier as the value of the NRPS `rlid`
  parameter, e.g. a GET to:
  `/api/lti/courses/1/names_and_roles?rlid=4dde05e8ca1973bcca9bffc13e1548820eee93a3`
  - Each `members` array element in the response should have a
  `message` array with a single element being the simplified
  representation of a LTI 1.3 launch payload, were that user to launch
  the context referenced by `rlid`.
  - The `message` entry should have two top level claims:
    - `"https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiResourceLinkRequest"`
    - `"https://purl.imsglobal.org/spec/lti/claim/custom": <object>`
  - The `custom` claim should include an entry for each `custom_fields`
  key/value pair configured above, with supported entries being
  correctly expanded and nonsense and unsupported entries being echoed
  as-is.
  - Repeat for a group context in the same course (still using the
  course's LTI ID as the `rlid` value). Results should be the same,
  though scoped to group membership.

Change-Id: If2591c62c494756d65774e3115abeca19935c988
Reviewed-on: https://gerrit.instructure.com/169090
Tested-by: Jenkins
Product-Review: Karl Lloyd <karl@instructure.com>
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Reviewed-by: Marc Phillips <mphillips@instructure.com>
QA-Review: Bill Smith <bsmith@instructure.com>
This commit is contained in:
Dan McCallum 2018-10-18 16:47:37 -07:00 committed by Marc Phillips
parent 2f2ca82c36
commit 75fdd13da6
15 changed files with 1165 additions and 297 deletions

View File

@ -107,6 +107,10 @@ module Lti::Ims::Providers
@user_factory = user_factory
end
def unwrap
enrollments
end
def user
@_user ||= @user_factory.user(enrollments.first.user)
end
@ -125,6 +129,10 @@ module Lti::Ims::Providers
end
class CourseContextDecorator < SimpleDelegator
def unwrap
__getobj__
end
def context_label
course_code
end

View File

@ -77,6 +77,10 @@ module Lti::Ims::Providers
@user_factory = user_factory
end
def unwrap
__getobj__
end
def context
@_context ||= GroupContextDecorator.new(super)
end
@ -105,6 +109,10 @@ module Lti::Ims::Providers
end
class GroupContextDecorator < SimpleDelegator
def unwrap
__getobj__
end
def context_label
nil
end

View File

@ -37,7 +37,15 @@ module Lti::Ims::Providers
{
memberships: memberships,
context: context,
api_metadata: api_metadata
assignment: assignment,
api_metadata: api_metadata,
controller: controller,
tool: tool,
opts: {
rlid: rlid,
role: role,
limit: limit
}.compact
}
end
@ -45,15 +53,32 @@ module Lti::Ims::Providers
UserDecorator.new(
user,
tool,
Lti::VariableExpander.new(
course.root_account,
course,
controller,
{
current_user: user,
tool: tool
}
)
user_variable_expander(user)
)
end
def user_variable_expander(user)
Lti::VariableExpander.new(
course.root_account,
course,
controller,
{
current_user: user,
tool: tool,
variable_whitelist: %w(
Person.name.full
Person.name.display
Person.name.family
Person.name.given
User.image
User.id
Canvas.user.id
vnd.instructure.User.uuid
Canvas.user.globalId
Canvas.user.sisSourceId
Person.sourcedId
)
}
)
end
@ -179,6 +204,10 @@ module Lti::Ims::Providers
enrollments
end
def limit
controller.params[:limit].to_i
end
def paginate(scope)
Api.jsonapi_paginate(scope, controller, base_url, pagination_args)
end
@ -186,7 +215,7 @@ module Lti::Ims::Providers
def pagination_args
# Treat LTI's `limit` param as override of std `per_page` API pagination param. Is no LTI override for `page`.
pagination_args = {}
if (limit = controller.params[:limit].to_i) > 0
if limit > 0
pagination_args[:per_page] = [limit, Api.max_per_page].min
# Ensure page size reset isn't accidentally clobbered by other pagination API params
clear_request_param :per_page
@ -207,7 +236,7 @@ module Lti::Ims::Providers
# has to change if more custom param support is added in the future. But that day may never come, so err on the
# side of making it very hard to leak user attributes.
class UserDecorator
attr_reader :user # Intentional backdoor. See Lti::Ims::NamesAndRolesSerializer for use case/s.
attr_reader :user, :expander, :tool
def initialize(user, tool, expander)
@user = user
@ -215,6 +244,10 @@ module Lti::Ims::Providers
@expander = expander
end
def unwrap
user
end
def id
user.id
end

View File

@ -33,7 +33,7 @@ module Lti::Ims
def serialize_context
{
id: lti_id_for(page[:context]),
id: lti_id(page[:context]),
label: page[:context].context_label,
title: page[:context].context_title,
}.compact
@ -46,6 +46,10 @@ module Lti::Ims
def serialize_membership(enrollment)
# Inbound model is either an ActiveRecord Enrollment or GroupMembership, with delegations in place
# to make them behave more or less the same for our purposes
member(enrollment).merge!(message(enrollment)).compact
end
def member(enrollment)
{
status: 'Active',
name: enrollment.user.name,
@ -56,13 +60,37 @@ module Lti::Ims
lis_person_sourcedid: enrollment.user.sourced_id,
# enrollment.user often wrapped for privacy policy reasons, but calculating the LTI ID really needs
# access to underlying AR model.
user_id: lti_id_for(enrollment.user.respond_to?(:user) ? enrollment.user.user : enrollment.user),
user_id: lti_id(enrollment.user),
roles: enrollment.lti_roles
}.compact
}
end
def lti_id_for(entity)
Lti::Asset.opaque_identifier_for(entity)
def message(enrollment)
return {} if page[:opts].blank? || page[:opts][:rlid].blank?
launch = Lti::Messages::ResourceLinkRequest.new(
tool: page[:tool],
context: unwrap(page[:context]),
user: enrollment.user,
expander: enrollment.user.expander,
return_url: nil,
opts: {
# See Lti::Ims::Providers::MembershipsProvider for additional constraints on custom param expansion
# already baked into `enrollment.user.expander`
claim_group_whitelist: [ :custom_params ]
}
).generate_post_payload_message
# One straggler field we can't readily control via white/blacklists
launch_hash = launch.to_h.except!("#{LtiAdvantage::Serializers::JwtMessageSerializer::IMS_CLAIM_PREFIX}version")
{ message: [ launch_hash ] }
end
def lti_id(entity)
Lti::Asset.opaque_identifier_for(unwrap(entity))
end
def unwrap(wrapped)
wrapped&.respond_to?(:unwrap) ? wrapped.unwrap : wrapped
end
attr_reader :page

View File

@ -25,6 +25,7 @@ module Lti
'default' => {template: 'lti/framed_launch'}.freeze,
'full_width_in_context' => {template: 'lti/full_width_in_context'}.freeze,
}.freeze
BLACKLIST_WILDCARD = '*'.freeze # to set up 'deny all' rules
def self.display_template(display_type=nil, display_override:nil)
unless TOOL_DISPLAY_TEMPLATES.key?(display_type)
@ -48,5 +49,11 @@ module Lti
end
end
def self.allowed?(candidate, whitelist, blacklist)
return true if whitelist.blank? && blacklist.blank?
return false if blacklist.present? && (blacklist.include?(candidate) || blacklist.include?(BLACKLIST_WILDCARD))
whitelist.blank? || whitelist.include?(candidate)
end
end
end

View File

@ -31,17 +31,26 @@ module Lti::Messages
@message = LtiAdvantage::Messages::JwtMessage.new
end
def generate_post_payload
add_security_claims!
add_public_claims! if @tool.public?
add_include_email_claims! if @tool.include_email?
add_include_name_claims! if @tool.include_name?
add_resource_claims!
add_private_claims!
def generate_post_payload_message
add_security_claims! if include_claims?(:security)
add_public_claims! if @tool.public? && include_claims?(:public)
add_include_email_claims! if @tool.include_email? && include_claims?(:email)
add_include_name_claims! if @tool.include_name? && include_claims?(:name)
add_resource_claims! if include_claims?(:resource)
add_context_claims! if include_claims?(:context)
add_tool_platform_claims! if include_claims?(:tool_platform)
add_launch_presentation_claims! if include_claims?(:launch_presentation)
add_i18n_claims! if include_claims?(:i18n)
add_roles_claims! if include_claims?(:roles)
add_custom_params_claims! if include_claims?(:custom_params)
add_names_and_roles_service_claims! if include_names_and_roles_service_claims?
@expander.expand_variables!(@message.extensions)
{ id_token: @message.to_jws(Lti::KeyStorage.present_key) }
@message
end
def generate_post_payload
{ id_token: generate_post_payload_message.to_jws(Lti::KeyStorage.present_key) }
end
private
@ -56,27 +65,44 @@ module Lti::Messages
@message.sub = Lti::Asset.opaque_identifier_for(@user)
end
def add_private_claims!
@message.locale = I18n.locale || I18n.default_locale.to_s
@message.roles = expand_variable('$com.Instructure.membership.roles').split ','
def add_context_claims!
@message.context.id = Lti::Asset.opaque_identifier_for(@context)
@message.context.label = @context.course_code if @context.respond_to?(:course_code)
@message.context.title = @context.name
@message.context.type = [Lti::SubstitutionsHelper::LIS_V2_ROLE_MAP[@context.class] || @context.class.to_s]
end
def add_tool_platform_claims!
@message.tool_platform.guid = @context.root_account.lti_guid
@message.tool_platform.name = @context.root_account.name
@message.tool_platform.version = 'cloud'
@message.tool_platform.product_family_code = 'canvas'
end
def add_launch_presentation_claims!
@message.launch_presentation.document_target = 'iframe'
@message.launch_presentation.height = @tool.extension_setting(@opts[:resource_type], :selection_height)
@message.launch_presentation.width = @tool.extension_setting(@opts[:resource_type], :selection_width)
@message.launch_presentation.return_url = @return_url
@message.launch_presentation.locale = I18n.locale || I18n.default_locale.to_s
@message.custom = custom_parameters
end
def add_i18n_claims!
# Repeated as @message.launch_presentation.locale above. Separated b/c often want one or the other but not both,
# e.g. NRPS v2 only wants this one and none of the launch_presention fields.
@message.locale = I18n.locale || I18n.default_locale.to_s
end
def add_roles_claims!
@message.roles = expand_variable('$com.Instructure.membership.roles').split ','
add_extension('roles', '$Canvas.xuser.allRoles')
add_extension('canvas_enrollment_state', '$Canvas.enrollment.enrollmentState')
end
def add_custom_params_claims!
@message.custom = custom_parameters
end
def add_include_name_claims!
@message.name = @user.name
@message.given_name = @user.first_name
@ -131,7 +157,9 @@ module Lti::Messages
end
def include_names_and_roles_service_claims?
(@context.is_a?(Course) || @context.is_a?(Group)) && @tool.lti_1_3_enabled?
include_claims?(:names_and_roles_service) &&
(@context.is_a?(Course) || @context.is_a?(Group)) &&
@tool.lti_1_3_enabled?
end
def add_names_and_roles_service_claims!
@ -163,9 +191,18 @@ module Lti::Messages
@expander.expand_variables!(custom_params_hash)
end
def include_claims?(claim_group)
Lti::AppUtil.allowed?(claim_group, @opts[:claim_group_whitelist], @opts[:claim_group_blacklist])
end
def include_extension?(extension_name)
Lti::AppUtil.allowed?(extension_name, @opts[:extension_whitelist], @opts[:extension_blacklist])
end
protected
def add_extension(key, value)
return unless include_extension?(key.to_sym)
@message.extensions["#{JwtMessage::EXTENSION_PREFIX}#{key}"] = value
end
end

View File

@ -22,8 +22,8 @@ module Lti::Messages
@message = LtiAdvantage::Messages::ResourceLinkRequest.new
end
def generate_post_payload
add_resource_link_request_claims!
def generate_post_payload_message
add_resource_link_request_claims! if include_claims?(:rlid)
super
end
@ -60,4 +60,4 @@ module Lti::Messages
add_extension('canvas_assignment_points_possible', '$Canvas.assignment.pointsPossible')
end
end
end
end

View File

@ -29,11 +29,16 @@ module Lti
attr_accessor :current_pseudonym, :content_tag, :assignment,
:tool_setting_link_id, :tool_setting_binding_id, :tool_setting_proxy_id, :tool, :attachment,
:collaboration
:collaboration, :variable_whitelist, :variable_blacklist
def self.register_expansion(name, permission_groups, expansion_proc, *guards)
@expansions ||= {}
@expansions["$#{name}".to_sym] = VariableExpansion.new(name, permission_groups, expansion_proc, *guards)
@expansions["$#{name}".to_sym] = VariableExpansion.new(
name,
permission_groups,
expansion_proc,
*([-> { Lti::AppUtil.allowed?(name, @variable_whitelist, @variable_blacklist) }] + guards)
)
end
def self.expansions

View File

@ -159,20 +159,83 @@ describe Lti::Ims::NamesAndRolesController do
end
end
# rubocop:disable RSpec/LetSetup
shared_examples 'rlid check' do
let(:rlid_param) { raise 'Override in example' }
let(:rlid_param) { expected_lti_id(course) }
let(:params_overrides) { super().merge(rlid: rlid_param) }
let(:user_full_name) { 'Marta Perkins' }
context 'when the rlid param specifies the course context LTI ID' do
let(:rlid_param) { expected_lti_id(course) }
it 'behaves just like a \'normal\' NRPS course membership lookup' do
send_request
expect_single_member(enrollment)
end
end
# Always an error at this writing b/c we don't yet support rlid=assignment.lti_context_id
context 'and the tool is configured with a mix of supported and unsupported custom parameters' do
let!(:tool) do
tool = super()
tool.settings[:custom_fields] = {
person_name_full: '$Person.name.full',
person_name_display: '$Person.name.display',
person_name_family: '$Person.name.family',
person_name_given: '$Person.name.given',
user_image: '$User.image',
user_id: '$User.id',
canvas_user_id: '$Canvas.user.id',
vns_instructure_user_uuid: '$vnd.instructure.User.uuid',
canvas_user_globalid: '$Canvas.user.globalId',
canvas_user_sissourceid: '$Canvas.user.sisSourceId',
person_sourced_id: '$Person.sourcedId',
unsupported_param_1: '$unsupported.param.1',
unsupported_param_2: '$unsupported.param.2'
}
tool.save!
tool
end
let!(:user) do
user = enrollment.user
user.email = 'marta.perkins@school.edu'
user.avatar_image_url = 'http://school.edu/image/url.png'
user.save!
user.pseudonyms.create!({
account: course.account,
unique_id: 'user1@example.com',
password: 'asdfasdf',
password_confirmation: 'asdfasdf',
workflow_state: 'active',
sis_user_id: 'user-1-sis-user-id-1'
})
user
end
it 'expands the supported parameters and echoes the rest' do
send_request
expect(json[:members][0]).to match_enrollment_for_rlid(
{
'https://purl.imsglobal.org/spec/lti/claim/custom' => {
'person_name_full' => 'Marta Perkins',
'person_name_display' => 'Marta Perkins',
'person_name_family' => 'Perkins',
'person_name_given' => 'Marta',
'user_image' => 'http://school.edu/image/url.png',
'user_id' => user.id,
'canvas_user_id' => user.id,
'vns_instructure_user_uuid' => user.uuid,
'canvas_user_globalid' => user.global_id,
'canvas_user_sissourceid' => 'user-1-sis-user-id-1',
'person_sourced_id' => 'user-1-sis-user-id-1',
'unsupported_param_1' => '$unsupported.param.1',
'unsupported_param_2' => '$unsupported.param.2'
}
},
enrollment
)
expect_member_count(1)
end
end
end
# rubocop:enable RSpec/LetSetup
context 'when the rlid param does not specify the course context LTI ID' do
let(:rlid_param) { "nonsense-#{expected_lti_id(course)}" }
@ -332,15 +395,13 @@ describe Lti::Ims::NamesAndRolesController do
end
end
# rubocop:disable RSpec/LetSetup
context 'when the rlid param is specified' do
# rubocop:disable RSpec/LetSetup
# rubocop:disable RSpec/EmptyLineAfterFinalLet
let!(:enrollment) { teacher_in_course(course: course, active_all: true) }
# rubocop:enable RSpec/EmptyLineAfterFinalLet
# rubocop:enable RSpec/LetSetup
let!(:enrollment) { teacher_in_course(course: course, active_all: true, name: user_full_name) }
it_behaves_like 'rlid check'
end
# rubocop:enable RSpec/LetSetup
context 'when a course has multiple enrollments' do
let!(:teacher_enrollment) { teacher_in_course(course: course, active_all: true) }
@ -782,17 +843,15 @@ describe Lti::Ims::NamesAndRolesController do
end
end
# rubocop:disable RSpec/LetSetup
context 'when the rlid param is specified' do
let(:group_record) { group_with_user(active_all: true, context: course).group }
let(:group_record) { group_with_user(active_all: true, context: course, name: user_full_name).group }
let(:group_member) { group_record.group_memberships.first }
# rubocop:disable RSpec/LetSetup
# rubocop:disable RSpec/EmptyLineAfterFinalLet
let!(:enrollment) { group_member }
# rubocop:enable RSpec/EmptyLineAfterFinalLet
# rubocop:enable RSpec/LetSetup
it_behaves_like 'rlid check'
end
# rubocop:enable RSpec/LetSetup
context 'when a group has multiple memberships' do
let!(:group_leadership) do
@ -1156,7 +1215,18 @@ describe Lti::Ims::NamesAndRolesController do
end
def match_enrollment(*enrollment)
enrollment.first.is_a?(Enrollment) ? be_lti_course_membership(*enrollment) : be_lti_group_membership(*enrollment)
if self.respond_to?(:rlid_param) && rlid_param.present?
match_enrollment_for_rlid({}, *enrollment)
else
enrollment.first.is_a?(Enrollment) ? be_lti_course_membership(*enrollment) : be_lti_group_membership(*enrollment)
end
end
def match_enrollment_for_rlid(message_matcher, *enrollment)
if enrollment.first.is_a?(Enrollment)
be_lti_course_membership_for_rlid(message_matcher, *enrollment)
else be_lti_group_membership_for_rlid(message_matcher, *enrollment)
end
end
def expect_enrollment_response_page
@ -1230,6 +1300,6 @@ describe Lti::Ims::NamesAndRolesController do
end
def users_from(*users_or_user_containers)
users_or_user_containers.map { |uoe| uoe.respond_to?(:user) ? uoe.user : uoe }
users_or_user_containers.map { |uc| uc.respond_to?(:user) ? uc.user : uc }
end
end

View File

@ -67,4 +67,50 @@ describe Lti::AppUtil do
expect(Lti::AppUtil.display_template("default", display_override: "full_width")).to eq(Lti::AppUtil::TOOL_DISPLAY_TEMPLATES["full_width"])
end
end
describe '.allowed?' do
it 'allows candidate if white- and blacklists are nil' do
expect(Lti::AppUtil).to be_allowed('foo', nil, nil)
end
it 'allows candidate if white- and blacklists are empty' do
expect(Lti::AppUtil).to be_allowed('foo', [], [])
end
it 'allows candidate if present in whitelist and not in blacklist' do
expect(Lti::AppUtil).to be_allowed('foo', ['foo'], ['bar'])
end
it 'disallows candidate if present in blacklist and not in whitelist' do
expect(Lti::AppUtil).to_not be_allowed('foo', ['bar'], ['foo'])
end
it 'disallows candidate if present in white- and blacklist' do
expect(Lti::AppUtil).to_not be_allowed('foo', ['foo'], ['foo'])
end
it 'disallows candidate if whitelist empty and blacklist wildcarded' do
expect(Lti::AppUtil).to_not be_allowed('foo', [], ['*'])
end
it 'disallows candidate if whitelist empty and is present blacklist' do
expect(Lti::AppUtil).to_not be_allowed('foo', [], ['foo'])
end
it 'disallows candidate if absent from both white- and blacklists' do
expect(Lti::AppUtil).to_not be_allowed('foo', ['bar'], ['baz'])
end
it 'disallows candidate if absent from whitelist and blacklist is empty' do
expect(Lti::AppUtil).to_not be_allowed('foo', ['bar'], [])
end
it 'allows candidate if present in multi-valued whitelist and not present in multi-valued blacklist' do
expect(Lti::AppUtil).to be_allowed('foo', ['bar', 'foo', 'baz'], ['bap','bam','ban'])
end
it 'disallows candidate if present in multi-valued blacklist and not present in multi-valued whitelist' do
expect(Lti::AppUtil).to_not be_allowed('foo', ['bap','bam','ban'], ['bar', 'foo', 'baz'])
end
end
end

File diff suppressed because it is too large Load Diff

View File

@ -122,6 +122,14 @@ describe Lti::Messages::ResourceLinkRequest do
it 'sets the resource link id' do
expect(jws.dig('https://purl.imsglobal.org/spec/lti/claim/resource_link', 'id')).to eq course.lti_context_id
end
context 'when rlid claim group disabled' do
let(:opts) { super().merge({claim_group_blacklist: [:rlid]}) }
it 'does not set the resource link id' do
expect(jws).not_to include('https://purl.imsglobal.org/spec/lti/claim/resource_link')
end
end
end
describe '#generate_post_payload_for_assignment' do

View File

@ -144,6 +144,44 @@ module Lti
expect(expanded[:some_name]).to eq "my variable is buried in here ${tests_expan} can you find it?"
end
it 'echoes registered variable if blacklisted' do
VariableExpander.register_expansion('test_expan', ['a'], -> { @context })
VariableExpander.register_expansion('variable1', ['a'], -> { 1 })
variable_expander.variable_blacklist = ['test_expan']
expanded1 = variable_expander.expand_variables!({some_name: '$test_expan'})
expanded2 = variable_expander.expand_variables!({some_name: '$variable1'})
expect(expanded1.count).to eq 1
expect(expanded1[:some_name]).to eq '$test_expan'
expect(expanded2.count).to eq 1
expect(expanded2[:some_name]).to eq 1
end
it 'echoes substring variable if blacklisted' do
allow(account).to receive(:id).and_return(42)
VariableExpander.register_expansion('test_expan', ['a'], -> { @context.id })
VariableExpander.register_expansion('variable1', ['a'], -> { 1 })
variable_expander.variable_blacklist = ['test_expan']
expanded1 = variable_expander.expand_variables!({some_name: 'my variable is buried in here ${test_expan} can you find it?'})
expanded2 = variable_expander.expand_variables!({some_name: 'my variable is buried in here ${variable1} can you find it?'})
expect(expanded1.count).to eq 1
expect(expanded1[:some_name]).to eq 'my variable is buried in here $test_expan can you find it?'
expect(expanded2.count).to eq 1
expect(expanded2[:some_name]).to eq 'my variable is buried in here 1 can you find it?'
end
it 'echoes multiple substring variables if blacklisted' do
allow(account).to receive(:id).and_return(42)
VariableExpander.register_expansion('test_expan', ['a'], -> { @context.id })
VariableExpander.register_expansion('variable1', ['a'], -> { 1 })
VariableExpander.register_expansion('other_variable', ['a'], -> { 2 })
variable_expander.variable_blacklist = ['test_expan','variable1']
expanded = variable_expander.expand_variables!(
{some_name: 'my variables ${variable1} is buried ${other_variable} in here ${test_expan} can you find them?'}
)
expect(expanded.count).to eq 1
expect(expanded[:some_name]).to eq 'my variables $variable1 is buried 2 in here $test_expan can you find them?'
end
describe '#self.expansion_keys' do
let(:expected_keys) do
VariableExpander.expansions.keys.map { |c| c.to_s[1..-1] }

View File

@ -38,6 +38,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
workflow_state: privacy_level
)
end
let(:message_matcher) { {} }
def serialize
subject.as_json.with_indifferent_access
@ -48,7 +49,12 @@ describe Lti::Ims::NamesAndRolesSerializer do
be_lti_group_membership_context(decorated_group)
end
def be_lti_membership
def be_lti_membership(for_rlid=false)
if for_rlid
return be_lti_course_membership_for_rlid(message_matcher, decorated_enrollment) if context_type == :course
return be_lti_group_membership_for_rlid(message_matcher, decorated_group_member)
end
return be_lti_course_membership(decorated_enrollment) if context_type == :course
be_lti_group_membership(decorated_group_member)
end
@ -64,15 +70,44 @@ describe Lti::Ims::NamesAndRolesSerializer do
})
end
shared_examples 'enrollment serialization' do
shared_examples 'enrollment serialization' do |for_rlid=false|
it 'properly formats NRPS json' do
json = serialize
expect(json[:id]).to eq url
expect(json[:context]).to be_lti_membership_context
expect(json[:members][0]).to be_lti_membership
expect(json[:members][0]).to be_lti_membership(for_rlid)
end
end
shared_examples 'serializes message array if rlid param present' do
let(:tool) do
tool = super()
tool.settings[:custom_fields] = {
user_id: '$User.id',
canvas_user_id: '$Canvas.user.id',
unsupported_param_1: '$unsupported.param.1',
unsupported_param_2: '$unsupported.param.2'
}
tool.save!
tool
end
let(:page) do
super().merge(opts: { rlid: 'rlid-value' })
end
let(:message_matcher) do
{
'https://purl.imsglobal.org/spec/lti/claim/custom' => {
'user_id' => user.id,
'canvas_user_id' => user.id,
'unsupported_param_1' => '$unsupported.param.1',
'unsupported_param_2' => '$unsupported.param.2'
}
}
end
it_behaves_like 'enrollment serialization', true
end
# Technically all these '...privacy policy' examples are redundant w/r/t be_lti_*_membership(). But those matchers
# know nothing about privacy policies... they just know that if a model provides a value, it should appear in the
# resulting json/hash. So you could have an incorrectly implemented privacy policy, but all the serialization tests
@ -128,6 +163,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
create_pseudonym!(user)
enrollment
end
let(:user) { enrollment.user }
let(:decorated_user_factory) do
Lti::Ims::Providers::CourseMembershipsProvider.new(course, nil, tool)
end
@ -141,9 +177,14 @@ describe Lti::Ims::NamesAndRolesSerializer do
let(:decorated_course) { Lti::Ims::Providers::CourseMembershipsProvider::CourseContextDecorator.new(course) }
let(:page) do
{
memberships: [decorated_enrollment],
url: url,
context: decorated_course
memberships: [decorated_enrollment],
context: decorated_course,
assignment: nil,
api_metadata: nil,
controller: nil,
tool: tool,
opts: {}
}
end
@ -155,7 +196,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
context 'and an anonymous tool' do
let(:privacy_level) { 'anonymous' }
it_behaves_like 'enrollment serialization'
it_behaves_like 'enrollment serialization'
it_behaves_like 'anonymous privacy policy'
end
@ -172,6 +213,8 @@ describe Lti::Ims::NamesAndRolesSerializer do
it_behaves_like 'enrollment serialization'
it_behaves_like 'email_only privacy policy'
end
it_behaves_like 'serializes message array if rlid param present'
end
context 'with a group' do
@ -186,6 +229,7 @@ describe Lti::Ims::NamesAndRolesSerializer do
create_pseudonym!(user)
enrollment
end
let(:user) { group_member.user }
let(:decorated_user_factory) do
Lti::Ims::Providers::GroupMembershipsProvider.new(group_record, nil, tool)
end
@ -199,9 +243,14 @@ describe Lti::Ims::NamesAndRolesSerializer do
let(:decorated_group) { Lti::Ims::Providers::GroupMembershipsProvider::GroupContextDecorator.new(group_record) }
let(:page) do
{
memberships: [decorated_group_member],
url: url,
context: decorated_group
memberships: [decorated_group_member],
context: decorated_group,
assignment: nil,
api_metadata: nil,
controller: nil,
tool: tool,
opts: {}
}
end
@ -230,6 +279,8 @@ describe Lti::Ims::NamesAndRolesSerializer do
it_behaves_like 'enrollment serialization'
it_behaves_like 'email_only privacy policy'
end
it_behaves_like 'serializes message array if rlid param present'
end
end
end

View File

@ -96,16 +96,37 @@ module Lti::Ims::NamesAndRolesMatchers
}.compact
end
def expected_message_array(message_matcher)
[
{
'https://purl.imsglobal.org/spec/lti/claim/message_type' => 'LtiResourceLinkRequest',
'https://purl.imsglobal.org/spec/lti/claim/custom' => {}
}.merge!(message_matcher.presence || {})
]
end
def expected_course_membership(user, *enrollments)
expected_base_membership(user).merge!('roles' => match_array(expected_course_lti_roles(*enrollments))).compact
end
def expected_course_membership_for_rlid(message_matcher, user, *enrollments)
expected_course_membership(user, *enrollments).
merge('message' => match_array(expected_message_array(message_matcher))).
compact
end
def expected_group_membership(user, membership)
expected_base_membership(user).merge!('roles' => match_array(expected_group_lti_roles(membership))).compact
end
def expected_group_membership_for_rlid(message_matcher, user, membership)
expected_group_membership(user, membership).
merge('message' => match_array(expected_message_array(message_matcher))).
compact
end
def unwrap_user(user)
user.respond_to?(:user) ? user.user : user
user.respond_to?(:unwrap) ? user.unwrap : user
end
RSpec::Matchers.define :be_lti_course_membership_context do |expected|
@ -144,6 +165,18 @@ module Lti::Ims::NamesAndRolesMatchers
attr_reader :actual, :expected
end
RSpec::Matchers.define :be_lti_course_membership_for_rlid do |message_matcher, *expected|
match do |actual|
@expected = expected_course_membership_for_rlid(message_matcher, expected.first.user, *expected)
values_match? @expected, actual
end
diffable
# Make sure a failure diffs the two JSON structs (w/o this will compare 'actual' JSON to 'expected' AR model)
attr_reader :actual, :expected
end
RSpec::Matchers.define :be_lti_group_membership do |expected|
match do |actual|
@expected = expected_group_membership(expected.user, expected)
@ -156,6 +189,18 @@ module Lti::Ims::NamesAndRolesMatchers
attr_reader :actual, :expected
end
RSpec::Matchers.define :be_lti_group_membership_for_rlid do |message_matcher, expected|
match do |actual|
@expected = expected_group_membership_for_rlid(message_matcher, expected.user, expected)
values_match? @expected, actual
end
diffable
# Make sure a failure diffs the two JSON structs (w/o this will compare 'actual' JSON to 'expected' AR model)
attr_reader :actual, :expected
end
RSpec::Matchers.define :be_lti_advantage_error_response_body do |expected_type, expected_message|
match do |actual|
@expected = {