Skip to content

Commit

Permalink
fix: Storage.prototype.remove and Storage.prototype.removeAll to hand…
Browse files Browse the repository at this point in the history
…le namespace correctly (#32)

* Fix Storage.prototype.remove to handle namespaces. And add coverage for basic CRUD operations.

* Add remove to SecureStorage and test for CRUD ops

* reverting accidental change in package.json

* Use mts instead for typing

---------

Co-authored-by: L❤️ ☮️ ✋ <[email protected]>
  • Loading branch information
shuntksh and louisgv committed May 10, 2023
1 parent 590a06c commit 3a25b34
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 18 deletions.
1 change: 1 addition & 0 deletions jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const config = {
clearMocks: true,
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.mts"],
extensionsToTreatAsEsm: [".ts"],
globals: {
chrome: {
Expand Down
67 changes: 67 additions & 0 deletions jest.setup.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { jest } from "@jest/globals"

/**
* Mimic the webcrypto API without implementing the actual encryption
* algorithms. Only the mock implementations used by the SecureStorage
*/
export const cryptoMock = {
subtle: {
importKey: jest.fn(),
deriveKey: jest.fn(),
decrypt: jest.fn(),
encrypt: jest.fn(),
digest: jest.fn()
},
getRandomValues: jest.fn()
}

cryptoMock.subtle.importKey.mockImplementation(
(format, keyData, algorithm, extractable, keyUsages) => {
return Promise.resolve({
format,
keyData,
algorithm,
extractable,
keyUsages
})
}
)

cryptoMock.subtle.deriveKey.mockImplementation(
(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) => {
return Promise.resolve({
algorithm,
baseKey,
derivedKeyAlgorithm,
extractable,
keyUsages
})
}
)

cryptoMock.subtle.decrypt.mockImplementation((_, __, data: ArrayBufferLike) => {
return Promise.resolve(new Uint8Array(data))
})

cryptoMock.subtle.encrypt.mockImplementation((_, __, data: ArrayBufferLike) => {
return Promise.resolve(new Uint8Array(data))
})

cryptoMock.subtle.digest.mockImplementation((_, __) => {
return Promise.resolve(new Uint8Array([0x01, 0x02, 0x03, 0x04]))
})

cryptoMock.getRandomValues.mockImplementation((array: Array<any>) => {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256)
}
return array
})

// The globalThis does not define crypto by default
Object.defineProperty(globalThis, "crypto", {
value: cryptoMock,
writable: true,
enumerable: true,
configurable: true
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@plasmohq/storage",
"version": "1.5.0",
"version": "1.6.0",
"description": "Safely and securely store data and share them across your extension and websites",
"type": "module",
"module": "./src/index.ts",
Expand Down
169 changes: 156 additions & 13 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ const { useStorage } = await import("~hook")

const mockStorage = {
data: {},
get(key: string) {
get(key?: string) {
if (!key) {
return { ...this.data }
}
return {
[key]: this.data[key]
}
},
set(key = "", value = "") {
this.data[key] = value
},
remove(key: string) {
delete this.data[key]
},
clear() {
this.data = {}
}
Expand All @@ -31,14 +37,23 @@ beforeEach(() => {
jest.fn().mockReset()
})

const createStorageMock = () => {
export const createStorageMock = (): {
mockStorage: typeof mockStorage
addListener: jest.Mock
removeListener: jest.Mock
getTriggers: jest.Mock
setTriggers: jest.Mock
removeTriggers: jest.Mock
} => {
let onChangedCallback: StorageWatchEventListener

const mockOutput = {
mockStorage,
addListener: jest.fn(),
removeListener: jest.fn(),
getTriggers: jest.fn(),
setTriggers: jest.fn()
setTriggers: jest.fn(),
removeTriggers: jest.fn()
}

const storage: typeof chrome.storage = {
Expand All @@ -63,18 +78,34 @@ const createStorageMock = () => {
Object.entries(changes).forEach(([key, value]) => {
mockStorage.set(key, value)

onChangedCallback(
{
[key]: {
oldValue: undefined,
newValue: value
}
},
"sync"
)
onChangedCallback &&
onChangedCallback(
{
[key]: {
oldValue: undefined,
newValue: value
}
},
"sync"
)
})
}
)
),
//@ts-ignore
remove: mockOutput.removeTriggers.mockImplementation((key: string) => {
mockStorage.remove(key)

onChangedCallback &&
onChangedCallback(
{
[key]: {
oldValue: mockStorage.data[key],
newValue: undefined
}
},
"sync"
)
})
}
}

Expand Down Expand Up @@ -220,3 +251,115 @@ describe("watch/unwatch", () => {
expect(storageMock.removeListener).toHaveBeenCalled()
})
})

// Create a new describe block for CRUD operations with namespace
describe("Storage - Basic CRUD operations with namespace", () => {
// Declare the storage and namespace variables
let storage = new Storage()
let storageMock: ReturnType<typeof createStorageMock>
const namespace = "testNamespace:"

// Initialize storage and storageMock before each test case
beforeEach(() => {
storageMock = createStorageMock()
storage = new Storage()
storage.setNamespace(namespace)
})

// Test set operation with namespace
test("set operation", async () => {
// Test data
const testKey = "key"
const testValue = "value"

// Perform set operation
await storage.set(testKey, testValue)

// Check if storageMock.setTriggers is called with the correct parameters
expect(storageMock.setTriggers).toHaveBeenCalledWith({
[`${namespace}${testKey}`]: JSON.stringify(testValue)
})
})

// Test get operation with namespace
test("get operation", async () => {
// Test data
const testKey = "key"
const testValue = "value"

// Perform set operation
await storage.set(testKey, testValue)

// Perform get operation
const getValue = await storage.get(testKey)

// Check if storageMock.getTriggers is called with the correct parameter
expect(storageMock.getTriggers).toHaveBeenCalledWith(
`${namespace}${testKey}`
)

// Check if the returned value is correct
expect(getValue).toEqual(testValue)
})

// Test getAll operation with namespace
test("getAll operation", async () => {
// Test data
const testKey1 = "key1"
const testValue1 = "value1"
const testKey2 = "key2"
const testValue2 = "value2"

// Perform set operations for two keys
await storage.set(testKey1, testValue1)
await storage.set(testKey2, testValue2)

// Perform getAll operation
const allData = await storage.getAll()

// Check if the returned object has the correct keys
// and ensure the keys are without namespace
expect(Object.keys(allData)).toEqual([testKey1, testKey2])
})

// Test remove operation with namespace
test("remove operation", async () => {
// Test data
const testKey = "key"
const testValue = "value"

// Perform set operation
await storage.set(testKey, testValue)

// Perform remove operation
await storage.remove(testKey)

// Check if storageMock.removeListener is called with the correct parameter
expect(storageMock.removeTriggers).toHaveBeenCalledWith(
`${namespace}${testKey}`
)
})

// Test removeAll operation with namespace
test("removeAll operation", async () => {
// Test data
const testKey1 = "key1"
const testValue1 = "value1"
const testKey2 = "key2"
const testValue2 = "value2"

// Perform set operations for two keys
await storage.set(testKey1, testValue1)
await storage.set(testKey2, testValue2)

// Perform removeAll operation
await storage.removeAll()

expect(storageMock.removeTriggers).toHaveBeenCalledWith(
`${namespace}${testKey1}`
)
expect(storageMock.removeTriggers).toHaveBeenCalledWith(
`${namespace}${testKey2}`
)
})
})
14 changes: 11 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export abstract class BaseStorage {
await this.#primaryClient.clear()
}

remove = async (key: string) => {
protected rawRemove = async (key: string) => {
if (this.isCopied(key)) {
this.#secondaryClient?.removeItem(key)
}
Expand All @@ -229,10 +229,11 @@ export abstract class BaseStorage {
}

removeAll = async () => {
const allData = await this.getAll()
// Using rawGetAll to retrieve all keys with namespace
const allData = await this.rawGetAll()
const keyList = Object.keys(allData)

await Promise.all(keyList.map(this.remove))
await Promise.all(keyList.map(this.rawRemove))
}

watch = (callbackMap: StorageCallbackMap) => {
Expand Down Expand Up @@ -329,6 +330,8 @@ export abstract class BaseStorage {
*/
abstract set: (key: string, rawValue: any) => Promise<string>

abstract remove: (key: string) => Promise<void>

/**
* Parse the value into its original form from storage raw value.
*/
Expand All @@ -353,6 +356,11 @@ export class Storage extends BaseStorage {
return this.rawSet(nsKey, value)
}

remove = async (key: string) => {
const nsKey = this.getNamespacedKey(key)
return this.rawRemove(nsKey)
}

setNamespace = (namespace: string) => {
this.keyNamespace = namespace
}
Expand Down
Loading

0 comments on commit 3a25b34

Please sign in to comment.