diff --git a/jest.config.ts b/jest.config.ts index fc34ebc5..cd3e9527 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -6,7 +6,7 @@ const config: Config = { maxWorkers: 1, // Fixed https://github.com/jestjs/jest/issues/11617#issuecomment-1028651059 rootDir: './src', preset: 'ts-jest', - resetMocks: true, + resetMocks: false, testEnvironment: 'node', testMatch: [ '**/*.spec.ts' ], collectCoverageFrom: [ 'src/**/*.ts' ], diff --git a/src/index.ts b/src/index.ts index 22ae44d1..301d978f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,8 @@ import { constants } from './utils' export * from './utils/enums' export { createContract } from './contracts' export { default as StakeWiseSDK } from './StakeWiseSDK' -export { wrapAbortPromise, AbortPromise } from './modules/gql-module' +export { default as localStorage } from './modules/local-storage' +export { wrapAbortPromise, AbortPromise, AbortRequest } from './modules/gql-module' export { configs, diff --git a/src/methods/utils/getListVariables.ts b/src/methods/utils/getListVariables.ts new file mode 100644 index 00000000..30dcafc8 --- /dev/null +++ b/src/methods/utils/getListVariables.ts @@ -0,0 +1,66 @@ +import { isAddress } from 'ethers' +import { validateArgs } from '../../utils' +import { StakeWiseSubgraphGraph } from '../../types/graphql/subgraph' + + +export type GetListVariablesInput = { + vaultAddress: string + orderDirection?: StakeWiseSubgraphGraph.OrderDirection + search?: string + limit?: number + skip?: number + addressIn?: string[] +} + +const validateList = (addressIn: string[]) => { + const isValid = addressIn.every((address) => isAddress(address)) + + if (!isValid) { + throw new Error('The "addressIn" argument must be an array of valid addresses') + } +} + +const getListVariables = (input: GetListVariablesInput): T => { + const { vaultAddress, orderDirection, search, limit, skip, addressIn } = input + + validateArgs.address({ vaultAddress }) + + if (typeof skip !== 'undefined') { + validateArgs.number({ skip }) + } + + if (typeof limit !== 'undefined') { + validateArgs.number({ limit }) + } + + if (typeof search !== 'undefined') { + validateArgs.string({ search }) + } + + if (typeof orderDirection !== 'undefined') { + if (![ 'asc', 'desc' ].includes(orderDirection)) { + throw new Error(`The "orderDirection" argument must be "asc" or "desc"`) + } + } + + if (typeof addressIn !== 'undefined') { + validateArgs.array({ addressIn }) + validateList(addressIn as string[]) + } + + const vault = vaultAddress.toLowerCase() + + const where = search + ? { vault, address_in: addressIn, address_contains: search.toLowerCase() } + : { vault, address_in: addressIn } + + return { + where, + skip: skip || 0, + limit: limit || 100, + orderDirection: orderDirection || 'desc', + } as T +} + + +export default getListVariables diff --git a/src/methods/utils/index.ts b/src/methods/utils/index.ts index 0cf5063c..13873f11 100644 --- a/src/methods/utils/index.ts +++ b/src/methods/utils/index.ts @@ -1,6 +1,8 @@ export type { BaseInput } from './types' export { default as getFiatRates } from './getFiatRates' export { default as getTransactions } from './getTransactions' +export type { GetListVariablesInput } from './getListVariables' +export { default as getListVariables } from './getListVariables' export { default as getFiatRatesByDay } from './getFiatRatesByDay' export { default as getStakewiseStats } from './getStakewiseStats' export { default as getPermitSignature } from './getPermitSignature' diff --git a/src/methods/vault/requests/getBlocklist/index.ts b/src/methods/vault/requests/getBlocklist/index.ts index dca971b9..d6cb7ebf 100644 --- a/src/methods/vault/requests/getBlocklist/index.ts +++ b/src/methods/vault/requests/getBlocklist/index.ts @@ -1,71 +1,23 @@ -import { isAddress } from 'ethers' import type { BlocklistAccountsQueryVariables, BlocklistAccountsQueryPayload } from '../../../../graphql/subgraph/vault' -import { apiUrls, validateArgs } from '../../../../utils' +import { apiUrls } from '../../../../utils' import graphql from '../../../../graphql' import { ModifiedBlocklist } from './types' import modifyBlocklist from './modifyBlocklist' +import { getListVariables, GetListVariablesInput } from '../../../utils' -type GetBlocklistInput = { - vaultAddress: string - orderDirection?: BlocklistAccountsQueryVariables['orderDirection'] - search?: string - limit?: number - skip?: number - addressIn?: BlocklistAccountsQueryVariables['where']['address_in'] +type GetBlocklistInput = GetListVariablesInput & { options: StakeWise.Options } -const validateList = (addressIn: string[]) => { - const isValid = addressIn.every((address) => isAddress(address)) - - if (!isValid) { - throw new Error('The "addressIn" argument must be an array of valid addresses') - } -} - const getBlocklist = (input: GetBlocklistInput) => { - const { vaultAddress, orderDirection, search, limit, skip, addressIn, options } = input - - validateArgs.address({ vaultAddress }) - - if (typeof skip !== 'undefined') { - validateArgs.number({ skip }) - } - - if (typeof limit !== 'undefined') { - validateArgs.number({ limit }) - } - - if (typeof search !== 'undefined') { - validateArgs.string({ search }) - } - - if (typeof orderDirection !== 'undefined') { - if (![ 'asc', 'desc' ].includes(orderDirection)) { - throw new Error(`The "orderDirection" argument must be "asc" or "desc"`) - } - } - - if (typeof addressIn !== 'undefined') { - validateArgs.array({ addressIn }) - validateList(addressIn as string[]) - } - - const vault = vaultAddress.toLowerCase() + const { options, ...rest } = input - const where = search - ? { vault, address_in: addressIn, address_contains: search.toLowerCase() } as BlocklistAccountsQueryVariables['where'] - : { vault, address_in: addressIn } as BlocklistAccountsQueryVariables['where'] + const variables = getListVariables(rest) return graphql.subgraph.vault.fetchBlocklistAccountsQuery({ url: apiUrls.getSubgraphqlUrl(options), - variables: { - where, - skip: skip || 0, - limit: limit || 100, - orderDirection: orderDirection || 'desc', - }, + variables, modifyResult: (data: BlocklistAccountsQueryPayload) => modifyBlocklist({ data }), }) } diff --git a/src/methods/vault/requests/getWhitelist/index.ts b/src/methods/vault/requests/getWhitelist/index.ts index 4cb64721..a231e387 100644 --- a/src/methods/vault/requests/getWhitelist/index.ts +++ b/src/methods/vault/requests/getWhitelist/index.ts @@ -1,71 +1,23 @@ -import { isAddress } from 'ethers' import type { WhitelistAccountsQueryVariables, WhitelistAccountsQueryPayload } from '../../../../graphql/subgraph/vault' -import { apiUrls, validateArgs } from '../../../../utils' +import { apiUrls } from '../../../../utils' import graphql from '../../../../graphql' import { ModifiedWhitelist } from './types' import modifyWhitelist from './modifyWhitelist' +import { getListVariables, GetListVariablesInput } from '../../../utils' -type GetWhitelistInput = { - vaultAddress: string - orderDirection?: WhitelistAccountsQueryVariables['orderDirection'] - search?: string - limit?: number - skip?: number - addressIn?: WhitelistAccountsQueryVariables['where']['address_in'] +type GetWhitelistInput = GetListVariablesInput & { options: StakeWise.Options } -const validateList = (addressIn: string[]) => { - const isValid = addressIn.every((address) => isAddress(address)) - - if (!isValid) { - throw new Error('The "addressIn" argument must be an array of valid addresses') - } -} - const getWhitelist = (input: GetWhitelistInput) => { - const { vaultAddress, orderDirection, search, limit, skip, addressIn, options } = input - - validateArgs.address({ vaultAddress }) - - if (typeof skip !== 'undefined') { - validateArgs.number({ skip }) - } - - if (typeof limit !== 'undefined') { - validateArgs.number({ limit }) - } - - if (typeof search !== 'undefined') { - validateArgs.string({ search }) - } - - if (typeof orderDirection !== 'undefined') { - if (![ 'asc', 'desc' ].includes(orderDirection)) { - throw new Error(`The "orderDirection" argument must be "asc" or "desc"`) - } - } - - if (typeof addressIn !== 'undefined') { - validateArgs.array({ addressIn }) - validateList(addressIn as string[]) - } - - const vault = vaultAddress.toLowerCase() + const { options, ...rest } = input - const where = search - ? { vault, address_in: addressIn, address_contains: search.toLowerCase() } as WhitelistAccountsQueryVariables['where'] - : { vault, address_in: addressIn } as WhitelistAccountsQueryVariables['where'] + const variables = getListVariables(rest) return graphql.subgraph.vault.fetchWhitelistAccountsQuery({ url: apiUrls.getSubgraphqlUrl(options), - variables: { - where, - skip: skip || 0, - limit: limit || 100, - orderDirection: orderDirection || 'desc', - }, + variables, modifyResult: (data: WhitelistAccountsQueryPayload) => modifyWhitelist({ data }), }) } diff --git a/src/modules/gql-module/abortCallback.ts b/src/modules/gql-module/abortCallback.ts index b5040f5b..0b18280c 100644 --- a/src/modules/gql-module/abortCallback.ts +++ b/src/modules/gql-module/abortCallback.ts @@ -14,8 +14,6 @@ class AbortCallback { then(onSuccess: (data: any) => any, onError?: (error: any) => any) { if (this.isAborted) { - const dummyPromise = new Promise(() => {}) - return new AbortCallback(dummyPromise, this.onAbort) } diff --git a/src/modules/gql-module/abortPromise.spec.ts b/src/modules/gql-module/abortPromise.spec.ts new file mode 100644 index 00000000..5cf96a9f --- /dev/null +++ b/src/modules/gql-module/abortPromise.spec.ts @@ -0,0 +1,157 @@ +import AbortPromise from './abortPromise' +import AbortCallback from './abortCallback' + + +describe('AbortPromise', () => { + + it('resolves the promise', async () => { + const mock = 'response data' + const abortPromise = new AbortPromise((resolve) => { + resolve(mock) + }) + + const result = await abortPromise + + expect(result).toEqual(mock) + }) + + it('rejects the promise', async () => { + const mock = 'error' + const abortPromise = new AbortPromise((resolve, reject) => { + reject(mock) + }) + + await expect(abortPromise).rejects.toEqual(mock) + }) + + it('calls then on promise resolve', async () => { + const mockThenFn = jest.fn() + const mockCatchFn = jest.fn() + + const abortPromise = new AbortPromise((resolve, reject) => { + resolve(null) + }) + .then(mockThenFn, mockCatchFn) + + await abortPromise + + expect(mockThenFn).toBeCalledTimes(1) + expect(mockCatchFn).toBeCalledTimes(0) + }) + + it('calls catch on promise reject', async () => { + const mockThenFn = jest.fn() + const mockCatchFn = jest.fn() + + const abortPromise = new AbortPromise((resolve, reject) => { + reject(null) + }) + .then(mockThenFn, mockCatchFn) + + await abortPromise + + expect(mockThenFn).toBeCalledTimes(0) + expect(mockCatchFn).toBeCalledTimes(1) + }) + + it('doesn\'t resolve aborted promise', async () => { + const mockThenFn = jest.fn() + const mockCatchFn = jest.fn() + + const abortPromise = new AbortPromise((resolve, reject) => { + resolve(null) + }) + .then(mockThenFn, mockCatchFn) + + abortPromise.abort() + + expect(mockThenFn).toBeCalledTimes(0) + expect(mockCatchFn).toBeCalledTimes(0) + }) + + it('doesn\'t reject aborted promise', async () => { + const mockThenFn = jest.fn() + const mockCatchFn = jest.fn() + + const abortPromise = new AbortPromise((resolve, reject) => { + reject(null) + }) + .then(mockThenFn, mockCatchFn) + + abortPromise.abort() + + expect(mockThenFn).toBeCalledTimes(0) + expect(mockCatchFn).toBeCalledTimes(0) + }) + + it('resolves multiple promises in "all" method', async () => { + const mock1 = 'response data 1' + const mock2 = 'response data 2' + + const promise1 = new Promise((resolve) => resolve(mock1)) + const promise2 = new Promise((resolve) => resolve(mock2)) + + const result = await AbortPromise.all([ promise1, promise2 ]) + + expect(result).toEqual([ mock1, mock2 ]) + }) + + it('resolves promise in "race" method', async () => { + const mock = 'response data' + + const promise1 = new Promise((resolve) => resolve(mock)) + const promise2 = new Promise(() => {}) + + const result = await AbortPromise.race([ promise1, promise2 ]) + + expect(result).toEqual(mock) + }) + + it('resolves multiple abort promises in "all" method', async () => { + const mock1 = 'response data 1' + const mock2 = 'response data 2' + + const promise1 = new AbortPromise((resolve) => resolve(mock1)) + const promise2 = new AbortPromise((resolve) => resolve(mock2)) + + const result = await AbortPromise.all([ promise1, promise2 ]) + + expect(result).toEqual([ mock1, mock2 ]) + }) + + it('doesn\'t resolve multiple promises in "all" method on abort', async () => { + const abort1 = jest.fn() + const abort2 = jest.fn() + const mockThenFn = jest.fn() + const mockCatchFn = jest.fn() + + const dummyPromise = new Promise(() => {}) + + const promise1 = new AbortCallback(dummyPromise, abort1) + const promise2 = new AbortCallback(dummyPromise, abort2) + + // @ts-ignore + const promise = AbortPromise.all([ promise1, promise2 ]) + .then(mockThenFn, mockCatchFn) + + promise.abort() + + expect(abort1).toBeCalledTimes(1) + expect(abort2).toBeCalledTimes(1) + expect(mockThenFn).toBeCalledTimes(0) + expect(mockCatchFn).toBeCalledTimes(0) + }) + + it('doesn\'t reject multiple promises in "all" method on abort', async () => { + const mockThenFn = jest.fn() + const mockCatchFn = jest.fn() + + const promise = AbortPromise.all([ Promise.reject(), Promise.reject() ]) + .then(mockThenFn, mockCatchFn) + + promise.abort() + + expect(mockThenFn).toBeCalledTimes(0) + expect(mockCatchFn).toBeCalledTimes(0) + }) +}) diff --git a/src/modules/gql-module/abortRequest.spec.ts b/src/modules/gql-module/abortRequest.spec.ts new file mode 100644 index 00000000..b56844c4 --- /dev/null +++ b/src/modules/gql-module/abortRequest.spec.ts @@ -0,0 +1,226 @@ +import fetchMock from 'jest-fetch-mock' + +import AbortRequest from './abortRequest' + + +beforeEach(() => { + fetchMock.enableMocks() +}) + +afterEach(() => { + fetchMock.resetMocks() +}) + +describe('AbortRequest', () => { + it('resolves the request', async () => { + const mock = 'response data' + fetchMock.mockResponse(JSON.stringify({ data: mock })) + + const abortRequest = new AbortRequest('https://example.com', { + method: 'GET', + onSuccess: (data) => data, + }) + + const result = await abortRequest + + expect(result).toEqual(mock) + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it('resolves modified data', async () => { + const mock = { test: 'response data' } + fetchMock.mockResponse(JSON.stringify({ data: mock })) + + const abortRequest = new AbortRequest<{ test: string }, string>('https://example.com', { + method: 'GET', + onSuccess: (data) => data.test, + }) + + const result = await abortRequest + + expect(result).toEqual(mock.test) + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it('rejects the request', async () => { + fetchMock.mockReject(new Error('Request failed')) + + const abortRequest = new AbortRequest('https://example.com', { + method: 'GET', + onSuccess: (data) => data, + }) + + await expect(abortRequest).rejects.toThrow('Request failed') + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it('aborts the request', async () => { + fetchMock.mockResponse(JSON.stringify({ data: 'response data' })) + + const abortRequest = new AbortRequest('https://example.com', { + method: 'GET', + onSuccess: (data) => data, + }) + + abortRequest.abort() + + const [ url, requestInit ] = fetchMock.mock.calls[0] + + expect(requestInit?.signal?.aborted).toEqual(true) + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it('handles multiple requests for the same body', async () => { + const mock = 'response data' + + fetchMock.mockResponse(JSON.stringify({ data: mock })) + + const url = 'https://example.com' + const options = { + method: 'POST', + body: 'sameBody', + onSuccess: (data: string) => data, + } + + const abortRequest1 = new AbortRequest(url, options) + const abortRequest2 = new AbortRequest(url, options) + + expect(abortRequest1.request).toBe(abortRequest2.request) + + const [ result1, result2 ] = await Promise.all([ + abortRequest1, + abortRequest2, + ]) + + expect(result1).toEqual(mock) + expect(result2).toEqual(mock) + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it('resolves the non-aborted request when multiple requests with the same body', async () => { + const mock = 'response data' + + fetchMock.mockResponse(JSON.stringify({ data: mock })) + + const url = 'https://example.com' + const options = { + method: 'POST', + body: 'sameBody', + onSuccess: (data: string) => data, + } + + const abortRequest1 = new AbortRequest(url, options) + const abortRequest2 = new AbortRequest(url, options) + + abortRequest1.abort() + + const result = await abortRequest2 + + expect(result).toEqual(mock) + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it('has "abort" method after "then", "catch" and "finally" methods', async () => { + const mock = 'response data' + + fetchMock.mockResponse(JSON.stringify({ data: mock })) + + const catchCallback = jest.fn() + const finallyCallback = jest.fn() + const onSuccessCallback = jest.fn() + const thenErrorCallback = jest.fn() + const thenSuccessCallback = jest.fn() + + const initialRequest = new AbortRequest('https://example.com', { + method: 'POST', + body: 'sameBody', + onSuccess: onSuccessCallback, + }) + const abortRequest = initialRequest + .then(thenSuccessCallback, thenErrorCallback) + .catch(catchCallback) + .finally(finallyCallback) + + abortRequest.abort() + + const [ url, requestInit ] = fetchMock.mock.calls[0] + + expect(requestInit?.signal?.aborted).toEqual(true) + expect(fetchMock.mock.calls.length).toBe(1) + expect(thenSuccessCallback).not.toHaveBeenCalled() + expect(thenErrorCallback).not.toHaveBeenCalled() + expect(catchCallback).not.toHaveBeenCalled() + expect(finallyCallback).not.toHaveBeenCalled() + expect(onSuccessCallback).not.toHaveBeenCalled() + }) + + it('rejects the error in "catch" method', async () => { + const error = 'Request failed' + fetchMock.mockReject(new Error(error)) + + const abortRequest = new AbortRequest('https://example.com', { + method: 'GET', + onSuccess: (data) => data, + }) + .catch((error: any) => error.message) + + const data = await abortRequest + + await expect(data).toBe(error) + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it('calls methods if promise is successful', async () => { + fetchMock.mockResponse(JSON.stringify({ data: 'response data' })) + + const catchCallback = jest.fn() + const finallyCallback = jest.fn() + const onSuccessCallback = jest.fn() + const thenErrorCallback = jest.fn() + const thenSuccessCallback = jest.fn() + + const abortRequest = new AbortRequest('https://example.com', { + method: 'GET', + onSuccess: onSuccessCallback, + }) + .then(thenSuccessCallback, thenErrorCallback) + .catch(catchCallback) + .finally(finallyCallback) + + await abortRequest + + expect(finallyCallback).toHaveBeenCalled() + expect(onSuccessCallback).toHaveBeenCalled() + expect(thenSuccessCallback).toHaveBeenCalled() + expect(thenErrorCallback).not.toHaveBeenCalled() + expect(catchCallback).not.toHaveBeenCalled() + }) + + it('calls methods if promise is not successful', async () => { + fetchMock.mockReject(new Error('Request failed')) + + const catchCallback = jest.fn() + const finallyCallback = jest.fn() + const onSuccessCallback = jest.fn() + const thenSuccessCallback = jest.fn() + const thenErrorCallback = jest.fn() + + thenErrorCallback.mockImplementation(() => Promise.reject()) + + const abortRequest = new AbortRequest('https://example.com', { + method: 'GET', + onSuccess: onSuccessCallback, + }) + .then(thenSuccessCallback, thenErrorCallback) + .catch(catchCallback) + .finally(finallyCallback) + + await abortRequest + + expect(finallyCallback).toHaveBeenCalled() + expect(onSuccessCallback).not.toHaveBeenCalled() + expect(thenSuccessCallback).not.toHaveBeenCalled() + expect(thenErrorCallback).toHaveBeenCalled() + expect(catchCallback).toHaveBeenCalled() + }) +}) diff --git a/src/modules/gql-module/abortRequest.ts b/src/modules/gql-module/abortRequest.ts index c3766f06..b261d856 100644 --- a/src/modules/gql-module/abortRequest.ts +++ b/src/modules/gql-module/abortRequest.ts @@ -7,11 +7,9 @@ type FirstCallback = (value: Data) => Data | any type EmptyCallback = () => void -type ErrorCallback = (error: Error | any) => Promise | AbortRequest - type AbortRequestInit = RequestInit & { - onSuccess: ModifyCallback - onError?: ErrorCallback + onSuccess?: ModifyCallback + onError?: (error: any) => Promise | AbortRequest } type PendingRequest = { @@ -21,9 +19,11 @@ type PendingRequest = { const requestsQueue: Record = {} +const dummyPromise = new Promise(() => {}) + // Returns fetch promise that can be aborted // If we create several promises, only one request will be executed -class AbortRequest { +class AbortRequest { private controller = new AbortController() request: Promise promise: AbortPromise @@ -36,8 +36,8 @@ class AbortRequest { this.body = init.body as string this.isAborted = false - this.requestId = `${url}_${this.body}` + this.requestId = `${url}_${this.body}` const pendingRequest = requestsQueue[this.requestId] if (pendingRequest) { @@ -68,16 +68,20 @@ class AbortRequest { throw new Error('Subgraph indexing error') } - return json?.data as Data + return (json?.data || json) as Data }) .catch((error) => { - requestsQueue[this.requestId] = undefined + if (!this.isAborted) { + requestsQueue[this.requestId] = undefined + + if (typeof onError === 'function') { + onError(error) + } - if (typeof onError === 'function') { - onError(error) + return Promise.reject(error) } - return Promise.reject(error) + return dummyPromise as Data }) requestsQueue[this.requestId] = { @@ -129,7 +133,7 @@ class AbortRequest { } else { requestsQueue[this.requestId] = undefined - this.controller.abort() + this.controller.abort('aborted') } } } diff --git a/src/modules/gql-module/index.ts b/src/modules/gql-module/index.ts index 352e0d23..19b947c9 100644 --- a/src/modules/gql-module/index.ts +++ b/src/modules/gql-module/index.ts @@ -1,4 +1,5 @@ export { default as graphqlFetch } from './graphqlFetch' export { default as AbortPromise } from './abortPromise' +export { default as AbortRequest } from './abortRequest' export type { FetchCodegenInput, FetchInput } from './types' export { default as wrapAbortPromise } from './wrapAbortPromise' diff --git a/src/modules/gql-module/utils/getRequestUrl.ts b/src/modules/gql-module/utils/getRequestUrl.ts index 682bb6be..f0af5845 100644 --- a/src/modules/gql-module/utils/getRequestUrl.ts +++ b/src/modules/gql-module/utils/getRequestUrl.ts @@ -1,5 +1,5 @@ import { constants } from '../../../utils' -import localStorage from './local-storage' +import localStorage from '../../local-storage' const sessionErrorUrl = constants.sessionStorageNames.moduleErrorUrl diff --git a/src/modules/gql-module/utils/saveErrorUrlToSessionStorage.ts b/src/modules/gql-module/utils/saveErrorUrlToSessionStorage.ts index 3a1937ab..00a7736b 100644 --- a/src/modules/gql-module/utils/saveErrorUrlToSessionStorage.ts +++ b/src/modules/gql-module/utils/saveErrorUrlToSessionStorage.ts @@ -1,5 +1,5 @@ import { constants } from '../../../utils' -import localStorage from './local-storage' +import localStorage from '../../local-storage' const sessionErrorUrl = constants.sessionStorageNames.moduleErrorUrl diff --git a/src/modules/gql-module/utils/local-storage/LocalStorage.ts b/src/modules/local-storage/LocalStorage.ts similarity index 100% rename from src/modules/gql-module/utils/local-storage/LocalStorage.ts rename to src/modules/local-storage/LocalStorage.ts diff --git a/src/modules/local-storage/MemoryStorage.spec.ts b/src/modules/local-storage/MemoryStorage.spec.ts new file mode 100644 index 00000000..2b34b940 --- /dev/null +++ b/src/modules/local-storage/MemoryStorage.spec.ts @@ -0,0 +1,66 @@ +import MemoryStorage from './MemoryStorage' + + +describe('MemoryStorage', () => { + + it('should write and read values', () => { + const storage = new MemoryStorage() + + storage.setItem('key1', 'value1') + storage.setItem('key2', 'value2') + + expect(storage.getItem('key1')).toEqual('value1') + expect(storage.getItem('key2')).toEqual('value2') + }) + + it('should overwrite values', () => { + const storage = new MemoryStorage() + + storage.setItem('key', 'value1') + + expect(storage.getItem('key')).toEqual('value1') + + storage.setItem('key', 'value2') + + expect(storage.getItem('key')).toEqual('value2') + }) + + it('should remove and clear', () => { + const storage = new MemoryStorage() + + storage.setItem('key1', 'value1') + storage.setItem('key2', 'value2') + storage.removeItem('key2') + + expect(storage.getItem('key1')).toEqual('value1') + expect(storage.getItem('key2')).toBeNull() + + storage.clear() + + expect(storage.length).toEqual(0) + }) + + it('should return null for undefined values', () => { + const storage = new MemoryStorage() + + expect(storage.getItem('key')).toBeNull() + }) + + it('should return null for undefined keys', () => { + const storage = new MemoryStorage() + + expect(storage.key(1)).toBeNull() + }) + + it('should return correct length', () => { + const storage = new MemoryStorage() + + storage.setItem('key1', 'value1') + + expect(storage.length).toEqual(1) + + storage.setItem('key2', 'value2') + + expect(storage.length).toEqual(2) + }) +}) diff --git a/src/modules/gql-module/utils/local-storage/MemoryStorage.ts b/src/modules/local-storage/MemoryStorage.ts similarity index 100% rename from src/modules/gql-module/utils/local-storage/MemoryStorage.ts rename to src/modules/local-storage/MemoryStorage.ts diff --git a/src/modules/gql-module/utils/local-storage/index.ts b/src/modules/local-storage/index.ts similarity index 100% rename from src/modules/gql-module/utils/local-storage/index.ts rename to src/modules/local-storage/index.ts