Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor WalletPersistence to use Platform Agnostic Storage Provider #337

Merged
merged 10 commits into from
Jul 25, 2021
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