Skip to content

Commit

Permalink
Improved Membership Fetch Error Handling (#4099)
Browse files Browse the repository at this point in the history
* addressed membership bug

* fixed bug

* added some tests and combined membership fetch actions
  • Loading branch information
blurpesec authored Aug 26, 2021
1 parent 7a21e39 commit 96e7b50
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 82 deletions.
6 changes: 0 additions & 6 deletions src/database/data/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,12 +386,6 @@ export const NODES_CONFIG: { [key in NetworkId]: StaticNodeConfig[] } = {
}
],
xDAI: [
{
name: NetworkUtils.makeNodeName('xDAI', 'mycrypto'),
type: NodeType.RPC,
service: 'MyCrypto',
url: 'https://xdai.mycryptoapi.com/'
},
{
name: NetworkUtils.makeNodeName('xDAI', 'xdaichain.com'),
type: NodeType.RPC,
Expand Down
2 changes: 1 addition & 1 deletion src/database/generateDefaultValues.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('Schema', () => {

it('adds Nodes to each Network', () => {
const nodes = toArray(defaultData[LSKeys.NETWORKS]).flatMap((n) => n.nodes);
expect(nodes).toHaveLength(58);
expect(nodes).toHaveLength(57);
});
});

Expand Down
56 changes: 52 additions & 4 deletions src/services/ApiService/MembershipApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { DEFAULT_NETWORK, XDAI_NETWORK } from '@config';
import { IMembershipId } from '@features/PurchaseMembership/config';
import { accountWithMembership, membershipApiResponse } from '@fixtures';
import { DEFAULT_NETWORK, POLYGON_NETWORK, XDAI_NETWORK } from '@config';
import { IMembershipId, MembershipStatus } from '@features/PurchaseMembership/config';
import { accountWithMembership, fAccount, fNetwork, fNetworks, membershipApiResponse } from '@fixtures';
import { StoreAccount, WalletId } from '@types';

import { formatResponse, getMembershipContracts } from './MembershipApi';
import MembershipApi, { formatResponse, getMembershipContracts } from './MembershipApi';

jest.mock('@mycrypto/unlock-scan', () => ({
...jest.requireActual('@mycrypto/unlock-scan'),
getUnlockTimestamps: jest.fn().mockResolvedValue(Promise.reject("error fetching balances"))
}));

describe('MembershipApi', () => {
it('formatResponse(): transforms timestamps to MembershipStatus', () => {
Expand All @@ -19,6 +25,48 @@ describe('MembershipApi', () => {
const actual = formatResponse(DEFAULT_NETWORK)(membershipApiResponse);
expect(actual).toEqual(expected);
});
it('getMultiNetworkMemberships(): handles fetch errors', async () => {
const accounts = [
{ address: accountWithMembership, networkId: DEFAULT_NETWORK, wallet: WalletId.LEDGER_NANO_S },
{ address: '0xfeac75a09662396283f4bb50f0a9249576a81866', networkId: XDAI_NETWORK },
{ ...fAccount, networkId: POLYGON_NETWORK }
] as StoreAccount[];
const polygonNetwork = { ...fNetwork, id: POLYGON_NETWORK };

const ethereumAccounts = accounts
.filter(({ networkId }) => networkId === DEFAULT_NETWORK)
.map(({ address }) => address);
const xdaiAccounts = accounts
.filter(({ networkId }) => networkId === XDAI_NETWORK)
.map(({ address }) => address);
const polygonAccounts = accounts
.filter(({ networkId }) => networkId === POLYGON_NETWORK)
.map(({ address }) => address);

const membershipFetchState = [
{
accounts: ethereumAccounts,
network: fNetworks[0]
},
{
accounts: xdaiAccounts,
network: fNetworks[2]
},
{
accounts: polygonAccounts,
network: polygonNetwork
}
];
const memberships = [] as MembershipStatus[];
const errors = {
xDAI: true,
MATIC: true,
Ethereum: true
}
const expected = { memberships, errors }
const res = await MembershipApi.getMultiNetworkMemberships(membershipFetchState)
expect(res).toStrictEqual(expected)
})
});

describe('getMembershipContracts()', () => {
Expand Down
35 changes: 35 additions & 0 deletions src/services/ApiService/MembershipApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ import {
MembershipStatus
} from '@features/PurchaseMembership/config';
import { ProviderHandler } from '@services/EthService/';
import { MembershipErrorState } from '@store/membership.slice';
import { Bigish, Network, NetworkId, TAddress } from '@types';
import { bigify } from '@utils';
import { mapObjIndexed, pickBy, pipe, toString } from '@vendor';

interface MembershipFetchConfig {
network: Network;
accounts: TAddress[];
}

export interface MembershipFetchResult {
memberships: MembershipStatus[];
errors: MembershipErrorState;
}

export const getMembershipContracts = (membershipNetworkId: NetworkId) =>
Object.values(MEMBERSHIP_CONFIG)
.filter(({ networkId }) => networkId === membershipNetworkId)
Expand Down Expand Up @@ -47,6 +58,30 @@ const MembershipApi = {
return getUnlockTimestamps(provider, addresses, {
contracts: getMembershipContracts(network.id)
}).then(formatResponse(network.id));
},
getMultiNetworkMemberships(configs: MembershipFetchConfig[]): Promise<MembershipFetchResult> {
return Promise.all(
configs.map((config) => {
return MembershipApi.getMemberships(config.accounts, config.network)
.then((memberships) => {
return { memberships, errors: {} } as MembershipFetchResult;
})
.catch((err) => {
console.log('[getMemberships]: Failed for network: ', config.network.id, ' err: ', err);
return {
memberships: [] as MembershipStatus[],
errors: { [config.network.id]: true }
} as MembershipFetchResult;
});
})
).then((membershipStates) =>
membershipStates.reduce((membershipState, acc) => {
return {
memberships: [...acc.memberships, ...membershipState.memberships],
errors: { ...acc.errors, ...membershipState.errors }
};
}, { memberships: [], errors: {} } as unknown as MembershipFetchResult)
);
}
};

Expand Down
3 changes: 1 addition & 2 deletions src/services/Store/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ export {
} from './asset.slice';
export {
fetchMemberships,
setMemberships,
setMembership,
deleteMembership,
fetchError,
setMembershipFetchState,
getIsMyCryptoMember,
getMembershipState
} from './membership.slice';
Expand Down
87 changes: 63 additions & 24 deletions src/services/Store/store/membership.slice.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { call } from 'redux-saga-test-plan/matchers';
import { throwError } from 'redux-saga-test-plan/providers';
import { expectSaga, mockAppState } from 'test-utils';

import { DEFAULT_NETWORK, POLYGON_NETWORK, XDAI_NETWORK } from '@config';
Expand All @@ -8,10 +7,16 @@ import { accountWithMembership, fAccount, fNetwork, fNetworks } from '@fixtures'
import { MembershipApi } from '@services/ApiService';
import { StoreAccount, WalletId } from '@types';

import slice, { fetchMemberships, fetchMembershipsSaga, initialState } from './membership.slice';
import slice, {
fetchMemberships,
fetchMembershipsSaga,
initialState,
MembershipErrorState,
setMembershipFetchState
} from './membership.slice';

const reducer = slice.reducer;
const { setMemberships, setMembership, deleteMembership, fetchError } = slice.actions;
const { setMembership, deleteMembership } = slice.actions;

describe('MembershipsSlice', () => {
it('has an initial state', () => {
Expand All @@ -20,23 +25,23 @@ describe('MembershipsSlice', () => {
expect(actual).toEqual(expected);
});

it('setMemberships(): adds multiple memberships state', () => {
it('setMembershipFetchState(): adds multiple memberships state', () => {
const m1 = { address: 'random', networkId: DEFAULT_NETWORK } as MembershipStatus;
const m2 = { address: 'random2', networkId: DEFAULT_NETWORK } as MembershipStatus;
const actual = reducer(initialState, setMemberships([m1, m2]));
const actual = reducer(initialState, setMembershipFetchState({ memberships: [m1, m2], errors: {} as MembershipErrorState}));
const expected = { ...initialState, record: [m1, m2] };
expect(actual).toEqual(expected);
});

it('setMemberships(): deduplicates memberships', () => {
it('setMembershipFetchState(): deduplicates memberships', () => {
const m1 = { address: 'random', networkId: DEFAULT_NETWORK } as MembershipStatus;
const m2 = { address: 'random2', networkId: DEFAULT_NETWORK } as MembershipStatus;
const actual = reducer({ ...initialState, record: [m1] }, setMemberships([m1, m2]));
const actual = reducer({ ...initialState, record: [m1] }, setMembershipFetchState({ memberships: [m1, m2], errors: {} as MembershipErrorState}));
const expected = { ...initialState, record: [m1, m2] };
expect(actual).toEqual(expected);
});

it('setMemberships(): deduplicates memberships uses mergeLeft', () => {
it('setMembershipFetchState(): deduplicates memberships uses mergeLeft', () => {
const m1 = {
address: 'random',
memberships: [{ type: 'onemonth' }],
Expand All @@ -45,7 +50,7 @@ describe('MembershipsSlice', () => {
const m2 = { address: 'random2', networkId: DEFAULT_NETWORK } as MembershipStatus;
const actual = reducer(
{ ...initialState, record: [m1] },
setMemberships([{ ...m1, memberships: [{ type: 'sixmonths' }] }, m2] as MembershipStatus[])
setMembershipFetchState({ memberships: [{ ...m1, memberships: [{ type: 'sixmonths' }] }, m2] as MembershipStatus[], errors: {} as MembershipErrorState})
);
const expected = {
...initialState,
Expand Down Expand Up @@ -73,9 +78,14 @@ describe('MembershipsSlice', () => {
expect(actual).toEqual(expected);
});

it('fetchError(): sets an error', () => {
const actual = reducer(initialState, fetchError());
const expected = { ...initialState, error: true };
it('setMembershipFetchState(): sets an error', () => {
const errorState = {
Ethereum: false,
xDAI: true,
MATIC: false
};
const actual = reducer(initialState, setMembershipFetchState({ memberships: [], errors: errorState }));
const expected = { ...initialState, error: errorState };
expect(actual).toEqual(expected);
});
});
Expand Down Expand Up @@ -126,18 +136,39 @@ describe('fetchMembershipsSaga()', () => {
.filter(({ networkId }) => networkId === POLYGON_NETWORK)
.map(({ address }) => address);

const membershipFetchState = [
{
accounts: ethereumAccounts,
network: fNetworks[0]
},
{
accounts: xdaiAccounts,
network: fNetworks[2]
},
{
accounts: polygonAccounts,
network: polygonNetwork
}
];

const membershipFetchExpected = {
memberships: res,
errors: {} as MembershipErrorState
};

const initialState = mockAppState({ accounts, networks: [...fNetworks, polygonNetwork] });

it('can fetch memberships from provided accounts', () => {
return (
expectSaga(fetchMembershipsSaga)
.withState(initialState)
.provide([
[call(MembershipApi.getMemberships, ethereumAccounts, fNetworks[0]), [res[0]]],
[call(MembershipApi.getMemberships, xdaiAccounts, fNetworks[2]), [res[1]]],
[call(MembershipApi.getMemberships, polygonAccounts, polygonNetwork), [res[2]]]
[
call(MembershipApi.getMultiNetworkMemberships, membershipFetchState),
membershipFetchExpected
]
])
.put(setMemberships(res))
.put(setMembershipFetchState({ memberships: res, errors: {} as MembershipErrorState }))
.dispatch(fetchMemberships(accounts))
// We test a `takeLatest` saga so we expect a timeout.
// use `silentRun` to silence the warning.
Expand All @@ -149,21 +180,29 @@ describe('fetchMembershipsSaga()', () => {
return expectSaga(fetchMembershipsSaga)
.withState(initialState)
.provide([
[call(MembershipApi.getMemberships, ethereumAccounts, fNetworks[0]), [res[0]]],
[call(MembershipApi.getMemberships, xdaiAccounts, fNetworks[2]), [res[1]]],
[call(MembershipApi.getMemberships, polygonAccounts, polygonNetwork), [res[2]]]
[
call(MembershipApi.getMultiNetworkMemberships, membershipFetchState),
membershipFetchExpected
]
])
.put(setMemberships(res))
.put(setMembershipFetchState({ memberships: res, errors: {} as MembershipErrorState }))
.dispatch(fetchMemberships())
.silentRun();
});

it('can sets error if the call fails', () => {
const error = new Error('error');
it('can sets error if the call throws an error', () => {
return expectSaga(fetchMembershipsSaga)
.withState(initialState)
.provide([[call.fn(MembershipApi.getMemberships), throwError(error)]])
.put(fetchError())
.provide([
[
call(MembershipApi.getMultiNetworkMemberships, membershipFetchState),
{
memberships: membershipFetchExpected.memberships,
errors: { Ethereum: true }
}
]
])
.put(setMembershipFetchState({ memberships: res, errors: { Ethereum: true } as MembershipErrorState }))
.dispatch(fetchMemberships())
.silentRun();
});
Expand Down
Loading

0 comments on commit 96e7b50

Please sign in to comment.