wrap localstorage for LTI tool use
refs INTEROP-7087 flag=lti_platform_storage why * part of IMS LTI Platform Storage proposal * will be used by LTI postMessages * put limits on storage key count and size, partitioned by tool, as defined by the Platform Storage spec test plan * specs Change-Id: I3af9ad8487cccaf510826c50a2b6c478fda75110 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/274217 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Evan Battaglia <ebattaglia@instructure.com> QA-Review: Evan Battaglia <ebattaglia@instructure.com> Product-Review: Xander Moffatt <xmoffatt@instructure.com>
This commit is contained in:
parent
117602ab88
commit
a0ee9c29d6
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 {
|
||||
addToLimit,
|
||||
clearData,
|
||||
clearLimit,
|
||||
getData,
|
||||
getLimit,
|
||||
putData,
|
||||
removeFromLimit
|
||||
} from '../platform_storage'
|
||||
|
||||
describe('platform_storage', () => {
|
||||
const tool_id = 'tool_id'
|
||||
const key = 'key'
|
||||
const value = 'value'
|
||||
|
||||
beforeEach(() => {
|
||||
clearLimit(tool_id)
|
||||
})
|
||||
|
||||
describe('getLimit', () => {
|
||||
it('defaults counts to 0', () => {
|
||||
const limit = getLimit('tool that does not have limit already')
|
||||
expect(limit.keyCount).toBe(0)
|
||||
expect(limit.charCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addToLimit', () => {
|
||||
it('increments key count for tool id', () => {
|
||||
const before = {...getLimit(tool_id)}
|
||||
addToLimit(tool_id, key, value)
|
||||
const after = getLimit(tool_id)
|
||||
expect(after.keyCount).toEqual(before.keyCount + 1)
|
||||
})
|
||||
|
||||
it('adds key and value length to char count for tool', () => {
|
||||
const before = {...getLimit(tool_id)}
|
||||
addToLimit(tool_id, key, value)
|
||||
const after = getLimit(tool_id)
|
||||
expect(after.charCount).toEqual(before.charCount + key.length + value.length)
|
||||
})
|
||||
|
||||
describe('when tool has reached key count limit', () => {
|
||||
beforeEach(() => {
|
||||
getLimit(tool_id).keyCount = 500
|
||||
})
|
||||
|
||||
it('throws a storage_exhaustion error', () => {
|
||||
expect(() => addToLimit(tool_id, key, value)).toThrow('Reached key limit')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when tool has reached char count limit', () => {
|
||||
beforeEach(() => {
|
||||
getLimit(tool_id).charCount = 4096
|
||||
})
|
||||
|
||||
it('throws a storage_exhaustion error', () => {
|
||||
expect(() => addToLimit(tool_id, key, value)).toThrow('Reached byte limit')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeFromLimit', () => {
|
||||
beforeEach(() => {
|
||||
addToLimit(tool_id, 'hello', 'world')
|
||||
addToLimit(tool_id, key, value)
|
||||
})
|
||||
|
||||
it('decrements key count for tool id', () => {
|
||||
const before = {...getLimit(tool_id)}
|
||||
removeFromLimit(tool_id, key, value)
|
||||
const after = getLimit(tool_id)
|
||||
expect(after.keyCount).toEqual(before.keyCount - 1)
|
||||
})
|
||||
|
||||
it('removes key and value length from char count for tool', () => {
|
||||
const before = {...getLimit(tool_id)}
|
||||
removeFromLimit(tool_id, key, value)
|
||||
const after = getLimit(tool_id)
|
||||
expect(after.charCount).toEqual(before.charCount - key.length - value.length)
|
||||
})
|
||||
|
||||
describe('when key count goes below 0', () => {
|
||||
beforeEach(() => {
|
||||
removeFromLimit(tool_id, key, value)
|
||||
removeFromLimit(tool_id, key, value)
|
||||
removeFromLimit(tool_id, key, value)
|
||||
})
|
||||
|
||||
it('resets key count to 0', () => {
|
||||
const {keyCount} = getLimit(tool_id)
|
||||
expect(keyCount).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when char count goes below 0', () => {
|
||||
beforeEach(() => {
|
||||
removeFromLimit(tool_id, key, value)
|
||||
removeFromLimit(tool_id, key, value)
|
||||
removeFromLimit(tool_id, key, value)
|
||||
})
|
||||
|
||||
it('resets char count to 0', () => {
|
||||
const {charCount} = getLimit(tool_id)
|
||||
expect(charCount).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('putData', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window.localStorage, 'setItem')
|
||||
})
|
||||
|
||||
it('namespaces key with tool id', () => {
|
||||
putData(tool_id, key, value)
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
`lti|platform_storage|${tool_id}|${key}`,
|
||||
value
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getData', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window.localStorage, 'getItem')
|
||||
putData(tool_id, key, value)
|
||||
})
|
||||
|
||||
it('namespaces key with tool id', () => {
|
||||
getData(tool_id, key)
|
||||
expect(window.localStorage.getItem).toHaveBeenCalledWith(
|
||||
`lti|platform_storage|${tool_id}|${key}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearData', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window.localStorage, 'removeItem')
|
||||
})
|
||||
|
||||
describe('when key does not exist', () => {
|
||||
it('does nothing', () => {
|
||||
expect(window.localStorage.removeItem).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when key is already stored', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window.localStorage, 'getItem').mockImplementation(() => value)
|
||||
putData(tool_id, key, value)
|
||||
})
|
||||
|
||||
it('namespaces key with tool id', () => {
|
||||
clearData(tool_id, key)
|
||||
expect(window.localStorage.removeItem).toHaveBeenCalledWith(
|
||||
`lti|platform_storage|${tool_id}|${key}`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 STORAGE_CHAR_LIMIT = 4096 // IMS minimum storage limit is 4096 bytes so 4096 chars is more than enough
|
||||
const STORAGE_KEY_LIMIT = 500
|
||||
|
||||
const limits = {}
|
||||
|
||||
const createLimit = tool_id => {
|
||||
if (!limits[tool_id]) {
|
||||
limits[tool_id] = {keyCount: 0, charCount: 0}
|
||||
}
|
||||
}
|
||||
|
||||
const getLimit = tool_id => {
|
||||
createLimit(tool_id)
|
||||
return limits[tool_id]
|
||||
}
|
||||
|
||||
const clearLimit = tool_id => {
|
||||
delete limits[tool_id]
|
||||
}
|
||||
|
||||
const addToLimit = (tool_id, key, value) => {
|
||||
createLimit(tool_id)
|
||||
const length = key.length + value.length
|
||||
|
||||
if (limits[tool_id].keyCount >= STORAGE_KEY_LIMIT) {
|
||||
const e = new Error('Reached key limit for tool')
|
||||
e.code = 'storage_exhaustion'
|
||||
throw e
|
||||
}
|
||||
|
||||
if (limits[tool_id].charCount + length > STORAGE_CHAR_LIMIT) {
|
||||
const e = new Error('Reached byte limit for tool')
|
||||
e.code = 'storage_exhaustion'
|
||||
throw e
|
||||
}
|
||||
|
||||
limits[tool_id].keyCount++
|
||||
limits[tool_id].charCount += length
|
||||
}
|
||||
|
||||
const removeFromLimit = (tool_id, key, value) => {
|
||||
limits[tool_id].keyCount--
|
||||
limits[tool_id].charCount -= key.length + value.length
|
||||
|
||||
if (limits[tool_id].keyCount < 0) {
|
||||
limits[tool_id].keyCount = 0
|
||||
}
|
||||
if (limits[tool_id].charCount < 0) {
|
||||
limits[tool_id].charCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
const getKey = (tool_id, key) => `lti|platform_storage|${tool_id}|${key}`
|
||||
|
||||
const putData = (tool_id, key, value) => {
|
||||
addToLimit(tool_id, key, value)
|
||||
window.localStorage.setItem(getKey(tool_id, key), value)
|
||||
}
|
||||
|
||||
const getData = (tool_id, key) => window.localStorage.getItem(getKey(tool_id, key))
|
||||
|
||||
const clearData = (tool_id, key) => {
|
||||
const value = getData(tool_id, key)
|
||||
if (value) {
|
||||
removeFromLimit(tool_id, key, value)
|
||||
window.localStorage.removeItem(getKey(tool_id, key))
|
||||
}
|
||||
}
|
||||
|
||||
export {getLimit, clearLimit, addToLimit, removeFromLimit, clearData, getData, putData}
|
Loading…
Reference in New Issue