Skip to content

Commit

Permalink
Refactor WalletPersistence to use Platform Agnostic Storage Provider (#…
Browse files Browse the repository at this point in the history
…337)

* PersistentWallet context to use SecureStore

* mock native secure storage (no web variant)

* remove platform.select in code, create different platform extension for secure store

* updated README.md

* implemented platform agnostic storage provider

* refactor WalletPersistence to just set/get

* WalletPersistence to store wallet separately to work with 2kb constraint

* added global mocking

* playground:start will kill all container before starting

* fixed ../api

Co-authored-by: ivan <[email protected]>
  • Loading branch information
fuxingloh and ivan authored Jul 25, 2021
1 parent 6038722 commit 6acc4de
Show file tree
Hide file tree
Showing 18 changed files with 501 additions and 189 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ those directories.

Directory | Description
------------------------|-------------
`/.github` | workflow for shift left automation
`/app/api` | api & middlewares for application, non-UI logic
`/.github` | GitHub Workflow for shift left automation
`/app/api` | API and middlewares logic for application, for non-UI logic only
`/app/assets` | assets of the project that can be loaded at startup
`/app/components` | top level components for a shared design language
`/app/components` | top level components for a atomic shared design language
`/app/contexts` | shared contexts for application, non-UI logic
`/app/hooks` | shared hooks for application, for UI logic only
`/app/screens` | screens hierarchy tree matching directory hierarchy tree
Expand Down
5 changes: 5 additions & 0 deletions __mocks__/expo-secure-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
getItemAsync: jest.fn(),
setItemAsync: jest.fn(),
deleteItemAsync: jest.fn()
}
3 changes: 3 additions & 0 deletions app/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './wallet/persistence'
export * from './storage'
export * from './logging'
61 changes: 0 additions & 61 deletions app/api/storage.test.ts

This file was deleted.

59 changes: 0 additions & 59 deletions app/api/storage.ts

This file was deleted.

138 changes: 138 additions & 0 deletions app/api/storage/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import ExpoSecureStore from "expo-secure-store";
import { EnvironmentNetwork } from "../../environment";
import { StorageAPI } from "./index";

// TODO(fuxingloh): 'jest-expo' only test native (provider.native.ts) by default, need to improve testing capability

const getItem = jest.spyOn(ExpoSecureStore, 'getItemAsync')
const setItem = jest.spyOn(ExpoSecureStore, 'setItemAsync')
const removeItem = jest.spyOn(ExpoSecureStore, 'deleteItemAsync')

beforeEach(() => {
jest.clearAllMocks()
})

describe('network', () => {
it('should default to Local Playground', async () => {
expect(await StorageAPI.getNetwork()).toBe(EnvironmentNetwork.LocalPlayground)
expect(getItem).toBeCalled()
});

it('should call setItem', async () => {
await StorageAPI.setNetwork(EnvironmentNetwork.RemotePlayground)
expect(setItem).toBeCalled()
});

it('should get Local Playground', async () => {
getItem.mockResolvedValue(EnvironmentNetwork.LocalPlayground)
expect(await StorageAPI.getNetwork()).toBe(EnvironmentNetwork.LocalPlayground)
});

it('should get Local Playground', async () => {
getItem.mockResolvedValue(EnvironmentNetwork.RemotePlayground)
expect(await StorageAPI.getNetwork()).toBe(EnvironmentNetwork.RemotePlayground)
});

it('should errored as network is not part of environment', async () => {
await expect(StorageAPI.setNetwork(EnvironmentNetwork.MainNet))
.rejects.toThrow('network is not part of environment')
});
})

describe('item', () => {
beforeEach(() => {
getItem.mockResolvedValue(EnvironmentNetwork.RemotePlayground)
})

it('should getItem with environment and network prefixed key', async () => {
await StorageAPI.getItem('get')
expect(getItem).toBeCalledTimes(2)
expect(getItem).toBeCalledWith('Development.NETWORK')
expect(getItem).toBeCalledWith('Development.Remote Playground.get')
})

it('should setItem with environment and network prefixed key', async () => {
await StorageAPI.setItem('set', 'value')
expect(setItem).toBeCalledWith('Development.Remote Playground.set', 'value')
})

it('should removeItem with environment and network prefixed key', async () => {
await StorageAPI.removeItem('remove')
expect(removeItem).toBeCalledWith('Development.Remote Playground.remove')
})
})

