Saga Testing (#415)

* add exports to config saga, refactor

* add config saga tests

* add exports to necessary files

* add remaining saga test & snapshots

* update orders saga spec to use Infinity constant

* update dWallet saga spec snapshot

* refactor config saga slightly

* update config saga spec

* update config saga snapshot

* update rates saga spec

* remove unused vars from config saga spec
This commit is contained in:
skubakdj 2017-11-29 23:07:16 -05:00 committed by Daniel Ternyak
parent 31963b334c
commit 6c09e7160a
16 changed files with 2043 additions and 29 deletions

View File

@ -69,14 +69,14 @@ export interface BityOrderCreateRequestedSwapAction {
};
}
interface BityOrderInput {
export interface BityOrderInput {
amount: string;
currency: string;
reference: string;
status: string;
}
interface BityOrderOutput {
export interface BityOrderOutput {
amount: string;
currency: string;
reference: string;

View File

@ -30,7 +30,8 @@ import {
changeNode,
changeNodeIntent,
setLatestBlock,
AddCustomNodeAction
AddCustomNodeAction,
ChangeNodeIntentAction
} from 'actions/config';
import { showNotification } from 'actions/notifications';
import translate from 'translations';
@ -100,13 +101,13 @@ export function* pollOfflineStatus(): SagaIterator {
}
// Fork our recurring API call, watch for the need to cancel.
function* handlePollOfflineStatus(): SagaIterator {
export function* handlePollOfflineStatus(): SagaIterator {
const pollOfflineStatusTask = yield fork(pollOfflineStatus);
yield take('CONFIG_STOP_POLL_OFFLINE_STATE');
yield cancel(pollOfflineStatusTask);
}
function* handleTogglePollOfflineStatus(): SagaIterator {
export function* handleTogglePollOfflineStatus(): SagaIterator {
const isForcedOffline = yield select(getForceOffline);
if (isForcedOffline) {
yield fork(handlePollOfflineStatus);
@ -117,14 +118,15 @@ function* handleTogglePollOfflineStatus(): SagaIterator {
// @HACK For now we reload the app when doing a language swap to force non-connected
// data to reload. Also the use of timeout to avoid using additional actions for now.
function* reload(): SagaIterator {
export function* reload(): SagaIterator {
setTimeout(() => location.reload(), 250);
}
function* handleNodeChangeIntent(action): SagaIterator {
export function* handleNodeChangeIntent(
action: ChangeNodeIntentAction
): SagaIterator {
const currentNode = yield select(getNode);
const currentConfig = yield select(getNodeConfig);
const currentWallet = yield select(getWalletInst);
const currentNetwork = currentConfig.network;
let actionConfig = NODES[action.payload];
@ -173,6 +175,8 @@ function* handleNodeChangeIntent(action): SagaIterator {
yield put(setLatestBlock(latestBlock));
yield put(changeNode(action.payload, actionConfig));
const currentWallet = yield select(getWalletInst);
// if there's no wallet, do not reload as there's no component state to resync
if (currentWallet && currentNetwork !== actionConfig.network) {
yield call(reload);
@ -185,7 +189,7 @@ export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator {
}
// unset web3 as the selected node if a non-web3 wallet has been selected
function* unsetWeb3Node(action): SagaIterator {
export function* unsetWeb3Node(action): SagaIterator {
const node = yield select(getNode);
const nodeConfig = yield select(getNodeConfig);
const newWallet = action.payload;
@ -196,7 +200,11 @@ function* unsetWeb3Node(action): SagaIterator {
}
// switch back to a node with the same network as MetaMask/Mist
const equivalentNode = Object.keys(NODES)
yield put(changeNodeIntent(equivalentNodeOrDefault(nodeConfig)));
}
export const equivalentNodeOrDefault = nodeConfig => {
const node = Object.keys(NODES)
.filter(key => key !== 'web3')
.reduce((found, key) => {
const config = NODES[key];
@ -210,12 +218,8 @@ function* unsetWeb3Node(action): SagaIterator {
}, '');
// if no equivalent node was found, use the app default
const newNode = equivalentNode.length
? equivalentNode
: configInitialState.nodeSelection;
yield put(changeNodeIntent(newNode));
}
return node.length ? node : configInitialState.nodeSelection;
};
export default function* configSaga(): SagaIterator {
yield takeLatest(

View File

@ -25,7 +25,7 @@ import { getTokens } from 'selectors/wallet';
import translate from 'translations';
import { TokenValue } from 'libs/units';
function* getDeterministicWallets(
export function* getDeterministicWallets(
action: GetDeterministicWalletsAction
): SagaIterator {
const { seed, dPath, publicKey, chainCode, limit, offset } = action.payload;
@ -64,7 +64,7 @@ function* getDeterministicWallets(
}
// Grab each wallet's main network token, and update it with it
function* updateWalletValues(): SagaIterator {
export function* updateWalletValues(): SagaIterator {
const node: INode = yield select(getNodeLib);
const wallets: DeterministicWalletData[] = yield select(getWallets);
@ -87,7 +87,7 @@ function* updateWalletValues(): SagaIterator {
}
// Grab the current desired token, and update the wallet with it
function* updateWalletTokenValues(): SagaIterator {
export function* updateWalletTokenValues(): SagaIterator {
const desiredToken: string = yield select(getDesiredToken);
if (!desiredToken) {
return;

View File

@ -5,7 +5,9 @@ import {
import { delay, SagaIterator } from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects';
function* handleNotification(action: ShowNotificationAction): SagaIterator {
export function* handleNotification(
action: ShowNotificationAction
): SagaIterator {
const { duration } = action.payload;
// show forever
if (duration === 0 || duration === Infinity) {

View File

@ -31,7 +31,7 @@ import {
export const getSwap = (state: AppState): SwapState => state.swap;
const ONE_SECOND = 1000;
const TEN_SECONDS = ONE_SECOND * 10;
const BITY_TIMEOUT_MESSAGE = `
export const BITY_TIMEOUT_MESSAGE = `
Time has run out.
If you have already sent, please wait 1 hour.
If your order has not be processed after 1 hour,
@ -81,7 +81,7 @@ export function* pollBityOrderStatusSaga(): SagaIterator {
}
}
function* postBityOrderCreate(
export function* postBityOrderCreate(
action: BityOrderCreateRequestedSwapAction
): SagaIterator {
const payload = action.payload;

View File

@ -20,7 +20,7 @@ export function* loadBityRates(): SagaIterator {
}
// Fork our recurring API call, watch for the need to cancel.
function* handleBityRates(): SagaIterator {
export function* handleBityRates(): SagaIterator {
const loadBityRatesTask = yield fork(loadBityRates);
yield take(TypeKeys.SWAP_STOP_LOAD_BITY_RATES);
yield cancel(loadBityRatesTask);

View File

@ -39,7 +39,7 @@ import { getNetworkConfig, getNodeLib } from 'selectors/config';
import { getTokens, getWalletInst } from 'selectors/wallet';
import translate from 'translations';
function* updateAccountBalance(): SagaIterator {
export function* updateAccountBalance(): SagaIterator {
try {
yield put(setBalancePending());
const wallet: null | IWallet = yield select(getWalletInst);
@ -56,7 +56,7 @@ function* updateAccountBalance(): SagaIterator {
}
}
function* updateTokenBalances(): SagaIterator {
export function* updateTokenBalances(): SagaIterator {
try {
const node: INode = yield select(getNodeLib);
const wallet: null | IWallet = yield select(getWalletInst);
@ -87,7 +87,7 @@ function* updateTokenBalances(): SagaIterator {
}
}
function* updateBalances(): SagaIterator {
export function* updateBalances(): SagaIterator {
yield fork(updateAccountBalance);
yield fork(updateTokenBalances);
}
@ -122,7 +122,7 @@ export function* unlockKeystore(action: UnlockKeystoreAction): SagaIterator {
yield put(setWallet(wallet));
}
function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator {
export function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator {
let wallet;
const { phrase, pass, path, address } = action.payload;
@ -139,7 +139,7 @@ function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator {
// inspired by v3:
// https://github.com/kvhnuke/etherwallet/blob/417115b0ab4dd2033d9108a1a5c00652d38db68d/app/scripts/controllers/decryptWalletCtrl.js#L311
function* unlockWeb3(): SagaIterator {
export function* unlockWeb3(): SagaIterator {
const failMsg1 = 'Could not connect to MetaMask / Mist.';
const failMsg2 = 'No accounts found in MetaMask / Mist.';
const { web3 } = window as any;
@ -170,7 +170,7 @@ function* unlockWeb3(): SagaIterator {
}
}
function* broadcastTx(action: BroadcastTxRequestedAction): SagaIterator {
export function* broadcastTx(action: BroadcastTxRequestedAction): SagaIterator {
const signedTx = action.payload.signedTx;
try {
const node: INode = yield select(getNodeLib);

View File

@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`handleNodeChangeIntent* should race getCurrentBlock and delay 1`] = `
Object {
"@@redux-saga/IO": true,
"RACE": Object {
"lb": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [],
"context": null,
"fn": [Function],
},
},
"to": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [
5000,
],
"context": null,
"fn": [Function],
},
},
},
}
`;
exports[`handleNodeChangeIntent* should select getCustomNodeConfig and match race snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"RACE": Object {
"lb": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [],
"context": null,
"fn": [Function],
},
},
"to": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [
5000,
],
"context": null,
"fn": [Function],
},
},
},
}
`;
exports[`pollOfflineStatus* should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Object {
"duration": Infinity,
"id": 0.001,
"level": "danger",
"msg": "Youve lost your connection to the network, check your internet
connection or try changing networks from the dropdown at the
top right of the page.",
},
"type": "SHOW_NOTIFICATION",
},
"channel": null,
},
}
`;
exports[`pollOfflineStatus* should race pingSucceeded and timeout 1`] = `
Object {
"@@redux-saga/IO": true,
"RACE": Object {
"pingSucceeded": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [],
"context": null,
"fn": [Function],
},
},
"timeout": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [
5000,
],
"context": null,
"fn": [Function],
},
},
},
}
`;

View File

@ -0,0 +1,216 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getDeterministicWallets* starting from publicKey & chainCode should match put snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Array [
Object {
"address": "0xa3fA2e024bf9964bb64bDf0afdCa8E3c374a6e41",
"index": 0,
"tokenValues": Object {},
},
Object {
"address": "0x9bbC617740413CDCc41fB69901b252e20fd6Ef61",
"index": 1,
"tokenValues": Object {},
},
Object {
"address": "0xE5297818aEee60385306b7087B024af20524C0FD",
"index": 2,
"tokenValues": Object {},
},
Object {
"address": "0x111789ac11B69fE7EbC307c908Efe7677fd347A2",
"index": 3,
"tokenValues": Object {},
},
Object {
"address": "0x315857914bEd907e0Cf33b5883e599dD6ACc45d2",
"index": 4,
"tokenValues": Object {},
},
Object {
"address": "0xEA316E5BFd9FDeCD81489929ae56DBE6ffaDD22C",
"index": 5,
"tokenValues": Object {},
},
Object {
"address": "0x5De67797cEeCeD707A6868bbE96331ED6f811F06",
"index": 6,
"tokenValues": Object {},
},
Object {
"address": "0xd0cb2Fe67f66DF4D6610E99dDE2ad1B0B5bf4054",
"index": 7,
"tokenValues": Object {},
},
Object {
"address": "0xC7D3A4101bf3Ac1A5aE898784F80D2F9A1E67B62",
"index": 8,
"tokenValues": Object {},
},
Object {
"address": "0x12228Ec03f6aFd6Dad66ceD9C8B9762b3080e319",
"index": 9,
"tokenValues": Object {},
},
],
"type": "DW_SET_WALLETS",
},
"channel": null,
},
}
`;
exports[`getDeterministicWallets* starting from seed should match put snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Array [
Object {
"address": "0x2e516E79F439469AA3DD43a93429ee45cBeb77Aa",
"index": 0,
"tokenValues": Object {},
},
Object {
"address": "0x4aE493688184D612aF70C0B04aA7f2C8eE03a1dE",
"index": 1,
"tokenValues": Object {},
},
Object {
"address": "0xbd208C37C747f784EFfA379aA417d867216Ace19",
"index": 2,
"tokenValues": Object {},
},
Object {
"address": "0x85FF91929287F6cdC801280EAf8fa899e22a02F6",
"index": 3,
"tokenValues": Object {},
},
Object {
"address": "0xDF5acC09CDf1f966CEc6eFAB3523804E1de65c7f",
"index": 4,
"tokenValues": Object {},
},
],
"type": "DW_SET_WALLETS",
},
"channel": null,
},
}
`;
exports[`updateWalletTokenValues* should match snapshot for put wallet1 update 1`] = `
Object {
"done": false,
"value": Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Object {
"address": "0x0",
"index": 0,
"tokenValues": Object {
"OMG": Object {
"decimal": 16,
"value": "64",
},
},
"value": "64",
},
"type": "DW_UPDATE_WALLET",
},
"channel": null,
},
},
}
`;
exports[`updateWalletTokenValues* should match snapshot for put wallet2 update 1`] = `
Object {
"done": false,
"value": Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Object {
"address": "0x1",
"index": 1,
"tokenValues": Object {
"BAT": Object {
"decimal": 16,
"value": "64",
},
"OMG": Object {
"decimal": 16,
"value": "c8",
},
},
"value": "64",
},
"type": "DW_UPDATE_WALLET",
},
"channel": null,
},
},
}
`;
exports[`updateWalletTokenValues* should match snapshot of wallet token balances 1`] = `
Object {
"@@redux-saga/IO": true,
"ALL": Array [
Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [
"0x0",
Object {
"address": "0x2",
"decimal": 16,
"symbol": "OMG",
},
],
"context": RpcNode {
"client": RPCClient {
"batch": [Function],
"call": [Function],
"decorateRequest": [Function],
"endpoint": "",
"headers": Object {},
},
"requests": RPCRequests {},
},
"fn": [Function],
},
},
Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [
"0x1",
Object {
"address": "0x2",
"decimal": 16,
"symbol": "OMG",
},
],
"context": RpcNode {
"client": RPCClient {
"batch": [Function],
"call": [Function],
"decorateRequest": [Function],
"endpoint": "",
"headers": Object {},
},
"requests": RPCRequests {},
},
"fn": [Function],
},
},
],
}
`;

View File

@ -0,0 +1,228 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`broadcastTx* should match put showNotifiction snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Object {
"duration": 0,
"id": 0.001,
"level": "success",
"msg": <TransactionSucceeded
blockExplorer="foo"
txHash="txHash"
/>,
},
"type": "SHOW_NOTIFICATION",
},
"channel": null,
},
}
`;
exports[`unlockKeystore* should match put setWallet snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Wallet {
"_privKey": Object {
"data": Array [
139,
203,
68,
86,
239,
3,
86,
206,
6,
44,
133,
124,
239,
221,
62,
209,
186,
180,
84,
50,
207,
118,
214,
213,
52,
8,
153,
207,
208,
247,
2,
232,
],
"type": "Buffer",
},
"_pubKey": undefined,
"signMessage": [Function],
"signRawTransaction": [Function],
"unlock": [Function],
},
"type": "WALLET_SET",
},
"channel": null,
},
}
`;
exports[`unlockMnemonic* should match put setWallet snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Wallet {
"_privKey": Object {
"data": Array [
49,
233,
127,
57,
92,
171,
198,
250,
163,
125,
138,
157,
107,
177,
133,
24,
124,
53,
112,
78,
123,
151,
108,
122,
17,
14,
47,
14,
171,
55,
195,
68,
],
"type": "Buffer",
},
"_pubKey": undefined,
"signMessage": [Function],
"signRawTransaction": [Function],
"unlock": [Function],
},
"type": "WALLET_SET",
},
"channel": null,
},
}
`;
exports[`unlockPrivateKey should match put setWallet snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Wallet {
"_privKey": Object {
"data": Array [
49,
233,
127,
57,
92,
171,
198,
250,
163,
125,
138,
157,
107,
177,
133,
24,
124,
53,
112,
78,
123,
151,
108,
122,
17,
14,
47,
14,
171,
55,
195,
68,
],
"type": "Buffer",
},
"_pubKey": undefined,
"signMessage": [Function],
"signRawTransaction": [Function],
"unlock": [Function],
},
"type": "WALLET_SET",
},
"channel": null,
},
}
`;
exports[`unlockWeb3* should match setWallet snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Web3Wallet {
"address": "0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854",
"network": "ETH",
"web3": Object {
"eth": Object {
"getAccounts": [Function],
},
"network": "1",
"version": Object {
"getNetwork": [Function],
},
},
},
"type": "WALLET_SET",
},
"channel": null,
},
}
`;
exports[`updateTokenBalances* should match put setTokenBalances snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"PUT": Object {
"action": Object {
"payload": Object {
"BAT": "c8",
"OMG": "64",
},
"type": "WALLET_SET_TOKEN_BALANCES",
},
"channel": null,
},
}
`;

417
spec/sagas/config.spec.ts Normal file
View File

@ -0,0 +1,417 @@
import { configuredStore } from 'store';
import { delay } from 'redux-saga';
import { call, cancel, fork, put, take, select } from 'redux-saga/effects';
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
import {
toggleOfflineConfig,
changeNode,
changeNodeIntent,
setLatestBlock
} from 'actions/config';
import {
pollOfflineStatus,
handlePollOfflineStatus,
handleNodeChangeIntent,
handleTogglePollOfflineStatus,
reload,
unsetWeb3Node,
equivalentNodeOrDefault
} from 'sagas/config';
import { NODES } from 'config/data';
import {
getNode,
getNodeConfig,
getOffline,
getForceOffline,
getCustomNodeConfigs
} from 'selectors/config';
import { INITIAL_STATE as configInitialState } from 'reducers/config';
import { getWalletInst } from 'selectors/wallet';
import { Web3Wallet } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { showNotification } from 'actions/notifications';
import translate from 'translations';
// init module
configuredStore.getState();
describe('pollOfflineStatus*', () => {
const nav = navigator as any;
const doc = document as any;
const data = {} as any;
data.gen = cloneableGenerator(pollOfflineStatus)();
const node = {
lib: {
ping: jest.fn()
}
};
const isOffline = true;
const isForcedOffline = true;
const raceSuccess = {
pingSucceeded: true
};
const raceFailure = {
pingSucceeded: false
};
let originalHidden;
let originalOnLine;
let originalRandom;
beforeAll(() => {
// backup global config
originalHidden = document.hidden;
originalOnLine = navigator.onLine;
originalRandom = Math.random;
// mock config
Object.defineProperty(document, 'hidden', { value: false, writable: true });
Object.defineProperty(navigator, 'onLine', { value: true, writable: true });
Math.random = () => 0.001;
});
afterAll(() => {
// restore global config
Object.defineProperty(document, 'hidden', {
value: originalHidden,
writable: false
});
Object.defineProperty(navigator, 'onLine', {
value: originalOnLine,
writable: false
});
Math.random = originalRandom;
});
it('should select getNodeConfig', () => {
expect(data.gen.next().value).toEqual(select(getNodeConfig));
});
it('should select getOffline', () => {
expect(data.gen.next(node).value).toEqual(select(getOffline));
});
it('should select getForceOffline', () => {
data.isOfflineClone = data.gen.clone();
expect(data.gen.next(isOffline).value).toEqual(select(getForceOffline));
});
it('should be done if isForcedOffline', () => {
data.clone1 = data.gen.clone();
expect(data.clone1.next(isForcedOffline).done).toEqual(true);
});
it('should call delay if document is hidden', () => {
data.clone2 = data.gen.clone();
doc.hidden = true;
expect(data.clone2.next(!isForcedOffline).value).toEqual(call(delay, 1000));
});
it('should race pingSucceeded and timeout', () => {
doc.hidden = false;
expect(data.gen.next(!isForcedOffline).value).toMatchSnapshot();
});
it('should put showNotification and put toggleOfflineConfig if pingSucceeded && isOffline', () => {
expect(data.gen.next(raceSuccess).value).toEqual(
put(
showNotification(
'success',
'Your connection to the network has been restored!',
3000
)
)
);
expect(data.gen.next().value).toEqual(put(toggleOfflineConfig()));
});
it('should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline', () => {
nav.onLine = !isOffline;
data.isOfflineClone.next(!isOffline);
data.isOfflineClone.next(!isForcedOffline);
data.clone3 = data.isOfflineClone.clone();
expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot();
expect(data.isOfflineClone.next().value).toEqual(
put(toggleOfflineConfig())
);
});
it('should call delay when neither case is true', () => {
expect(data.clone3.next(raceSuccess).value).toEqual(call(delay, 5000));
});
});
describe('handlePollOfflineStatus*', () => {
const gen = handlePollOfflineStatus();
const mockTask = createMockTask();
it('should fork pollOffineStatus', () => {
const expectedForkYield = fork(pollOfflineStatus);
expect(gen.next().value).toEqual(expectedForkYield);
});
it('should take CONFIG_STOP_POLL_OFFLINE_STATE', () => {
expect(gen.next(mockTask).value).toEqual(
take('CONFIG_STOP_POLL_OFFLINE_STATE')
);
});
it('should cancel pollOfflineStatus', () => {
expect(gen.next().value).toEqual(cancel(mockTask));
});
});
describe('handleTogglePollOfflineStatus*', () => {
const data = {} as any;
data.gen = cloneableGenerator(handleTogglePollOfflineStatus)();
const isForcedOffline = true;
it('should select getForceOffline', () => {
expect(data.gen.next().value).toEqual(select(getForceOffline));
});
it('should fork handlePollOfflineStatus when isForcedOffline', () => {
data.clone = data.gen.clone();
expect(data.gen.next(isForcedOffline).value).toEqual(
fork(handlePollOfflineStatus)
);
});
it('should call handlePollOfflineStatus when !isForcedOffline', () => {
expect(data.clone.next(!isForcedOffline).value).toEqual(
call(handlePollOfflineStatus)
);
});
it('should be done', () => {
expect(data.gen.next().done).toEqual(true);
expect(data.clone.next().done).toEqual(true);
});
});
describe('handleNodeChangeIntent*', () => {
let originalRandom;
// normal operation variables
const defaultNode = configInitialState.nodeSelection;
const defaultNodeConfig = NODES[defaultNode];
const newNode = Object.keys(NODES).reduce(
(acc, cur) => (NODES[acc].network === defaultNodeConfig.network ? cur : acc)
);
const newNodeConfig = NODES[newNode];
const changeNodeIntentAction = changeNodeIntent(newNode);
const truthyWallet = true;
const latestBlock = '0xa';
const raceSuccess = {
lb: latestBlock
};
const raceFailure = {
to: true
};
const data = {} as any;
data.gen = cloneableGenerator(handleNodeChangeIntent)(changeNodeIntentAction);
beforeAll(() => {
originalRandom = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = originalRandom;
});
it('should select getNode', () => {
expect(data.gen.next().value).toEqual(select(getNode));
});
it('should select nodeConfig', () => {
expect(data.gen.next(defaultNode).value).toEqual(select(getNodeConfig));
});
it('should race getCurrentBlock and delay', () => {
expect(data.gen.next(defaultNodeConfig).value).toMatchSnapshot();
});
it('should put showNotification and put changeNode if timeout', () => {
data.clone1 = data.gen.clone();
expect(data.clone1.next(raceFailure).value).toEqual(
put(showNotification('danger', translate('ERROR_32'), 5000))
);
expect(data.clone1.next().value).toEqual(
put(changeNode(defaultNode, defaultNodeConfig))
);
expect(data.clone1.next().done).toEqual(true);
});
it('should put setLatestBlock', () => {
expect(data.gen.next(raceSuccess).value).toEqual(
put(setLatestBlock(latestBlock))
);
});
it('should put changeNode', () => {
expect(data.gen.next().value).toEqual(
put(changeNode(changeNodeIntentAction.payload, newNodeConfig))
);
});
it('should select getWalletInst', () => {
expect(data.gen.next().value).toEqual(select(getWalletInst));
});
it('should call reload if wallet exists and network is new', () => {
data.clone2 = data.gen.clone();
expect(data.clone2.next(truthyWallet).value).toEqual(call(reload));
expect(data.clone2.next().done).toEqual(true);
});
it('should be done', () => {
expect(data.gen.next().done).toEqual(true);
});
// custom node variables
const customNodeConfigs = [
{
name: 'name',
url: 'url',
port: 443,
network: 'network'
}
];
const customNodeIdFound = 'url:443';
const customNodeIdNotFound = 'notFound';
const customNodeAction = changeNodeIntent(customNodeIdFound);
const customNodeNotFoundAction = changeNodeIntent(customNodeIdNotFound);
data.customNode = handleNodeChangeIntent(customNodeAction);
data.customNodeNotFound = handleNodeChangeIntent(customNodeNotFoundAction);
// test custom node
it('should select getCustomNodeConfig and match race snapshot', () => {
data.customNode.next();
data.customNode.next(defaultNode);
expect(data.customNode.next(defaultNodeConfig).value).toEqual(
select(getCustomNodeConfigs)
);
expect(data.customNode.next(customNodeConfigs).value).toMatchSnapshot();
});
// test custom node not found
it('should select getCustomNodeConfig, put showNotification, put changeNode', () => {
data.customNodeNotFound.next();
data.customNodeNotFound.next(defaultNode);
expect(data.customNodeNotFound.next(defaultNodeConfig).value).toEqual(
select(getCustomNodeConfigs)
);
expect(data.customNodeNotFound.next(customNodeConfigs).value).toEqual(
put(
showNotification(
'danger',
`Attempted to switch to unknown node '${
customNodeNotFoundAction.payload
}'`,
5000
)
)
);
expect(data.customNodeNotFound.next().value).toEqual(
put(changeNode(defaultNode, defaultNodeConfig))
);
expect(data.customNodeNotFound.next().done).toEqual(true);
});
});
describe('unsetWeb3Node*', () => {
const fakeAction = {};
const mockNode = 'web3';
const mockNodeConfig = { network: 'ETH' };
const gen = unsetWeb3Node(fakeAction);
it('should select getNode', () => {
expect(gen.next().value).toEqual(select(getNode));
});
it('should select getNodeConfig', () => {
expect(gen.next(mockNode).value).toEqual(select(getNodeConfig));
});
it('should put changeNodeIntent', () => {
expect(gen.next(mockNodeConfig).value).toEqual(
put(changeNodeIntent(equivalentNodeOrDefault(mockNodeConfig)))
);
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
it('should return early if node type is not web3', () => {
const gen1 = unsetWeb3Node({ payload: false });
gen1.next(); //getNode
gen1.next('notWeb3'); //getNodeConfig
expect(gen1.next().done).toEqual(true);
});
it('should return early if wallet type is web3', () => {
const mockWeb3 = {};
const mockAddress = '0x0';
const mockNetwork = 'ETH';
const mockWeb3Wallet = new Web3Wallet(mockWeb3, mockAddress, mockNetwork);
const gen2 = unsetWeb3Node({ payload: mockWeb3Wallet });
gen2.next(); //getNode
gen2.next('web3'); //getNodeConfig
expect(gen2.next().done).toEqual(true);
});
});
describe('equivalentNodeOrDefault', () => {
const originalNodeList = Object.keys(NODES);
const appDefaultNode = configInitialState.nodeSelection;
const mockNodeConfig = {
network: 'ETH',
service: 'fakeService',
lib: new RPCNode('fakeEndpoint'),
estimateGas: false
};
afterEach(() => {
Object.keys(NODES).forEach(node => {
if (originalNodeList.indexOf(node) === -1) {
delete NODES[node];
}
});
});
it('should return node with equivalent network', () => {
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'Kovan'
});
expect(NODES[node].network).toEqual('Kovan');
});
it('should return app default if no eqivalent is found', () => {
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'noEqivalentExists'
});
expect(node).toEqual(appDefaultNode);
});
it('should ignore web3 from node list', () => {
NODES.web3 = {
...mockNodeConfig,
network: 'uniqueToWeb3'
};
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'uniqueToWeb3'
});
expect(node).toEqual(appDefaultNode);
});
});

