Add require_scopes field to DevKey Management

Can now set whether a developer key require scopes or not.
Legacy behavior of scopes is to allow dev keys to have
access to all endpoints, this allows for the continuing
behavior.

Fixes: PLAT-3345

Test Plan:
 - Go to a create/upate a key
 - Notice the require scopes checkbox
 - Click the require scopes checkbox so that it is
   toggled off
 - Scopes ui should be removed and saving the key
   persists the state
 - attempt to access an endpoint using the changed
   endpoint that had scopes, it should be able to act
   like a legacy key
 - go back to the key management modal and flip the
   require scopes back to on.
 - attempt to access an endpoint that does not have a
   scope for the dev key, should fail
 - for a legacy key, the scopes ui should be off when
   first editing.

Change-Id: I0a53ff8a44b80081b518d780a8288f4cc4c36027
Reviewed-on: https://gerrit.instructure.com/150216
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
Product-Review: Jesse Poulos <jpoulos@instructure.com>
Tested-by: Jenkins
This commit is contained in:
Marc Phillips 2018-05-14 14:37:45 -06:00 committed by Marc Alan Phillips
parent 5571a9e878
commit 489b5d6ecf
16 changed files with 327 additions and 164 deletions

View File

@ -148,6 +148,7 @@ class DeveloperKeysController < ApplicationController
:redirect_uris,
:vendor_code,
:visible,
:require_scopes,
scopes: []
)
end

View File