describe('byte length validation', () => {
beforeEach(() => {
getItem.mockResolvedValue(EnvironmentNetwork.LocalPlayground)
})

it('should set if 1 byte length', async () => {
await StorageAPI.setItem('key', '1')
expect(setItem).toBeCalledWith('Development.Local Playground.key', '1')
})

it('should set if 100 byte length', async () => {
await StorageAPI.setItem('key', '0000000000100000000020000000003000000000400000000050000000006000000000700000000080000000009000000000')
expect(setItem).toBeCalledWith('Development.Local Playground.key', '0000000000100000000020000000003000000000400000000050000000006000000000700000000080000000009000000000')
})

function generateText (length: number, sequence: string) {
let text = ''
for (let i = 0; i < length; i++) {
text += sequence
}
return text
}

it('should set if 2047 byte length', async () => {
const text = generateText(2047, '0')

await StorageAPI.setItem('key', text)
expect(setItem).toBeCalledWith('Development.Local Playground.key', text)
})

it('should error if 2048 byte length', async () => {
const text = generateText(2048, '0')
const promise = StorageAPI.setItem('key', text)
await expect(promise).rejects.toThrow('value exceed 2048 bytes, unable to setItem')
})

it('should error if 2049 byte length', async () => {
const text = generateText(2049, '0')

const promise = StorageAPI.setItem('key', text)
await expect(promise).rejects.toThrow('value exceed 2048 bytes, unable to setItem')
})

describe('utf-8 1-4 char', () => {
it('should set if 3 byte length utf-8', async () => {
const text = generateText(1, '好')

await StorageAPI.setItem('key', text)
expect(setItem).toBeCalledWith('Development.Local Playground.key', text)
})

it('should set if 2046 byte length utf-8', async () => {
const text = generateText(682, '好')

await StorageAPI.setItem('key', text)
expect(setItem).toBeCalledWith('Development.Local Playground.key', text)
})


it('should error if 2049 byte length', async () => {
const text = generateText(683, '好')

const promise = StorageAPI.setItem('key', text)
await expect(promise).rejects.toThrow('value exceed 2048 bytes, unable to setItem')
})

it('should error if 3072 byte length', async () => {
const text = generateText(1024, '好')

const promise = StorageAPI.setItem('key', text)
await expect(promise).rejects.toThrow('value exceed 2048 bytes, unable to setItem')
})
})
})
71 changes: 71 additions & 0 deletions app/api/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { EnvironmentNetwork, getEnvironment } from '../../environment'
import { StorageProvider } from './provider'

/**
* @return EnvironmentNetwork if invalid, will be set to `networks[0]`
*/
async function getNetwork (): Promise<EnvironmentNetwork> {
const env = getEnvironment()
const network = await StorageProvider.getItem(`${env.name}.NETWORK`)

if ((env.networks as any[]).includes(network)) {
return network as EnvironmentNetwork
}

await setNetwork(env.networks[0])
return env.networks[0]
}

/**
* @param network {EnvironmentNetwork} with set with 'environment' prefixed
*/
async function setNetwork (network: EnvironmentNetwork): Promise<void> {
const env = getEnvironment()

if (!env.networks.includes(network)) {
throw new Error('network is not part of environment')
}

await StorageProvider.setItem(`${env.name}.NETWORK`, network)
}

async function getKey (key: string): Promise<string> {
const env = getEnvironment()
const network = await getNetwork()
return `${env.name}.${network}.${key}`
}

/**
* @param key {string} of item with 'environment' and 'network' prefixed
* @return {string | null}
*/
async function getItem (key: string): Promise<string | null> {
return await StorageProvider.getItem(await getKey(key))
}

/**
* @param key {string} of item with 'environment' and 'network' prefixed
* @param value {string} to set
* @throws Error when byte length exceed 2048 bytes
*/
async function setItem (key: string, value: string): Promise<void> {
if (Buffer.byteLength(value, 'utf-8') >= 2048) {
throw new Error('value exceed 2048 bytes, unable to setItem')
}
return await StorageProvider.setItem(await getKey(key), value)
}

/**
* @param key {string} of item with 'environment' and 'network' prefixed
*/
async function removeItem (key: string): Promise<void> {
await StorageProvider.removeItem(await getKey(key))
}

export const StorageAPI = {
getNetwork,
setNetwork,
getItem,
setItem,
removeItem
}
15 changes: 15 additions & 0 deletions app/api/storage/provider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Provider } from './provider'

/**
* Provider storage interface for platform agnostic storage provider
*/
export interface IStorage {
getItem: (key: string) => Promise<string | null>
setItem: (key: string, value: string) => Promise<void>
removeItem: (key: string) => Promise<void>
}

/**
* Platform agnostic storage provider
*/
export const StorageProvider = Provider
8 changes: 8 additions & 0 deletions app/api/storage/provider/provider.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ExpoSecureStore from 'expo-secure-store'
import { IStorage } from './index'

export const Provider: IStorage = {
getItem: ExpoSecureStore.getItemAsync,
setItem: ExpoSecureStore.setItemAsync,
removeItem: ExpoSecureStore.deleteItemAsync
}
11 changes: 11 additions & 0 deletions app/api/storage/provider/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import { IStorage } from './index'

/**
* NOTE: We don't officially support web platform yet.
*/
export const Provider: IStorage = {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
removeItem: AsyncStorage.removeItem
}
Loading

0 comments on commit 6acc4de

Please sign in to comment.