Skip to content

Commit

Permalink
Collaborations: support deep linking response
Browse files Browse the repository at this point in the history
Closes INTEROP-6430
flag=none

Support for the deep linking request was added
for the collaborations placement in 930f6ed.

This change is the second half and allows LTI
1.3 tools to return content items at that
placement.

Test Plan:
1. Install an LTI 1.3 tool that handles deep
   linking requests
2. Install an LTI 1.1 tool that supports
   content item requests (the LTI 1.1
   example tool on GitHub works here).
3. Navigate to the collaborations page
   of a course.
4. Create a new collaboration using the LTI
   1.3 tool. Verify the following during
   creation:
    - Specifying a "Message" in the deep
      linking response shows that message
      in a flash message
    - Specifying an "Error Message" in the
      deep linking response shows that
      error message in a flash message
    - Upon collaboration creation, Canvas
      launches the tool to the URL given in
      the deep linking response in a new
      tab.
5. Refresh the collaborations page and click
   on the new collaboration. Verify the tool
   launches to the correct URL in a new tab.
6. Create a new collaboration using the LTI
   1.x tool. Verify the same items listed
   in step 4.
7. Verify collaborations can be deleted via
   the "delete" icon.

Change-Id: I756b94410b1d8c527e698debd84138008feb8937
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/256594
Tested-by: Service Cloud Jenkins <[email protected]>
Reviewed-by: Wagner Goncalves <[email protected]>
QA-Review: Wagner Goncalves <[email protected]>
Product-Review: Karl Lloyd <[email protected]>
  • Loading branch information
westonkd committed Jan 20, 2021
1 parent b76829d commit d720d0d
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 74 deletions.
29 changes: 29 additions & 0 deletions app/jsx/deep_linking/DeepLinking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2021 - 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/>.
*/

const deepLinkingResponseMessageType = 'LtiDeepLinkingResponse'

// Checks to see if a postMessage event is valid for
// deep linking content item processing
export function isValidDeepLinkingEvent(event, env) {
return !!(
event.origin === env.DEEP_LINKING_POST_MESSAGE_ORIGIN &&
event.data &&
event.data.messageType === deepLinkingResponseMessageType
)
}
68 changes: 68 additions & 0 deletions app/jsx/deep_linking/__tests__/DeepLinking.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2021 - 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/>.
*/

import {isValidDeepLinkingEvent} from '../DeepLinking'

describe('isValidDeepLinkingEvent', () => {
let data, event, env, parameters

beforeEach(() => {
event = {data: {messageType: 'LtiDeepLinkingResponse'}, origin: 'canvas.instructure.com'}
env = {DEEP_LINKING_POST_MESSAGE_ORIGIN: 'canvas.instructure.com'}
parameters = [event, env]
})

const subject = () => isValidDeepLinkingEvent(...parameters)

it('return true', () => {
expect(subject()).toEqual(true)
})

describe('when the message origin is incorrect', () => {
beforeEach(() => {
event = {data, origin: 'wrong.origin.com'}
parameters = [event, env]
})

it('return false', () => {
expect(subject()).toEqual(false)
})
})

describe('when the event data is not present', () => {
beforeEach(() => {
event = {origin: 'canvas.instructure.com'}
parameters = [event, env]
})

it('return false', () => {
expect(subject()).toEqual(false)
})
})

describe('when the messageType is incorrect', () => {
beforeEach(() => {
event = {data: {messageType: 'WrongMessageType'}, origin: 'canvas.instructure.com'}
parameters = [event, env]
})

it('return false', () => {
expect(subject()).toEqual(false)
})
})
})
192 changes: 192 additions & 0 deletions app/jsx/deep_linking/__tests__/collaborations.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2021 - 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/>.
*/

import $ from 'jquery'
import {
addDeepLinkingListener,
handleDeepLinking,
collaborationUrl,
onExternalContentReady
} from '../collaborations'

describe('addDeepLinkingListener', () => {
let addEventListener
const subject = () => {
addDeepLinkingListener()
}

beforeAll(() => {
addEventListener = global.addEventListener
global.addEventListener = jest.fn()
})

afterAll(() => {
global.addEventListener = addEventListener
})

beforeEach(() => {
global.addEventListener.mockClear()
})

it('adds the message handler to the window', () => {
subject()
expect(global.addEventListener).toHaveBeenCalledWith('message', handleDeepLinking)
})
})