@ -22,11 +22,32 @@ import I18n from 'i18n!react_developer_keys'
import ScreenReaderContent from '@instructure/ui-core/lib/components/ScreenReaderContent'
import TextArea from '@instructure/ui-core/lib/components/TextArea'
import TextInput from '@instructure/ui-core/lib/components/TextInput'
import React from 'react'
import PropTypes from 'prop-types'
import DeveloperKeyScopes from './Scopes'
export default class DeveloperKeyFormFields extends React.Component {
constructor (props) {
super(props)
const { developerKey } = props
this.state = {
requireScopes: developerKey && developerKey.require_scopes
}
}
get keyForm () {
return this.keyFormRef
}
get requireScopes () {
return this.state.requireScopes
}
setKeyFormRef = node => { this.keyFormRef = node }
fieldValue(field, defaultValue) {
const {developerKey} = this.props
if (Object.keys(developerKey).length > 0) {
@ -35,74 +56,74 @@ export default class DeveloperKeyFormFields extends React.Component {
return developerKey[field]
}
developerKeySettings() {
return (
<FormFieldGroup
rowSpacing="small"
vAlign="middle"
description={<ScreenReaderContent>{I18n.t('Developer Key Settings')}</ScreenReaderContent>}
>
<TextInput
label={I18n.t('Key Name:')}
name="developer_key[name]"
defaultValue={this.fieldValue('name', 'Unnamed Tool')}
/>
<TextInput
label={I18n.t('Owner Email:')}
name="developer_key[email]"
defaultValue={this.fieldValue('email')}
/>
<TextInput
label={I18n.t('Redirect URI (Legacy):')}
name="developer_key[redirect_uri]"
defaultValue={this.fieldValue('redirect_uri')}
/>
<TextArea
label={I18n.t('Redirect URIs:')}
name="developer_key[redirect_uris]"
defaultValue={this.fieldValue('redirect_uris')}
resize="both"
/>
<TextInput
label={I18n.t('Vendor Code (LTI 2):')}
name="developer_key[vendor_code]"
defaultValue={this.fieldValue('vendor_code')}
/>
<TextInput
label={I18n.t('Icon URL:')}
name="developer_key[icon_url]"
defaultValue={this.fieldValue('icon_url')}
/>
<TextArea
label={I18n.t('Notes:')}
name="developer_key[notes]"
defaultValue={this.fieldValue('notes')}
resize="both"
/>
</FormFieldGroup>
)
}
scopeSettings() {
return (
<DeveloperKeyScopes
developerKey={this.props.developerKey}
availableScopes={this.props.availableScopes}
availableScopesPending={this.props.availableScopesPending}
store={this.props.store}
actions={this.props.actions}
/>
)
handleRequireScopesChange = () => {
this.setState({ requireScopes: !this.state.requireScopes })
}
render() {
return (
<Grid hAlign="center">
<GridRow>
<GridCol width={3}>{this.developerKeySettings()}</GridCol>
<GridCol width={8}>{this.scopeSettings()}</GridCol>
</GridRow>
</Grid>
<form ref={this.setKeyFormRef}>
<Grid hAlign="center">
<GridRow>
<GridCol width={3}>
<FormFieldGroup
rowSpacing="small"
vAlign="middle"
description={<ScreenReaderContent>{I18n.t('Developer Key Settings')}</ScreenReaderContent>}
>
<TextInput
label={I18n.t('Key Name:')}
name="developer_key[name]"
defaultValue={this.fieldValue('name', 'Unnamed Tool')}
/>
<TextInput
label={I18n.t('Owner Email:')}
name="developer_key[email]"
defaultValue={this.fieldValue('email')}
/>
<TextInput
label={I18n.t('Redirect URI (Legacy):')}
name="developer_key[redirect_uri]"
defaultValue={this.fieldValue('redirect_uri')}
/>
<TextArea
label={I18n.t('Redirect URIs:')}
name="developer_key[redirect_uris]"
defaultValue={this.fieldValue('redirect_uris')}
resize="both"
/>
<TextInput
label={I18n.t('Vendor Code (LTI 2):')}
name="developer_key[vendor_code]"
defaultValue={this.fieldValue('vendor_code')}
/>
<TextInput
label={I18n.t('Icon URL:')}
name="developer_key[icon_url]"
defaultValue={this.fieldValue('icon_url')}
/>
<TextArea
label={I18n.t('Notes:')}
name="developer_key[notes]"
defaultValue={this.fieldValue('notes')}
resize="both"
/>
</FormFieldGroup>
</GridCol>
<GridCol width={8}>
<DeveloperKeyScopes
availableScopes={this.props.availableScopes}
availableScopesPending={this.props.availableScopesPending}
developerKey={this.props.developerKey}
requireScopes={this.state.requireScopes}
onRequireScopesChange={this.handleRequireScopesChange}
dispatch={this.props.dispatch}
listDeveloperKeyScopesSet={this.props.listDeveloperKeyScopesSet}
/>
</GridCol>
</GridRow>
</Grid>
</form>
)
}
}
@ -112,19 +133,16 @@ DeveloperKeyFormFields.defaultProps = {
}
DeveloperKeyFormFields.propTypes = {
store: PropTypes.shape({
dispatch: PropTypes.func.isRequired
}).isRequired,
actions: PropTypes.shape({
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
}).isRequired,
dispatch: PropTypes.func.isRequired,
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
developerKey: PropTypes.shape({
notes: PropTypes.string,
icon_url: PropTypes.string,
vendor_code: PropTypes.string,
redirect_uris: PropTypes.string,
email: PropTypes.string,
name: PropTypes.string
name: PropTypes.string,
require_scopes: PropTypes.bool
}),
availableScopes: PropTypes.objectOf(PropTypes.arrayOf(
PropTypes.shape({

View File

@ -42,22 +42,37 @@ export default class DeveloperKeyModal extends React.Component {
return this.developerKey() ? I18n.t('Create developer key') : I18n.t('Edit developer key')
}
get submissionForm () {
return this.newForm ? this.newForm.keyForm : <form />
}
get requireScopes () {
return this.newForm && this.newForm.requireScopes
}
submitForm = () => {
const method = this.developerKey() ? 'put' : 'post'
const formData = new FormData(this.form)
const scopesArrayKey = 'developer_key[scopes][]'
const formData = new FormData(this.submissionForm)
this.props.selectedScopes.forEach((scope) => {
formData.append(scopesArrayKey, scope)
})
if (this.props.selectedScopes.length > 0) {
this.props.store.dispatch(
this.props.actions.createOrEditDeveloperKey(formData, this.developerKeyUrl(), method)
)
} else {
if (!this.requireScopes) {
formData.delete('developer_key[scopes][]')
formData.append('developer_key[require_scopes]', false)
} else if (this.props.selectedScopes.length === 0 &&
this.props.createOrEditDeveloperKeyState.developerKey.scopes.length === 0
) {
$.flashError(I18n.t('At least one scope must be selected.'))
return
} else {
const scopesArrayKey = 'developer_key[scopes][]'
this.props.selectedScopes.forEach((scope) => {
formData.append(scopesArrayKey, scope)
})
formData.append('developer_key[require_scopes]', true)
}
this.props.store.dispatch(
this.props.actions.createOrEditDeveloperKey(formData, this.developerKeyUrl(), method)
)
}
modalBody() {
@ -76,26 +91,24 @@ export default class DeveloperKeyModal extends React.Component {
}
developerKeyForm() {
return (
<form
ref={el => {
this.form = el
}}
>
<DeveloperKeyFormFields
developerKey={this.developerKey()}
availableScopes={this.props.availableScopes}
availableScopesPending={this.props.availableScopesPending}
store={this.props.store}
actions={this.props.actions}
/>
</form>
)
const {
availableScopes,
availableScopesPending,
createOrEditDeveloperKeyState: { developerKey }
} = this.props;
return <DeveloperKeyFormFields
ref={this.setNewFormRef}
developerKey={developerKey}
availableScopes={availableScopes}
availableScopesPending={availableScopesPending}
dispatch={this.props.store.dispatch}
listDeveloperKeyScopesSet={this.props.actions.listDeveloperKeyScopesSet}
/>
}
modalContainerRef = div => {
this.modalContainer = div
}
setNewFormRef = node => { this.newForm = node }
modalContainerRef = node => { this.modalContainer = node }
modalIsOpen() {
return this.props.createOrEditDeveloperKeyState.developerKeyModalOpen
@ -124,7 +137,7 @@ export default class DeveloperKeyModal extends React.Component {
mountNode={this.props.mountNode}
>
<ModalHeader>
<Heading level="h4">{I18n.t('Key Settings')}</Heading>
<Heading level="h3" as="h2">{I18n.t('Key Settings')}</Heading>
</ModalHeader>
<ModalBody>{this.modalBody()}</ModalBody>
<ModalFooter>
@ -152,7 +165,8 @@ DeveloperKeyModal.propTypes = {
actions: PropTypes.shape({
createOrEditDeveloperKey: PropTypes.func.isRequired,
developerKeysModalClose: PropTypes.func.isRequired,
setEditingDeveloperKey: PropTypes.func.isRequired
setEditingDeveloperKey: PropTypes.func.isRequired,
listDeveloperKeyScopesSet: PropTypes.func.isRequired
}).isRequired,
createOrEditDeveloperKeyState: PropTypes.shape({
developerKeyCreateOrEditSuccessful: PropTypes.bool.isRequired,

View File

@ -19,12 +19,19 @@
import I18n from 'i18n!react_developer_keys'
import PropTypes from 'prop-types'
import React from 'react'
import Billboard from '@instructure/ui-core/lib/components/Billboard'
import Checkbox from '@instructure/ui-core/lib/components/Checkbox'
import Flex, { FlexItem } from '@instructure/ui-layout/lib/components/Flex'
import Grid, {GridCol, GridRow} from '@instructure/ui-core/lib/components/Grid'
import IconWarning from 'instructure-icons/lib/Line/IconWarningLine'
import IconSearchLine from 'instructure-icons/lib/Line/IconSearchLine'
import ScreenReaderContent from '@instructure/ui-core/lib/components/ScreenReaderContent'
import Spinner from '@instructure/ui-core/lib/components/Spinner'
import Text from '@instructure/ui-core/lib/components/Text'
import TextInput from '@instructure/ui-core/lib/components/TextInput'
import View from '@instructure/ui-layout/lib/components/View'
import DeveloperKeyScopesList from './ScopesList'
export default class DeveloperKeyScopes extends React.Component {
@ -36,7 +43,14 @@ export default class DeveloperKeyScopes extends React.Component {
})
}
enforceScopesSrText () {
return this.props.requireScopes
? I18n.t('Clicking the checkbox will cause scopes table to disappear below')
: I18n.t('Clicking the checkbox will cause scopes table to appear below')
}
body() {
const { developerKey } = this.props
if (this.props.availableScopesPending) {
return (
<GridRow hAlign="space-around">
@ -48,16 +62,31 @@ export default class DeveloperKeyScopes extends React.Component {
</GridRow>
)
}
return (
<GridRow>
<GridCol>
<DeveloperKeyScopesList
availableScopes={this.props.availableScopes}
selectedScopes={this.props.developerKey.scopes}
filter={this.state.filter}
actions={this.props.actions}
dispatch={this.props.store.dispatch}
/>
<View>
{this.props.requireScopes
? <DeveloperKeyScopesList
availableScopes={this.props.availableScopes}
selectedScopes={developerKey ? developerKey.scopes : []}
filter={this.state.filter}
listDeveloperKeyScopesSet={this.props.listDeveloperKeyScopesSet}
dispatch={this.props.dispatch}
/>
: <Billboard
hero={<IconWarning />}
size="large"
headingAs="h2"
headingLevel="h2"
margin="xx-large"
readOnly
heading={I18n.t('When scope enforcement is disabled, tokens have access to all endpoints available to the authorizing user.')}
/>
}
</View>
</GridCol>
</GridRow>
)
@ -73,13 +102,30 @@ export default class DeveloperKeyScopes extends React.Component {
</Text>
</GridCol>
<GridCol width="auto">
<TextInput
label={<ScreenReaderContent />}
placeholder={I18n.t('Search endpoints')}
type="search"
icon={() => <IconSearchLine />}
onChange={this.handleFilterChange}
/>
<Flex>
<FlexItem padding="0 large 0 0" data-automation="enforce_scopes">
<Checkbox
variant="toggle"
label={
<span>
<Text>{I18n.t('Enforce Scopes')}</Text>
<ScreenReaderContent>{this.enforceScopesSrText()}</ScreenReaderContent>
</span>
}
checked={this.props.requireScopes}
onChange={this.props.onRequireScopesChange}
/>
</FlexItem>
<FlexItem>
<TextInput
label={<ScreenReaderContent />}
placeholder={I18n.t('Search endpoints')}
type="search"
icon={() => <IconSearchLine />}
onChange={this.handleFilterChange}
/>
</FlexItem>
</Flex>
</GridCol>
</GridRow>
{this.body()}
@ -96,12 +142,8 @@ DeveloperKeyScopes.propTypes = {
})
)).isRequired,
availableScopesPending: PropTypes.bool.isRequired,
store: PropTypes.shape({
dispatch: PropTypes.func.isRequired
}).isRequired,
actions: PropTypes.shape({
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
}).isRequired,
dispatch: PropTypes.func.isRequired,
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
developerKey: PropTypes.shape({
notes: PropTypes.string,
icon_url: PropTypes.string,
@ -110,7 +152,9 @@ DeveloperKeyScopes.propTypes = {
email: PropTypes.string,
name: PropTypes.string,
scopes: PropTypes.arrayOf(PropTypes.string)
})
}),
requireScopes: PropTypes.bool.isRequired,
onRequireScopesChange: PropTypes.func.isRequired
}
DeveloperKeyScopes.defaultProps = {

View File

@ -65,7 +65,7 @@ export default class DeveloperKeyScopesList extends React.Component {
selectedScopes,
readOnlySelected: this.onlySelectGet(selectedScopes)
})
this.props.dispatch(this.props.actions.listDeveloperKeyScopesSet(selectedScopes))
this.props.dispatch(this.props.listDeveloperKeyScopesSet(selectedScopes))
}
uniqueSelectedScopes(selectedScopes) {
@ -94,7 +94,8 @@ export default class DeveloperKeyScopesList extends React.Component {
selectedScopes: newScopes,
readOnlySelected: event.currentTarget.checked
})
this.props.dispatch(this.props.actions.listDeveloperKeyScopesSet(newScopes))
this.props.dispatch(this.props.listDeveloperKeyScopesSet(newScopes))
}
noFilter() {
@ -176,9 +177,7 @@ export default class DeveloperKeyScopesList extends React.Component {
DeveloperKeyScopesList.propTypes = {
dispatch: PropTypes.func.isRequired,
actions: PropTypes.shape({
listDeveloperKeyScopesSet: PropTypes.func.isRequired
}).isRequired,
listDeveloperKeyScopesSet: PropTypes.func.isRequired,
availableScopes: PropTypes.objectOf(
PropTypes.arrayOf(
PropTypes.shape({

View File

@ -19,7 +19,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import Pill from '@instructure/ui-core/lib/components/Pill'
export default class DeveloperKeyScopesList extends React.Component {
export default class ScopesMethod extends React.Component {
methodColorMap() {
return({
get: 'primary',
@ -48,11 +48,11 @@ export default class DeveloperKeyScopesList extends React.Component {
}
}
DeveloperKeyScopesList.propTypes = {
ScopesMethod.propTypes = {
method: PropTypes.string.isRequired,
margin: PropTypes.string
}
DeveloperKeyScopesList.defaultProps = {
ScopesMethod.defaultProps = {
margin: undefined
}

View File

@ -18,9 +18,10 @@
import React from 'react'
import { mount } from 'enzyme'
import sinon from 'sinon'
import DeveloperKeyScopes from '../Scopes'
function props(pending = false) {
function props(pending = false, requireScopes = true, onRequireScopesChange = () => {}) {
return({
developerKey: {},
availableScopes: {
@ -47,7 +48,10 @@ function props(pending = false) {
]
},
availableScopesPending: pending,
store: { dispatch: () => {} }
dispatch: () => {},
listDeveloperKeyScopesSet: () => {},
requireScopes,
onRequireScopesChange
})
}
@ -74,3 +78,28 @@ it('handles filter input change by setting the filter state', () => {
wrapper.instance().handleFilterChange(eventDup)
expect(wrapper.state().filter).toBe('banana')
})
it('renders Billboard if requireScopes is false', () => {
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, false)} />)
wrapper.find('Billboard')
expect(wrapper.find('Billboard')).toHaveLength(1)
})
it('does not render Billboard if requireScopes is true', () => {
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, true)} />)
wrapper.find('Billboard')
expect(wrapper.find('Billboard')).toHaveLength(0)
})
it('renders DeveloperKeyScopesList if requireScopes is true', () => {
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, true)} />)
wrapper.find('DeveloperKeyScopesList')
expect(wrapper.find('DeveloperKeyScopesList')).toHaveLength(1)
})
it('controls requireScopes change when clicking requireScopes button', () => {
const requireScopesStub = sinon.stub()
const wrapper = mount(<DeveloperKeyScopes {...props(undefined, true, requireScopesStub)} />)
wrapper.find('Checkbox').filterWhere(n => n.prop('variant') === 'toggle').props().onChange()
expect(requireScopesStub.called).toBe(true)
})

View File

@ -115,6 +115,8 @@ const scopes = {
}
const props = {
dispatch: () => {},
listDeveloperKeyScopesSet: () => {},
availableScopes: {
"oauth":[
{
@ -139,8 +141,7 @@ const props = {
]
},
filter: '',
actions: {listDeveloperKeyScopesSet: () => {}},
dispatch: () => {}
actions: {listDeveloperKeyScopesSet: () => {}}
}
it("renders each group", () => {
@ -174,7 +175,7 @@ it("only renders 10 groups on the initaial render", () => {
expect(wrapper.instance().state.availableScopes).toHaveLength(10)
})
describe("handlerReadOnlySelected", () => {
describe("handleReadOnlySelected", () => {
it("selects all scopes with GET as the verb", () => {
const wrapper = mount(<DeveloperKeyScopesList {...props} />)
const fakeEvent = {
@ -285,4 +286,4 @@ describe("setSelectedScopes", () => {
expect(state.readOnlySelected).toEqual(false)
})
})
})
})

View File

@ -27,7 +27,13 @@ import makeVisibleDeveloperKeyReducer from '../reducers/makeVisibleReducer'
import makeInvisibleDeveloperKeyReducer from '../reducers/makeInvisibleReducer'
import listDeveloperKeyScopesReducer from '../reducers/listScopesReducer'
const createStoreWithMiddleware = applyMiddleware(ReduxThunk)(createStore)
const middleware = [
ReduxThunk,
// this is so redux-logger is not included in the production webpack bundle
(process.env.NODE_ENV !== 'production') && require('redux-logger')() // eslint-disable-line global-require
].filter(Boolean)
const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore)
const developerKeysReducer = combineReducers({
listDeveloperKeys: listDeveloperKeysReducer,

View File

@ -39,7 +39,6 @@ class DeveloperKey < ActiveRecord::Base
before_save :protect_default_key
after_save :clear_cache
after_create :create_default_account_binding
before_validation :set_require_scopes
before_validation :validate_scopes!
validates_as_url :redirect_uri, allowed_schemes: nil
@ -236,11 +235,6 @@ class DeveloperKey < ActiveRecord::Base
owner_account.developer_key_account_bindings.create!(developer_key: self)
end
def set_require_scopes
return unless api_token_scoping_on?
self.require_scopes = self.scopes.present?
end
def validate_scopes!
return true unless api_token_scoping_on?
return true if self.scopes.empty?

View File

@ -20,7 +20,7 @@ module Api::V1::DeveloperKey
include Api::V1::Json
DEVELOPER_KEY_JSON_ATTRS = %w(
name created_at email user_id user_name icon_url notes workflow_state scopes
name created_at email user_id user_name icon_url notes workflow_state scopes require_scopes
).freeze
INHERITED_DEVELOPER_KEY_JSON_ATTRS = %w[name created_at icon_url workflow_state].freeze

View File

@ -11,6 +11,7 @@
"@instructure/ui-buttons": "^5.9.0",
"@instructure/ui-core": "^4.8.0",
"@instructure/ui-icons": "^5.9.0",
"@instructure/ui-layout": "^5.9.0",
"@instructure/ui-menu": "^5.9.0",
"@instructure/ui-overlays": "^5.9.0",
"@instructure/ui-themeable": "^5.9.0",
@ -158,10 +159,10 @@
"style-loader": "^0.20",
"stylelint": "^9",
"through2": "^2",
"uglifyjs-webpack-plugin": "^1.2",
"webpack": "^3",
"webpack-cleanup-plugin": "^0.5",
"webpack-manifest-plugin": "^1",
"uglifyjs-webpack-plugin": "^1.2",
"xsslint": "0.1.4",
"yaml-loader": "^0.5"
},

View File

@ -106,6 +106,6 @@ test('populates the key notes', () => {
})
test('renders the scopes component', () => {
const [, , , , , input] = formFieldInputs(developerKey)
const [, , , , , , input] = formFieldInputs(developerKey)
equal(input.placeholder, "Search endpoints")
})

View File

@ -19,7 +19,7 @@
import React from 'react'
import {mount, shallow} from 'enzyme'
import DeveloperKeyModal from 'jsx/developer_keys/NewKeyModal'
import $ from 'jquery'
import $ from '../../../../app/coffeescripts/jquery.rails_flash_notifications'
QUnit.module('NewKeyModal', {
teardown() {
@ -100,7 +100,7 @@ test('it opens the modal if isOpen prop is true', () => {
/>
)
equal(wrapper.find('Modal').prop('open'), true)
ok(wrapper.find('Modal Heading [level="h4"]').exists())
ok(wrapper.find('Modal Heading [level="h3"]').exists())
})
test('it closes the modal if isOpen prop is false', () => {
@ -316,6 +316,47 @@ test('it sends the contents of the form saving', () => {
const createOrEditSpy = sinon.spy()
const dispatchSpy = sinon.spy()
const fakeActions = {
createOrEditDeveloperKey: createOrEditSpy
}
const fakeStore = {
dispatch: dispatchSpy
}
const developerKey2 = Object.assign({}, developerKey, { require_scopes: true, scopes: ['test'] })
const editDeveloperKeyState2 = Object.assign({}, editDeveloperKeyState, { developerKey: developerKey2 })
const wrapper = mount(
<DeveloperKeyModal
availableScopes={{}}
availableScopesPending={false}
closeModal={() => {}}
createOrEditDeveloperKeyState={editDeveloperKeyState2}
listDeveloperKeyScopesState={listDeveloperKeyScopesState}
actions={fakeActions}
store={fakeStore}
mountNode={modalMountNode}
selectedScopes={selectedScopes}
/>
)
wrapper.node.submitForm()
const [[sentFormData]] = createOrEditSpy.args
equal(sentFormData.get('developer_key[name]'), developerKey.name)
equal(sentFormData.get('developer_key[email]'), developerKey.email)
equal(sentFormData.get('developer_key[redirect_uri]'), developerKey.redirect_uri)
equal(sentFormData.get('developer_key[redirect_uris]'), developerKey.redirect_uris)
equal(sentFormData.get('developer_key[vendor_code]'), developerKey.vendor_code)
equal(sentFormData.get('developer_key[icon_url]'), developerKey.icon_url)
equal(sentFormData.get('developer_key[notes]'), developerKey.notes)
equal(sentFormData.get('developer_key[require_scopes]'), 'true')
})
test('sends form content without scopes and require_scopes set to false when not require_scopes', () => {
const createOrEditSpy = sinon.spy()
const dispatchSpy = sinon.spy()
const fakeActions = {
createOrEditDeveloperKey: createOrEditSpy
}
@ -348,6 +389,7 @@ test('it sends the contents of the form saving', () => {
equal(sentFormData.get('developer_key[vendor_code]'), developerKey.vendor_code)
equal(sentFormData.get('developer_key[icon_url]'), developerKey.icon_url)
equal(sentFormData.get('developer_key[notes]'), developerKey.notes)
equal(sentFormData.get('developer_key[require_scopes]'), 'false')
})
test('it adds each selected scope to the form data', () => {
@ -355,12 +397,14 @@ test('it adds each selected scope to the form data', () => {
const dispatchSpy = sinon.spy()
const fakeActions = { createOrEditDeveloperKey: createOrEditSpy }
const fakeStore = { dispatch: dispatchSpy }
const developerKey2 = Object.assign({}, developerKey, { require_scopes: true, scopes: ['test'] })
const editDeveloperKeyState2 = Object.assign({}, editDeveloperKeyState, { developerKey: developerKey2 })
const wrapper = mount(
<DeveloperKeyModal
availableScopes={{}}
availableScopesPending={false}
closeModal={() => {}}
createOrEditDeveloperKeyState={editDeveloperKeyState}
createOrEditDeveloperKeyState={editDeveloperKeyState2}
listDeveloperKeyScopesState={listDeveloperKeyScopesState}
actions={fakeActions}
store={fakeStore}
@ -379,12 +423,14 @@ test('flashes an error if no scopes are selected', () => {
const dispatchSpy = sinon.spy()
const fakeActions = { createOrEditDeveloperKey: createOrEditSpy }
const fakeStore = { dispatch: dispatchSpy }
const developerKey2 = Object.assign({}, developerKey, { require_scopes: true, scopes: [] })
const editDeveloperKeyState2 = Object.assign({}, editDeveloperKeyState, { developerKey: developerKey2 })
const wrapper = mount(
<DeveloperKeyModal
availableScopes={{}}
availableScopesPending={false}
closeModal={() => {}}
createOrEditDeveloperKeyState={editDeveloperKeyState}
createOrEditDeveloperKeyState={editDeveloperKeyState2}
listDeveloperKeyScopesState={listDeveloperKeyScopesState}
actions={fakeActions}
store={fakeStore}
@ -395,4 +441,4 @@ test('flashes an error if no scopes are selected', () => {
wrapper.node.submitForm()
ok(flashStub.calledWith('At least one scope must be selected.'))
flashStub.restore()
})
})

View File

@ -102,14 +102,14 @@ describe DeveloperKey do
end.not_to raise_exception
end
it 'sets "require_scopes" to true if scopes are present' do
key = DeveloperKey.create!(scopes: valid_scopes)
expect(key.require_scopes).to eq true
it 'does not set "require_scopes" to true if scopes are present and require_scopes is false' do
key = DeveloperKey.create!(scopes: valid_scopes, require_scopes: false)
expect(key.require_scopes).to eq false
end
it 'sets "require_scopes" to false if scopes are blank' do
key = DeveloperKey.create!
expect(key.require_scopes).to eq false
it 'does not set "require_scopes" to false if scopes are blank and require_scopes is true' do
key = DeveloperKey.create!(require_scopes: true)
expect(key.require_scopes).to eq true
end
end

View File

@ -64,6 +64,10 @@ describe 'Developer Keys' do
fj("#keys tbody tr.key button:has(svg[name='IconEdit'])").click
end
def click_enforce_scopes
f("[data-automation='enforce_scopes'] div").click
end
def click_scope_group_checkbox
fxpath('//*[@class="scopes-group"]/span[1]/span[2]').click
end
@ -73,7 +77,7 @@ describe 'Developer Keys' do
end
def select_all_readonly_checkbox
fxpath("//*[@class='scopes-list']/span/div/span/span/span/span[2]/div")
fxpath("//*[@class='scopes-list']/span/div/span[1]/span/span/span[2]/div")
end
def all_endpoints_readonly_checkbox_selected?
@ -88,6 +92,7 @@ describe 'Developer Keys' do
f("input[name='developer_key[email]']").send_keys("admin@example.com")
f("textarea[name='developer_key[redirect_uris]']").send_keys("http://example.com")
f("input[name='developer_key[icon_url]']").send_keys("/images/delete.png")
click_enforce_scopes
click_scope_group_checkbox
find_button("Save Key").click
@ -109,6 +114,7 @@ describe 'Developer Keys' do
replace_content(f("input[name='developer_key[email]']"), "admins@example.com")
replace_content(f("textarea[name='developer_key[redirect_uris]']"), "http://b/")
replace_content(f("input[name='developer_key[icon_url]']"), "/images/add.png")
click_enforce_scopes
click_scope_group_checkbox
find_button("Save Key").click
@ -131,6 +137,7 @@ describe 'Developer Keys' do
replace_content(f("input[name='developer_key[email]']"), "admins@example.com")
replace_content(f("input[name='developer_key[redirect_uri]']"), "https://b/")
replace_content(f("input[name='developer_key[icon_url]']"), "/images/add.png")
click_enforce_scopes
click_scope_group_checkbox
find_button("Save Key").click
@ -149,6 +156,7 @@ describe 'Developer Keys' do
get "/accounts/#{Account.default.id}/developer_keys"
click_edit_icon
f("input[name='developer_key[icon_url]']").clear
click_enforce_scopes
click_scope_group_checkbox
find_button("Save Key").click
@ -318,11 +326,12 @@ describe 'Developer Keys' do
Account.default.enable_feature!(:api_token_scoping)
end
def expand_scope_group_by_filter(scope)
def expand_scope_group_by_filter(scope_group)
get "/accounts/#{Account.default.id}/developer_keys"
find_button("Developer Key").click
filter_scopes_by_name(scope)
fj(".toggle-scope-group span:contains('#{scope}')").click
click_enforce_scopes
filter_scopes_by_name(scope_group)
fj(".toggle-scope-group span:contains('#{scope_group}')").click
end
def filter_scopes_by_name(scope)
@ -380,14 +389,13 @@ describe 'Developer Keys' do
end
it "removes scopes from backend developer key via UI" do
skip 'will be fixed in PLAT-3391'
expand_scope_group_by_filter('assignment_groups_api')
click_scope_group_checkbox
find_button("Save Key").click
click_edit_icon
filter_scopes_by_name 'assignment_groups_api'
click_scope_group_checkbox
filter_scopes_by_name 'account_domain_lookups'
click_scope_group_checkbox
dk = DeveloperKey.last
find_button("Save Key").click
wait_for_ajaximations
@ -395,8 +403,10 @@ describe 'Developer Keys' do
end
it "keeps all endpoints read only checkbox checked after save" do
skip 'will be fixed in PLAT-3391'
get "/accounts/#{Account.default.id}/developer_keys"
find_button("Developer Key").click
click_enforce_scopes
select_all_readonly_checkbox.click
find_button("Save Key").click
click_edit_icon