View File

@ -0,0 +1,220 @@
import { configuredStore } from 'store';
import { INode } from 'libs/nodes/INode';
import { cloneableGenerator } from 'redux-saga/utils';
import { all, apply, fork, put, select } from 'redux-saga/effects';
import RpcNode from 'libs/nodes/rpc';
import { getDesiredToken, getWallets } from 'selectors/deterministicWallets';
import { getTokens } from 'selectors/wallet';
import { getNodeLib } from 'selectors/config';
import * as dWalletActions from 'actions/deterministicWallets';
import { Token } from 'config/data';
import {
getDeterministicWallets,
updateWalletValues,
updateWalletTokenValues
} from 'sagas/deterministicWallets';
import { TokenValue, Wei } from 'libs/units';
// init module
configuredStore.getState();
const genWalletData1 = () => ({
index: 0,
address: '0x0',
value: TokenValue('100'),
tokenValues: {
OMG: {
value: TokenValue('100'),
decimal: 16
}
}
});
const genWalletData2 = () => ({
index: 1,
address: '0x1',
value: TokenValue('100'),
tokenValues: {
BAT: {
value: TokenValue('100'),
decimal: 16
}
}
});
const genBalances = () => [Wei('100'), Wei('200')];
describe('getDeterministicWallets*', () => {
describe('starting from seed', () => {
const dWallet = {
seed:
'1ba4b713b9cf6f91e8e2eea015fc4e107452fa7d8ade32322207967371e5c0fb93289d4dde94ce13625ecc60279d211b6d677c67f54b9e97c7e68afc9ca1b5ea',
dPath: "m/44'/60'/0'/0"
};
const action = dWalletActions.getDeterministicWallets(dWallet);
const gen = getDeterministicWallets(action);
it('should match put snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
});
it('should fork updateWalletValues', () => {
expect(gen.next().value).toEqual(fork(updateWalletValues));
});
it('should fork updateWalletTokenValues', () => {
expect(gen.next().value).toEqual(fork(updateWalletTokenValues));
});
});
describe('starting from publicKey & chainCode', () => {
const dWallet = {
dPath: '',
publicKey:
'02fcba7ecf41bc7e1be4ee122d9d22e3333671eb0a3a87b5cdf099d59874e1940f',
chainCode:
'180c998615636cd875aa70c71cfa6b7bf570187a56d8c6d054e60b644d13e9d3',
limit: 10,
offset: 0
};
const action = dWalletActions.getDeterministicWallets(dWallet);
const gen = getDeterministicWallets(action);
it('should match put snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
});
it('should fork updateWalletValues', () => {
expect(gen.next().value).toEqual(fork(updateWalletValues));
});
it('should fork updateWalletTokenValues', () => {
expect(gen.next().value).toEqual(fork(updateWalletTokenValues));
});
});
});
describe('updateWalletValues*', () => {
const walletData1 = genWalletData1();
const walletData2 = genWalletData2();
const wallets: dWalletActions.DeterministicWalletData[] = [
walletData1,
walletData2
];
const balances = genBalances();
const node: INode = new RpcNode('');
const gen = updateWalletValues();
it('should select getNodeLib', () => {
expect(gen.next().value).toEqual(select(getNodeLib));
});
it('should select getWallets', () => {
expect(gen.next(node).value).toEqual(select(getWallets));
});
it('should get balance for all wallets', () => {
expect(gen.next(wallets).value).toEqual(
all([
apply(node, node.getBalance, [walletData1.address]),
apply(node, node.getBalance, [walletData2.address])
])
);
});
it('should put updateDeterministicWallet for wallet1', () => {
expect(gen.next(balances).value).toEqual(
put(
dWalletActions.updateDeterministicWallet({
...walletData1,
value: balances[0]
})
)
);
});
it('should put updateDeterministicWallet for wallet2', () => {
expect(gen.next(balances).value).toEqual(
put(
dWalletActions.updateDeterministicWallet({
...walletData2,
value: balances[1]
})
)
);
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('updateWalletTokenValues*', () => {
const walletData1 = genWalletData1();
const walletData2 = genWalletData2();
const wallets: dWalletActions.DeterministicWalletData[] = [
walletData1,
walletData2
];
const node: INode = new RpcNode('');
const token1: Token = {
address: '0x2',
symbol: 'OMG',
decimal: 16
};
const token2: Token = {
address: '0x3',
symbol: 'BAT',
decimal: 16
};
const tokens = [token1, token2];
const tokenBalances = [TokenValue('100'), TokenValue('200')];
const desiredToken = 'OMG';
const data = {} as any;
data.gen = cloneableGenerator(updateWalletTokenValues)();
it('should select getDesiredToken', () => {
expect(data.gen.next().value).toEqual(select(getDesiredToken));
});
it('should return if desired token is falsey', () => {
data.clone1 = data.gen.clone();
data.clone1.next();
expect(data.clone1.next().done).toEqual(true);
});
it('should select getTokens', () => {
data.clone2 = data.gen.clone();
expect(data.gen.next(desiredToken).value).toEqual(select(getTokens));
});
it('should return if desired token is not amongst tokens', () => {
data.clone2.next('fakeDesiredToken');
expect(data.clone2.next(tokens).done).toEqual(true);
});
it('should select getNodeLib', () => {
expect(data.gen.next(tokens).value).toEqual(select(getNodeLib));
});
it('should select getWallets', () => {
expect(data.gen.next(node).value).toEqual(select(getWallets));
});
it('should match snapshot of wallet token balances', () => {
expect(data.gen.next(wallets).value).toMatchSnapshot();
});
it('should match snapshot for put wallet1 update', () => {
expect(data.gen.next(tokenBalances)).toMatchSnapshot();
});
it('should match snapshot for put wallet2 update', () => {
expect(data.gen.next()).toMatchSnapshot();
});
it('should be done', () => {
expect(data.gen.next().done).toEqual(true);
});
});

View File

@ -0,0 +1,44 @@
import { delay } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import { handleNotification } from 'sagas/notifications';
import {
ShowNotificationAction,
showNotification,
closeNotification
} from 'actions/notifications';
describe('handleNotification*', () => {
const level = 'success';
const msg = 'msg';
const duration = 10;
const notificationAction1: ShowNotificationAction = showNotification(
level,
msg,
duration
);
const notificationAction2: ShowNotificationAction = showNotification(
level,
msg,
0
);
const gen1 = handleNotification(notificationAction1);
const gen2 = handleNotification(notificationAction2);
it('should call delay with duration', () => {
expect(gen1.next(notificationAction1).value).toEqual(call(delay, duration));
});
it('should return when duration is zero', () => {
expect(gen2.next(notificationAction2).done).toEqual(true);
});
it('should put closeNotification', () => {
expect(gen1.next(notificationAction1).value).toEqual(
put(closeNotification(notificationAction1.payload))
);
});
it('should be done', () => {
expect(gen1.next().done).toEqual(true);
});
});

View File

@ -0,0 +1,371 @@
import { showNotification } from 'actions/notifications';
import {
bityOrderCreateFailedSwap,
bityOrderCreateSucceededSwap,
bityOrderCreateRequestedSwap,
BityOrderPostResponse,
BityOrderInput,
BityOrderOutput,
BityOrderResponse,
changeStepSwap,
orderStatusRequestedSwap,
orderStatusSucceededSwap,
orderTimeSwap,
startOrderTimerSwap,
startPollBityOrderStatus,
stopLoadBityRatesSwap,
stopPollBityOrderStatus
} from 'actions/swap';
import { getOrderStatus, postOrder } from 'api/bity';
import {
State as SwapState,
INITIAL_STATE as INITIAL_SWAP_STATE
} from 'reducers/swap';
import { delay } from 'redux-saga';
import {
call,
cancel,
cancelled,
fork,
put,
select,
take,
takeEvery
} from 'redux-saga/effects';
import {
getSwap,
pollBityOrderStatus,
pollBityOrderStatusSaga,
postBityOrderCreate,
postBityOrderSaga,
bityTimeRemaining,
BITY_TIMEOUT_MESSAGE
} from 'sagas/swap/orders';
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
const ONE_SECOND = 1000;
const TEN_SECONDS = ONE_SECOND * 10;
const ELEVEN_SECONDS = ONE_SECOND * 11;
const orderInput: BityOrderInput = {
amount: 'amount',
currency: 'currency',
reference: 'reference',
status: 'status'
};
const orderOutput: BityOrderOutput = {
amount: 'amount',
currency: 'currency',
reference: 'reference',
status: 'status'
};
describe('pollBityOrderStatus*', () => {
const data = {} as any;
data.gen = cloneableGenerator(pollBityOrderStatus)();
const fakeSwap: SwapState = {
...INITIAL_SWAP_STATE,
orderId: '1'
};
const orderResponse: BityOrderResponse = {
input: orderInput,
output: orderOutput,
status: 'status'
};
const cancelledSwap = 'CANC';
const successStatus = {
error: null,
data: orderResponse
};
const errorStatus = {
error: true,
msg: 'error message'
};
let random;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should select getSwap', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
it('should put orderStatusRequestedSwap', () => {
expect(data.gen.next(fakeSwap).value).toEqual(
put(orderStatusRequestedSwap())
);
});
it('should call getOrderStatus with swap.orderId', () => {
expect(data.gen.next().value).toEqual(
call(getOrderStatus, fakeSwap.orderId)
);
});
it('should put showNotfication on error', () => {
data.clone = data.gen.clone();
expect(data.clone.next(errorStatus).value).toEqual(
put(
showNotification(
'danger',
`Bity Error: ${errorStatus.msg}`,
TEN_SECONDS
)
)
);
});
it('should put orderStatusSucceededSwap', () => {
expect(data.gen.next(successStatus).value).toEqual(
put(orderStatusSucceededSwap(successStatus.data))
);
});
it('should call delay for 5 seconds', () => {
expect(data.gen.next().value).toEqual(call(delay, ONE_SECOND * 5));
});
it('should select getSwap', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
it('should break loop if swap is cancelled', () => {
data.clone2 = data.gen.clone();
expect(data.clone2.next(cancelledSwap).value).toEqual(cancelled());
expect(data.clone2.next().done).toEqual(true);
});
it('should restart loop', () => {
expect(data.gen.next(fakeSwap).value).toEqual(
put(orderStatusRequestedSwap())
);
});
});
describe('pollBityOrderStatusSaga*', () => {
const data = {} as any;
data.gen = cloneableGenerator(pollBityOrderStatusSaga)();
const mockedTask = createMockTask();
it('should take SWAP_START_POLL_BITY_ORDER_STATUS', () => {
expect(data.gen.next().value).toEqual(
take('SWAP_START_POLL_BITY_ORDER_STATUS')
);
});
it('should be done if order status is false', () => {
data.clone = data.gen.clone();
expect(data.clone.next(false).done).toEqual(true);
});
it('should fork pollBityOrderStatus', () => {
expect(data.gen.next(true).value).toEqual(fork(pollBityOrderStatus));
});
it('should take SWAP_STOP_POLL_BITY_ORDER_STATUS', () => {
expect(data.gen.next(mockedTask).value).toEqual(
take('SWAP_STOP_POLL_BITY_ORDER_STATUS')
);
});
it('should cancel pollBityOrderStatusTask', () => {
expect(data.gen.next().value).toEqual(cancel(mockedTask));
});
});
describe('postBityOrderCreate*', () => {
const amount = 100;
const destinationAddress = '0x0';
const pair = 'BTC_ETH';
const action = bityOrderCreateRequestedSwap(amount, destinationAddress, pair);
const orderResp: BityOrderPostResponse = {
payment_address: '0x0',
status: 'status',
input: orderInput,
output: orderOutput,
timestamp_created: 'timestamp_created',
validFor: 10,
id: '0'
};
const successOrder = { error: false, data: orderResp };
const errorOrder = { error: true, msg: 'error msg' };
const connectionErrMsg =
'Connection Error. Please check the developer console for more details and/or contact support';
const data = {} as any;
data.gen = cloneableGenerator(postBityOrderCreate)(action);
let random;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should put stopLoadBityRatesSwap', () => {
expect(data.gen.next().value).toEqual(put(stopLoadBityRatesSwap()));
});
it('should call postOrder', () => {
data.clone1 = data.gen.clone();
expect(data.gen.next().value).toEqual(
call(postOrder, amount, destinationAddress, action.payload.mode, pair)
);
});
it('should put bityOrderCreateSucceededSwap', () => {
data.clone2 = data.gen.clone();
expect(data.gen.next(successOrder).value).toEqual(
put(bityOrderCreateSucceededSwap(successOrder.data))
);
});
it('should put changeStepSwap', () => {
expect(data.gen.next().value).toEqual(put(changeStepSwap(3)));
});
it('should put startOrderTimerSwap', () => {
expect(data.gen.next().value).toEqual(put(startOrderTimerSwap()));
});
it('should put startPollBityOrderStatus', () => {
expect(data.gen.next().value).toEqual(put(startPollBityOrderStatus()));
});
// failure modes
it('should handle a connection exeception', () => {
expect(data.clone1.throw().value).toEqual(
put(showNotification('danger', connectionErrMsg, TEN_SECONDS))
);
expect(data.clone1.next().value).toEqual(put(bityOrderCreateFailedSwap()));
expect(data.clone1.next().done).toEqual(true);
});
it('should handle an errored order', () => {
expect(data.clone2.next(errorOrder).value).toEqual(
put(
showNotification('danger', `Bity Error: ${errorOrder.msg}`, TEN_SECONDS)
)
);
expect(data.clone2.next().value).toEqual(put(bityOrderCreateFailedSwap()));
});
});
describe('postBityOrderSaga*', () => {
const gen = postBityOrderSaga();
it('should takeEvery SWAP_ORDER_CREATE_REQUESTED', () => {
expect(gen.next().value).toEqual(
takeEvery('SWAP_ORDER_CREATE_REQUESTED', postBityOrderCreate)
);
});
});
describe('bityTimeRemaining*', () => {
const orderTime = new Date().toISOString();
const orderTimeExpired = new Date().getTime() - ELEVEN_SECONDS;
const swapValidFor = 10; //seconds
const swapOrder = {
...INITIAL_SWAP_STATE,
orderTimestampCreatedISOString: orderTime,
validFor: swapValidFor
};
const swapOrderExpired = {
...INITIAL_SWAP_STATE,
orderTimestampCreatedISOString: new Date(orderTimeExpired).toISOString(),
validFor: swapValidFor
};
let random;
const data = {} as any;
data.gen = cloneableGenerator(bityTimeRemaining)();
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should take SWAP_ORDER_START_TIMER', () => {
expect(data.gen.next().value).toEqual(take('SWAP_ORDER_START_TIMER'));
});
it('should break while loop when take SWAP_ORDER_START_TIMER is false', () => {
data.clone1 = data.gen.clone();
expect(data.clone1.next().done).toEqual(true);
});
it('should call delay of one second', () => {
expect(data.gen.next(true).value).toEqual(call(delay, ONE_SECOND));
});
it('should select getSwap', () => {
expect(data.gen.next().value).toEqual(select(getSwap));
});
it('should handle if isValidUntil.isAfter(now)', () => {
data.clone2 = data.gen.clone();
const result = data.clone2.next(swapOrder).value;
expect(result).toHaveProperty('PUT');
expect(result.PUT.action.type).toEqual('SWAP_ORDER_TIME');
expect(result.PUT.action.payload).toBeGreaterThan(0);
});
it('should handle an OPEN order state', () => {
const openOrder = { ...swapOrderExpired, orderStatus: 'OPEN' };
data.OPEN = data.gen.clone();
expect(data.OPEN.next(openOrder).value).toEqual(put(orderTimeSwap(0)));
expect(data.OPEN.next().value).toEqual(put(stopPollBityOrderStatus()));
expect(data.OPEN.next().value).toEqual(
put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
);
expect(data.OPEN.next().value).toEqual(
put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a CANC order state', () => {
const cancOrder = { ...swapOrderExpired, orderStatus: 'CANC' };
data.CANC = data.gen.clone();
expect(data.CANC.next(cancOrder).value).toEqual(
put(stopPollBityOrderStatus())
);
expect(data.CANC.next().value).toEqual(
put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
);
expect(data.CANC.next().value).toEqual(
put(showNotification('danger', BITY_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a RCVE order state', () => {
const rcveOrder = { ...swapOrderExpired, orderStatus: 'RCVE' };
data.RCVE = data.gen.clone();
expect(data.RCVE.next(rcveOrder).value).toEqual(
put(showNotification('warning', BITY_TIMEOUT_MESSAGE, Infinity))
);
});
it('should handle a FILL order state', () => {
const fillOrder = { ...swapOrderExpired, orderStatus: 'FILL' };
data.FILL = data.gen.clone();
expect(data.FILL.next(fillOrder).value).toEqual(
put(stopPollBityOrderStatus())
);
expect(data.FILL.next().value).toEqual(
put({ type: 'SWAP_STOP_LOAD_BITY_RATES' })
);
});
});

View File

@ -0,0 +1,86 @@
import { showNotification } from 'actions/notifications';
import { loadBityRatesSucceededSwap } from 'actions/swap';
import { getAllRates } from 'api/bity';
import { delay } from 'redux-saga';
import { call, cancel, fork, put, take, takeLatest } from 'redux-saga/effects';
import { createMockTask } from 'redux-saga/utils';
import { Pairs } from 'actions/swap/actionTypes';
import {
loadBityRates,
handleBityRates,
getBityRatesSaga
} from 'sagas/swap/rates';
describe('loadBityRates*', () => {
const gen1 = loadBityRates();
const gen2 = loadBityRates();
const rates: Pairs = {
ETHBTC: 1,
ETHREP: 2,
BTCETH: 3,
BTCREP: 4
};
let random;
beforeAll(() => {
random = Math.random;
Math.random = () => 0.001;
});
afterAll(() => {
Math.random = random;
});
it('should call getAllRates', () => {
expect(gen1.next().value).toEqual(call(getAllRates));
});
it('should put loadBityRatesSucceededSwap', () => {
expect(gen1.next(rates).value).toEqual(
put(loadBityRatesSucceededSwap(rates))
);
});
it('should call delay for 5 seconds', () => {
expect(gen1.next().value).toEqual(call(delay, 30000));
});
it('should handle an exception', () => {
const err = { message: 'error' };
gen2.next();
expect((gen2 as any).throw(err).value).toEqual(
put(showNotification('danger', err.message))
);
});
});
describe('handleBityRates*', () => {
const gen = handleBityRates();
const mockTask = createMockTask();
it('should fork loadBityRates', () => {
expect(gen.next().value).toEqual(fork(loadBityRates));
});
it('should take SWAP_STOP_LOAD_BITY_RATES', () => {
expect(gen.next(mockTask).value).toEqual(take('SWAP_STOP_LOAD_BITY_RATES'));
});
it('should cancel loadBityRatesTask', () => {
expect(gen.next().value).toEqual(cancel(mockTask));
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('getBityRatesSaga*', () => {
const gen = getBityRatesSaga();
it('should takeLatest SWAP_LOAD_RATES_REQUESTED', () => {
expect(gen.next().value).toEqual(
takeLatest('SWAP_LOAD_BITY_RATES_REQUESTED', handleBityRates)
);
});
});

327
spec/sagas/wallet.spec.tsx Normal file
View File

@ -0,0 +1,327 @@
import { configuredStore } from 'store';
import RpcNode from 'libs/nodes/rpc';
import {
broadcastTxSucceded,
setBalanceFullfilled,
setBalancePending,
unlockPrivateKey as unlockPrivateKeyActionGen,
unlockKeystore as unlockKeystoreActionGen,
unlockMnemonic as unlockMnemonicActionGen,
broadcastTx as broadcastTxActionGen
} from 'actions/wallet';
import { Wei } from 'libs/units';
import { changeNodeIntent } from 'actions/config';
import { INode } from 'libs/nodes/INode';
import { initWeb3Node, Token } from 'config/data';
import { apply, call, cps, fork, put, select } from 'redux-saga/effects';
import { getNetworkConfig, getNodeLib } from 'selectors/config';
import { getTokens, getWalletInst } from 'selectors/wallet';
import {
updateAccountBalance,
updateTokenBalances,
updateBalances,
unlockPrivateKey,
unlockKeystore,
unlockMnemonic,
unlockWeb3,
broadcastTx
} from 'sagas/wallet';
import { PrivKeyWallet } from 'libs/wallet/non-deterministic';
// init module
configuredStore.getState();
const pkey = '31e97f395cabc6faa37d8a9d6bb185187c35704e7b976c7a110e2f0eab37c344';
const wallet = PrivKeyWallet(Buffer.from(pkey, 'hex'));
const address = '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854';
const balance = Wei('100');
const node: INode = new RpcNode('');
const token1: Token = {
address: '0x2',
symbol: 'OMG',
decimal: 16
};
const token2: Token = {
address: '0x3',
symbol: 'BAT',
decimal: 16
};
const tokens = [token1, token2];
const balances = [Wei('100'), Wei('200')];
const utcKeystore = {
version: 3,
id: 'cb788af4-993d-43ad-851b-0d2031e52c61',
address: '25a24679f35e447f778cf54a3823facf39904a63',
Crypto: {
ciphertext:
'4193915c560835d00b2b9ff5dd20f3e13793b2a3ca8a97df649286063f27f707',
cipherparams: {
iv: 'dccb8c009b11d1c6226ba19b557dce4c'
},
cipher: 'aes-128-ctr',
kdf: 'scrypt',
kdfparams: {
dklen: 32,
salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab',
n: 1024,
r: 8,
p: 1
},
mac: '774fbe4bf35e7e28df15cd6c3546e74ce6608e9ab68a88d50227858a3b05769a'
}
};
// necessary so we can later inject a mocked web3 to the window
declare var window: any;
describe('updateAccountBalance*', () => {
const gen1 = updateAccountBalance();
const gen2 = updateAccountBalance();
it('should put setBalancePending', () => {
expect(gen1.next().value).toEqual(put(setBalancePending()));
});
it('should select getWalletInst', () => {
expect(gen1.next().value).toEqual(select(getWalletInst));
});
it('should return if wallet is falsey', () => {
gen2.next();
gen2.next();
gen2.next(null);
expect(gen2.next().done).toBe(true);
});
it('should select getNodeLib', () => {
expect(gen1.next(wallet).value).toEqual(select(getNodeLib));
});
it('should apply wallet.getAddressString', () => {
expect(gen1.next(node).value).toEqual(
apply(wallet, wallet.getAddressString)
);
});
it('should apply node.getBalance', () => {
expect(gen1.next(address).value).toEqual(
apply(node, node.getBalance, [address])
);
});
it('should put setBalanceFulfilled', () => {
expect(gen1.next(balance).value).toEqual(
put(setBalanceFullfilled(balance))
);
});
it('should be done', () => {
expect(gen1.next().done).toEqual(true);
});
});
describe('updateTokenBalances*', () => {
const gen1 = updateTokenBalances();
const gen2 = updateTokenBalances();
const gen3 = updateTokenBalances();
it('should select getNodeLib', () => {
expect(gen1.next().value).toEqual(select(getNodeLib));
});
it('should select getWalletInst', () => {
expect(gen1.next(node).value).toEqual(select(getWalletInst));
});
it('should select getTokens', () => {
expect(gen1.next(wallet).value).toEqual(select(getTokens));
});
it('should return if wallet is falsey', () => {
gen2.next();
gen2.next(node);
gen2.next(null);
expect(gen2.next().done).toEqual(true);
});
it('should return if node is falsey', () => {
gen3.next();
gen3.next(null);
gen3.next(wallet);
expect(gen3.next().done).toEqual(true);
});
it('should apply wallet.getAddressString', () => {
expect(gen1.next(tokens).value).toEqual(
apply(wallet, wallet.getAddressString)
);
});
it('should apply node.getTokenBalances', () => {
expect(gen1.next(address).value).toEqual(
apply(node, node.getTokenBalances, [address, tokens])
);
});
it('should match put setTokenBalances snapshot', () => {
expect(gen1.next(balances).value).toMatchSnapshot();
});
it('should be done', () => {
expect(gen1.next().done).toEqual(true);
});
});
describe('updateBalances*', () => {
const gen = updateBalances();
it('should fork updateAccountBalance', () => {
expect(gen.next().value).toEqual(fork(updateAccountBalance));
});
it('should fork updateTokenBalances', () => {
expect(gen.next().value).toEqual(fork(updateTokenBalances));
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('unlockPrivateKey', () => {
const value = {
key: pkey,
password: ''
};
const action = unlockPrivateKeyActionGen(value);
const gen = unlockPrivateKey(action);
it('should match put setWallet snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('unlockKeystore*', () => {
const action = unlockKeystoreActionGen({
file: JSON.stringify(utcKeystore),
password: 'testtesttest'
});
const gen = unlockKeystore(action);
it('should match put setWallet snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('unlockMnemonic*', () => {
const action = unlockMnemonicActionGen({
phrase:
'first catalog away faculty jelly now life kingdom pigeon raise gain accident',
pass: '',
path: "m/44'/60'/0'/0/8",
address: '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854'
});
const gen = unlockMnemonic(action);
it('should match put setWallet snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
});
it('should be done', () => {
expect(gen.next().done).toEqual(true);
});
});
describe('unlockWeb3*', () => {
const gen = unlockWeb3();
const accounts = [address];
window.web3 = {
eth: {
getAccounts: jest.fn(cb => cb(undefined, accounts))
},
version: {
getNetwork: jest.fn(cb => cb(undefined, '1'))
},
network: '1'
};
beforeAll(async done => {
await initWeb3Node();
done();
});
afterAll(() => {
delete window.web3;
});
it('should call initWeb3Node', () => {
expect(gen.next().value).toEqual(call(initWeb3Node));
});
it('should cps web3.eth.getAccounts', () => {
expect(gen.next().value).toEqual(cps(window.web3.eth.getAccounts));
});
it('should put changeNodeIntent', () => {
expect(gen.next(accounts).value).toEqual(put(changeNodeIntent('web3')));
});
it('should match setWallet snapshot', () => {
expect(gen.next().value).toMatchSnapshot();
});
});
describe('broadcastTx*', () => {
const signedTx = 'signedTx';
const txHash = 'txHash';
const action = broadcastTxActionGen(signedTx);
const gen = broadcastTx(action);
const networkConfig = {
blockExplorer: 'foo'
};
let random;
beforeAll(() => {
random = Math.random;
Math.random = jest.fn(() => 0.001);
});
afterAll(() => {
Math.random = random;
});
it('should select getNodeLib', () => {
expect(gen.next().value).toEqual(select(getNodeLib));
});
it('should select getNetworkConfig', () => {
expect(gen.next(node).value).toEqual(select(getNetworkConfig));
});
it('should apply node.sendRawTx', () => {
expect(gen.next(networkConfig).value).toEqual(
apply(node, node.sendRawTx, [signedTx])
);
});
it('should match put showNotifiction snapshot', () => {
expect(gen.next(txHash).value).toMatchSnapshot();
});
it('should put broadcastTxSucceded', () => {
expect(gen.next().value).toEqual(
put(broadcastTxSucceded(txHash, signedTx))
);
});
});