describe('handleDeepLinking', () => {
const content_items = [
{
type: 'link',
title: 'title',
url: 'http://www.tool.com'
}
]

const event = overrides => ({
origin: 'http://www.test.com',
data: {messageType: 'LtiDeepLinkingResponse', content_items},
...overrides
})

let ajaxJSON, flashError, env

beforeAll(() => {
env = window.ENV
flashError = $.flashError
ajaxJSON = $.ajaxJSON

window.ENV = {
DEEP_LINKING_POST_MESSAGE_ORIGIN: 'http://www.test.com'
}

$.flashError = jest.fn()
$.ajaxJSON = jest.fn().mockImplementation(() => ({}))
})

afterAll(() => {
window.ENV = env
$.flashError = flashError
$.ajaxJSON = ajaxJSON
})

beforeEach(() => {
$.ajaxJSON.mockClear()
$.flashError.mockClear()
})

it('creates the collaboration', async () => {
await handleDeepLinking(event())
expect($.ajaxJSON).toHaveBeenCalledWith(
undefined,
'POST',
{contentItems: JSON.stringify(content_items)},
expect.anything(),
expect.anything()
)
})

describe('when the event is invalid', () => {
const overrides = {
origin: 'http://bad.origin.com'
}

it('does not attempt to create a collaboration', async () => {
await handleDeepLinking(event(overrides))
expect($.ajaxJSON).not.toHaveBeenCalled()
})
})

describe('when there is a unhandled error parsing the content item', () => {
const overrides = {
data: {messageType: 'LtiDeepLinkingResponse', content_items: 1}
}

it('does not attempt to create a collaboration', async () => {
await handleDeepLinking(event(overrides))
expect($.ajaxJSON).not.toHaveBeenCalled()
})

it('shows an error message to the user', async () => {
await handleDeepLinking(event(overrides))
expect($.flashError).toHaveBeenCalled()
})
})
})

describe('collaborationUrl', () => {
it('returns a collaboration url', () => {
expect(collaborationUrl(1)).toEqual(`${window.location.toString()}/1`)
})
})

describe('onExternalContentReady', () => {
const params = overrides => [
{},
{
contentItems: {},
...overrides
}
]
let querySelector, ajaxJSON

beforeAll(() => {
querySelector = global.document.querySelector
ajaxJSON = $.ajaxJSON

global.document.querySelector = jest.fn().mockImplementation(() => ({
href: 'http://www.test.com/update',
getAttribute: () => 'http://www.test.com/create'
}))
$.ajaxJSON = jest.fn().mockImplementation(() => ({}))
})

afterAll(() => {
global.document.querySelector = querySelector
$.ajaxJSON = ajaxJSON
})

beforeEach(() => {
global.document.querySelector.mockClear()
$.ajaxJSON.mockClear()
})

it('creates a new collaboration', () => {
onExternalContentReady(...params())
expect($.ajaxJSON).toHaveBeenCalledWith(
'http://www.test.com/create',
'POST',
expect.anything(),
expect.anything(),
expect.anything()
)
})

describe('with a service id', () => {
it('updates the existing collaboration', () => {
onExternalContentReady(...params({service_id: 1}))
expect($.ajaxJSON).toHaveBeenCalledWith(
'http://www.test.com/update',
'PUT',
expect.anything(),
expect.anything(),
expect.anything()
)
})
})
})
96 changes: 96 additions & 0 deletions app/jsx/deep_linking/collaborations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2021 - 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/>.
*/

import $ from 'jquery'
import {isValidDeepLinkingEvent} from './DeepLinking'
import processSingleContentItem from './processors/processSingleContentItem'
import I18n from 'i18n!collaborations'

export const addDeepLinkingListener = () => {
removeDeepLinkingListener()
window.addEventListener('message', handleDeepLinking)
}

/*
* Creates or updates a Collaboration in Canvas.
*
* A processing function called by both the
* LTI Advantage handleDeepLinking handler and the
* LTI 1.1 content item handler.
*/
export function onExternalContentReady(e, data) {
const contentItem = {contentItems: JSON.stringify(data.contentItems)}
if (data.service_id) {
updateCollaboration(contentItem, data.service_id)
} else {
createCollaboration(contentItem)
}
}

/*
* Handles deep linking response events in
* the collaborations UI. Only a single LtiResourceLink
* content item is supported
*/
export const handleDeepLinking = async event => {
// Don't attempt to process invalid messages
if (!isValidDeepLinkingEvent(event, ENV)) {
return
}

try {
const item = await processSingleContentItem(event)
onExternalContentReady(event, {
service_id: item.service_id,
contentItems: [item]
})
} catch (e) {
$.flashError(I18n.t('Error retrieving content from tool'))
}
}

export function collaborationUrl(id) {
return window.location.toString() + '/' + id
}

const removeDeepLinkingListener = () => {
window.removeEventListener('message', handleDeepLinking)
}

function updateCollaboration(contentItem, collab_id) {
const url = document.querySelector('.collaboration_' + collab_id + ' a.title')?.href
$.ajaxJSON(url, 'PUT', contentItem, collaborationSuccess, msg => {
$.screenReaderFlashMessage(I18n.t('Collaboration update failed'))
})
}

function createCollaboration(contentItem) {
const url = document.querySelector('#new_collaboration')?.getAttribute('action')
$.ajaxJSON(url, 'POST', contentItem, collaborationSuccess, msg => {
$.screenReaderFlashMessage(I18n.t('Collaboration creation failed'))
})
}

function collaborationSuccess(msg) {
openCollaboration(msg.collaboration.id)
window.location.reload()
}

function openCollaboration(id) {
window.open(collaborationUrl(id))
}
Loading

0 comments on commit d720d0d

Please sign in to comment.