post lti tool config to api when creating lti dev key
closes PLAT-3745 test plan: * have an example tool config (from `lti-1.3-test-tool`) that includes a public_jwk * create a new lti dev key, paste example config into json field, click "Next" * an api request should finish without errors * `state.createLtiKeyState.toolConfiguration` should be the object representation of the json config * finish creating the key, it should show up in the list * in the rails console, confirm that `Lti::ToolConfiguration.last` has a `developer_key_id` that matches the newly created key Change-Id: I495ae406c0f06e65ab80bcb14ed6086d9ff4bdce Reviewed-on: https://gerrit.instructure.com/166664 Tested-by: Jenkins Reviewed-by: Marc Phillips <mphillips@instructure.com> Reviewed-by: Weston Dransfield <wdransfield@instructure.com> QA-Review: Weston Dransfield <wdransfield@instructure.com> Product-Review: Xander Moffatt <xmoffatt@instructure.com>
This commit is contained in:
parent
3648396895
commit
37470d9b52
|
@ -26,6 +26,7 @@ import {ModalFooter} from '@instructure/ui-overlays/lib/components/Modal'
|
|||
export default class LtiKeyFooter extends React.Component {
|
||||
onAdvanceToCustomization = () => {
|
||||
this.props.dispatch(this.props.ltiKeysSetCustomizing(true))
|
||||
this.props.onAdvanceToCustomization()
|
||||
}
|
||||
|
||||
onSave = e => {
|
||||
|
@ -53,7 +54,8 @@ export default class LtiKeyFooter extends React.Component {
|
|||
render() {
|
||||
return (
|
||||
<ModalFooter>
|
||||
<Button onClick={this.onCancel}>{I18n.t('Cancel')}</Button>
|
||||
<Button onClick={this.onCancel}>{I18n.t('Cancel')}</Button>
|
||||
|
||||
{this.nextOrSaveButton()}
|
||||
</ModalFooter>
|
||||
)
|
||||
|
@ -65,5 +67,6 @@ LtiKeyFooter.propTypes = {
|
|||
ltiKeysSetCustomizing: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func.isRequired,
|
||||
onSaveClick: PropTypes.func.isRequired,
|
||||
onAdvanceToCustomization: PropTypes.func.isRequired,
|
||||
customizing: PropTypes.bool.isRequired
|
||||
}
|
||||
|
|
|
@ -86,12 +86,31 @@ export default class DeveloperKeyModal extends React.Component {
|
|||
)
|
||||
}
|
||||
|
||||
saveLtiToolConfiguration = () => {
|
||||
const formData = new FormData(this.submissionForm)
|
||||
this.props.store.dispatch(this.props.actions.saveLtiToolConfiguration({
|
||||
account_id: this.props.ctx.params.contextId,
|
||||
developer_key: {
|
||||
name: formData.get("developer_key[name]"),
|
||||
email: formData.get("developer_key[email]"),
|
||||
notes: formData.get("developer_key[notes]"),
|
||||
test_cluster_only: this.testClusterOnly
|
||||
},
|
||||
settings: JSON.parse(formData.get("tool_configuration")),
|
||||
settings_url: formData.get("tool_configuration_url"),
|
||||
}))
|
||||
}
|
||||
|
||||
get isLtiKey() {
|
||||
return this.props.createLtiKeyState.isLtiKey
|
||||
}
|
||||
|
||||
modalBody() {
|
||||
if (this.props.createOrEditDeveloperKeyState.developerKeyCreateOrEditPending) {
|
||||
const isSavingDeveloperKey = this.props.createOrEditDeveloperKeyState.developerKeyCreateOrEditPending
|
||||
const isSavingLtiToolConfig = this.props.createLtiKeyState.saveToolConfigurationPending
|
||||
if (
|
||||
isSavingDeveloperKey || isSavingLtiToolConfig
|
||||
) {
|
||||
return this.spinner()
|
||||
}
|
||||
return this.developerKeyForm()
|
||||
|
@ -103,6 +122,7 @@ export default class DeveloperKeyModal extends React.Component {
|
|||
<LtiKeyFooter
|
||||
onCancelClick={this.closeModal}
|
||||
onSaveClick={this.submitForm}
|
||||
onAdvanceToCustomization={this.saveLtiToolConfiguration}
|
||||
customizing={this.props.createLtiKeyState.customizing}
|
||||
ltiKeysSetCustomizing={this.props.actions.ltiKeysSetCustomizing}
|
||||
dispatch={this.props.store.dispatch}
|
||||
|
@ -160,8 +180,7 @@ export default class DeveloperKeyModal extends React.Component {
|
|||
closeModal = () => {
|
||||
const { actions, store } = this.props
|
||||
store.dispatch(actions.developerKeysModalClose())
|
||||
store.dispatch(actions.ltiKeysSetCustomizing(false))
|
||||
store.dispatch(actions.ltiKeysSetLtiKey(false))
|
||||
store.dispatch(actions.resetLtiState())
|
||||
store.dispatch(actions.editDeveloperKey())
|
||||
}
|
||||
|
||||
|
@ -203,11 +222,16 @@ DeveloperKeyModal.propTypes = {
|
|||
createOrEditDeveloperKey: PropTypes.func.isRequired,
|
||||
developerKeysModalClose: PropTypes.func.isRequired,
|
||||
editDeveloperKey: PropTypes.func.isRequired,
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired
|
||||
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
|
||||
saveLtiToolConfiguration: PropTypes.func.isRequired,
|
||||
resetLtiState: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
createLtiKeyState: PropTypes.shape({
|
||||
isLtiKey: PropTypes.bool.isRequired,
|
||||
customizing: PropTypes.bool.isRequired
|
||||
customizing: PropTypes.bool.isRequired,
|
||||
toolConfiguration: PropTypes.object.isRequired,
|
||||
toolConfigurationUrl: PropTypes.string.isRequired,
|
||||
saveToolConfigurationPending: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
createOrEditDeveloperKeyState: PropTypes.shape({
|
||||
developerKeyCreateOrEditSuccessful: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -27,8 +27,8 @@ export default class ToolConfiguration extends React.Component {
|
|||
if (!this.props.createLtiKeyState.customizing) {
|
||||
return (
|
||||
<ToolConfigurationForm
|
||||
toolConfiguration={this.props.toolConfiguration}
|
||||
configurationUrl={this.props.configurationUrl}
|
||||
toolConfiguration={this.props.createLtiKeyState.toolConfiguration}
|
||||
toolConfigurationUrl={this.props.createLtiKeyState.toolConfigurationUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -55,20 +55,14 @@ export default class ToolConfiguration extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ToolConfiguration.defaultProps = {
|
||||
toolConfiguration: undefined,
|
||||
configurationUrl: undefined
|
||||
}
|
||||
|
||||
ToolConfiguration.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
setEnabledScopes: PropTypes.func.isRequired,
|
||||
setDisabledPlacements: PropTypes.func.isRequired,
|
||||
toolConfiguration: PropTypes.object,
|
||||
configurationUrl: PropTypes.string,
|
||||
createLtiKeyState: PropTypes.shape({
|
||||
customizing: PropTypes.bool.isRequired,
|
||||
toolConfiguration: PropTypes.object.isRequired,
|
||||
toolConfigurationUrl: PropTypes.string.isRequired,
|
||||
enabledScopes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
disabledPlacements: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
}).isRequired
|
||||
|
|
|
@ -45,13 +45,20 @@ export default class ToolConfigurationForm extends React.Component {
|
|||
if (this.state.configurationType === 'json') {
|
||||
return (
|
||||
<TextArea
|
||||
name="tool_configuration"
|
||||
defaultValue={this.toolConfiguration}
|
||||
label={I18n.t('LTI 1.3 Configuration')}
|
||||
maxHeight="20rem"
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <TextInput defaultValue={this.props.configurationUrl} label={I18n.t('JSON URL')} />
|
||||
return (
|
||||
<TextInput
|
||||
name="tool_configuration_url"
|
||||
defaultValue={this.props.toolConfigurationUrl}
|
||||
label={I18n.t('JSON URL')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -75,12 +82,7 @@ export default class ToolConfigurationForm extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ToolConfigurationForm.defaultProps = {
|
||||
toolConfiguration: undefined,
|
||||
configurationUrl: undefined
|
||||
}
|
||||
|
||||
ToolConfigurationForm.propTypes = {
|
||||
toolConfiguration: PropTypes.object,
|
||||
configurationUrl: PropTypes.string
|
||||
toolConfiguration: PropTypes.object.isRequired,
|
||||
toolConfigurationUrl: PropTypes.string.isRequired
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ function newProps(customizing = false) {
|
|||
return {
|
||||
onCancelClick: jest.fn(),
|
||||
onSaveClick: jest.fn(),
|
||||
onAdvanceToCustomization: jest.fn(),
|
||||
customizing,
|
||||
dispatch: jest.fn(),
|
||||
ltiKeysSetCustomizing: jest.fn()
|
||||
|
@ -64,6 +65,7 @@ it("Advances to the 'customizing' state when 'next' is clicked", () => {
|
|||
.at(1)
|
||||
.simulate('click')
|
||||
expect(props.ltiKeysSetCustomizing).toHaveBeenCalledWith(true)
|
||||
expect(props.onAdvanceToCustomization).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("Renders the 'Next' button if not customizing", () => {
|
||||
|
|
|
@ -23,7 +23,7 @@ import ToolConfigurationForm from '../ToolConfigurationForm'
|
|||
function newProps() {
|
||||
return {
|
||||
toolConfiguration: {name: 'Test Tool', url: 'https://www.test.com/launch'},
|
||||
configurationUrl: 'https://www.test.com/config.json'
|
||||
toolConfigurationUrl: 'https://www.test.com/config.json'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ describe('when configuration method is by URL', () => {
|
|||
|
||||
it('renders the tool configuration URL in a text input', () => {
|
||||
const textInput = wrapper.find('TextInput')
|
||||
const expectedString = newProps().configurationUrl
|
||||
const expectedString = newProps().toolConfigurationUrl
|
||||
expect(textInput.html()).toEqual(expect.stringContaining(expectedString))
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 actions from 'jsx/developer_keys/actions/ltiKeyActions'
|
||||
import developerKeysActions from 'jsx/developer_keys/actions/developerKeysActions'
|
||||
import axios from 'axios'
|
||||
|
||||
const dispatch = jest.fn()
|
||||
|
||||
beforeAll(() => {
|
||||
axios.post = jest.fn().mockResolvedValue({
|
||||
data: {tool_configuration: {settings: {test: 'config'}, developer_key_id: '1'}}
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axios.post.mockRestore()
|
||||
})
|
||||
|
||||
describe('saveLtiToolConfiguration', () => {
|
||||
const save = (includeUrl = false) => {
|
||||
actions.saveLtiToolConfiguration({
|
||||
account_id: '1',
|
||||
developer_key: {name: 'test'},
|
||||
settings: {test: 'config'},
|
||||
...(includeUrl ? {settings_url: 'test.url'} : {})
|
||||
})(dispatch)
|
||||
}
|
||||
|
||||
it('sets the developer key with provided fields', () => {
|
||||
save()
|
||||
expect(dispatch).toBeCalledWith(developerKeysActions.setEditingDeveloperKey({name: 'test'}))
|
||||
})
|
||||
|
||||
it('sets the tool configuration url if provided', () => {
|
||||
save(true)
|
||||
expect(dispatch).toBeCalledWith(actions.setLtiToolConfigurationUrl('test.url'))
|
||||
})
|
||||
|
||||
describe('on successful response', () => {
|
||||
it('sets the tool configuration', () => {
|
||||
expect(dispatch).toBeCalledWith(actions.setLtiToolConfiguration({test: 'config'}))
|
||||
})
|
||||
|
||||
it('prepends the developer key to the list', () => {
|
||||
expect(dispatch).toBeCalledWith(
|
||||
developerKeysActions.listDeveloperKeysPrepend({name: 'test', id: '1'})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -15,6 +15,9 @@
|
|||
* 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 axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
import developerKeysActions from './developerKeysActions'
|
||||
|
||||
const actions = {}
|
||||
|
||||
|
@ -42,4 +45,76 @@ actions.ltiKeysSetDisabledPlacements = payload => ({
|
|||
payload
|
||||
})
|
||||
|
||||
actions.SET_LTI_TOOL_CONFIGURATION = 'SET_LTI_TOOL_CONFIGURATION'
|
||||
actions.setLtiToolConfiguration = payload => ({
|
||||
type: actions.SET_LTI_TOOL_CONFIGURATION,
|
||||
payload
|
||||
})
|
||||
|
||||
actions.SET_LTI_TOOL_CONFIGURATION_URL = 'SET_LTI_TOOL_CONFIGURATION_URL'
|
||||
actions.setLtiToolConfigurationUrl = payload => ({
|
||||
type: actions.SET_LTI_TOOL_CONFIGURATION_URL,
|
||||
payload
|
||||
})
|
||||
|
||||
actions.RESET_LTI_STATE = 'RESET_LTI_STATE'
|
||||
actions.resetLtiState = () => ({type: actions.RESET_LTI_STATE})
|
||||
|
||||
actions.SAVE_LTI_TOOL_CONFIGURATION_START = 'SAVE_LTI_TOOL_CONFIGURATION_START'
|
||||
actions.saveLtiToolConfigurationStart = () => ({
|
||||
type: actions.SAVE_LTI_TOOL_CONFIGURATION_START
|
||||
})
|
||||
|
||||
actions.SAVE_LTI_TOOL_CONFIGURATION_FAILED = 'SAVE_LTI_TOOL_CONFIGURATION_FAILED'
|
||||
actions.saveLtiToolConfigurationFailed = payload => ({
|
||||
type: actions.SAVE_LTI_TOOL_CONFIGURATION_FAILED,
|
||||
payload
|
||||
})
|
||||
|
||||
actions.SAVE_LTI_TOOL_CONFIGURATION_SUCCESSFUL = 'SAVE_LTI_TOOL_CONFIGURATION_SUCCESSFUL'
|
||||
actions.saveLtiToolConfigurationSuccessful = payload => ({
|
||||
type: actions.SAVE_LTI_TOOL_CONFIGURATION_SUCCESSFUL,
|
||||
payload
|
||||
})
|
||||
|
||||
actions.saveLtiToolConfiguration = ({
|
||||
account_id,
|
||||
settings,
|
||||
settings_url,
|
||||
developer_key
|
||||
}) => dispatch => {
|
||||
dispatch(actions.saveLtiToolConfigurationStart())
|
||||
dispatch(developerKeysActions.setEditingDeveloperKey(developer_key))
|
||||
|
||||
if (settings_url) {
|
||||
dispatch(actions.setLtiToolConfigurationUrl(settings_url))
|
||||
}
|
||||
|
||||
const url = `/api/lti/accounts/${account_id}/developer_keys/tool_configuration`
|
||||
|
||||
axios
|
||||
.post(url, {
|
||||
tool_configuration: {
|
||||
settings,
|
||||
...(settings_url ? {settings_url} : {})
|
||||
},
|
||||
developer_key
|
||||
})
|
||||
.then(response => {
|
||||
dispatch(actions.saveLtiToolConfigurationSuccessful())
|
||||
dispatch(actions.setLtiToolConfiguration(response.data.tool_configuration.settings))
|
||||
|
||||
const newDevKey = {
|
||||
...developer_key,
|
||||
id: response.data.tool_configuration.developer_key_id
|
||||
}
|
||||
dispatch(developerKeysActions.setEditingDeveloperKey(newDevKey))
|
||||
dispatch(developerKeysActions.listDeveloperKeysPrepend(newDevKey))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(actions.saveLtiToolConfigurationFailed(error))
|
||||
$.flashError(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
export default actions
|
||||
|
|
|
@ -23,7 +23,11 @@ function freshState() {
|
|||
return {
|
||||
isLtiKey: false,
|
||||
customizing: false,
|
||||
saveToolConfigurationPending: false,
|
||||
saveToolConfigurationSuccessful: false,
|
||||
saveToolConfigurationError: null,
|
||||
toolConfiguration: {},
|
||||
toolConfigurationUrl: '',
|
||||
enabledScopes: [],
|
||||
disabledPlacements: []
|
||||
}
|
||||
|
@ -85,3 +89,48 @@ it('handles "LTI_KEYS_SET_DISABLED_PLACEMENTS"', () => {
|
|||
expect(newState.disabledPlacements).toEqual(['account_navigation'])
|
||||
expect(newState.enabledScopes).toEqual([])
|
||||
})
|
||||
|
||||
it('handles "SET_LTI_TOOL_CONFIGURATION"', () => {
|
||||
const state = freshState()
|
||||
const config = {test: 'config'}
|
||||
const action = actions.setLtiToolConfiguration(config)
|
||||
const newState = reducer(state, action)
|
||||
|
||||
expect(newState.toolConfiguration).toEqual(config)
|
||||
})
|
||||
|
||||
it('handles "SET_LTI_TOOL_CONFIGURATION_URL"', () => {
|
||||
const state = freshState()
|
||||
const configUrl = 'config.url'
|
||||
const action = actions.setLtiToolConfigurationUrl(configUrl)
|
||||
const newState = reducer(state, action)
|
||||
|
||||
expect(newState.toolConfigurationUrl).toEqual(configUrl)
|
||||
})
|
||||
it('handles "SAVE_LTI_TOOL_CONFIGURATION_START"', () => {
|
||||
const state = freshState()
|
||||
const action = actions.saveLtiToolConfigurationStart()
|
||||
const newState = reducer(state, action)
|
||||
|
||||
expect(newState.saveToolConfigurationPending).toEqual(true)
|
||||
})
|
||||
|
||||
it('handles "SAVE_LTI_TOOL_CONFIGURATION_ERROR"', () => {
|
||||
const state = freshState()
|
||||
const error = new Error('error')
|
||||
const action = actions.saveLtiToolConfigurationFailed(error)
|
||||
const newState = reducer(state, action)
|
||||
|
||||
expect(newState.saveToolConfigurationPending).toEqual(false)
|
||||
expect(newState.saveToolConfigurationError).toEqual(error)
|
||||
})
|
||||
|
||||
it('handles "SAVE_LTI_TOOL_CONFIGURATION_SUCCESSFUL"', () => {
|
||||
const state = freshState()
|
||||
const action = actions.saveLtiToolConfigurationSuccessful()
|
||||
const newState = reducer(state, action)
|
||||
|
||||
expect(newState.saveToolConfigurationPending).toEqual(false)
|
||||
expect(newState.saveToolConfigurationError).toBeNull()
|
||||
expect(newState.saveToolConfigurationSuccessful).toEqual(true)
|
||||
})
|
||||
|
|
|
@ -16,12 +16,16 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ACTION_NAMES from '../actions/developerKeysActions'
|
||||
import ACTION_NAMES from '../actions/ltiKeyActions'
|
||||
|
||||
const initialState = {
|
||||
isLtiKey: false,
|
||||
customizing: false,
|
||||
saveToolConfigurationPending: false,
|
||||
saveToolConfigurationSuccessful: false,
|
||||
saveToolConfigurationError: null,
|
||||
toolConfiguration: {},
|
||||
toolConfigurationUrl: '',
|
||||
enabledScopes: [],
|
||||
disabledPlacements: []
|
||||
}
|
||||
|
@ -42,6 +46,36 @@ const ltiKeysHandlers = {
|
|||
[ACTION_NAMES.LTI_KEYS_SET_DISABLED_PLACEMENTS]: (state, action) => ({
|
||||
...state,
|
||||
disabledPlacements: action.payload
|
||||
}),
|
||||
[ACTION_NAMES.SET_LTI_TOOL_CONFIGURATION]: (state, action) => ({
|
||||
...state,
|
||||
toolConfiguration: action.payload
|
||||
}),
|
||||
[ACTION_NAMES.SET_LTI_TOOL_CONFIGURATION_URL]: (state, action) => ({
|
||||
...state,
|
||||
toolConfigurationUrl: action.payload
|
||||
}),
|
||||
[ACTION_NAMES.SAVE_LTI_TOOL_CONFIGURATION_START]: state => ({
|
||||
...state,
|
||||
saveToolConfigurationPending: true
|
||||
}),
|
||||
[ACTION_NAMES.SAVE_LTI_TOOL_CONFIGURATION_FAILED]: (state, action) => ({
|
||||
...state,
|
||||
saveToolConfigurationPending: false,
|
||||
saveToolConfigurationError: action.payload
|
||||
}),
|
||||
[ACTION_NAMES.SAVE_LTI_TOOL_CONFIGURATION_SUCCESSFUL]: state => ({
|
||||
...state,
|
||||
saveToolConfigurationPending: false,
|
||||
saveToolConfigurationError: null,
|
||||
saveToolConfigurationSuccessful: true
|
||||
}),
|
||||
[ACTION_NAMES.RESET_LTI_STATE]: state => ({
|
||||
...state,
|
||||
isLtiKey: false,
|
||||
customizing: false,
|
||||
toolConfiguration: {},
|
||||
toolConfigurationUrl: ''
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -354,7 +354,7 @@ test('clears the lti key state when modal is closed', () => {
|
|||
const actions = Object.assign(fakeActions, {
|
||||
developerKeysModalClose: () => {},
|
||||
ltiKeysSetCustomizing: () => {},
|
||||
ltiKeysSetLtiKey: ltiStub
|
||||
resetLtiState: ltiStub
|
||||
})
|
||||
|
||||
const wrapper = mount(
|
||||
|
|
Loading…
Reference in New Issue