Skip to content

Commit

Permalink
Add more unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Jun 1, 2019
1 parent b5ecb01 commit 1234e98
Show file tree
Hide file tree
Showing 16 changed files with 538 additions and 8 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"tscheck": "tsc --noEmit --project tsconfig.json",
"tslint": "tslint --project .",
"prettier:diff": "prettier --write --config ./.prettierrc --list-different './**/*.ts' './**/*.tsx'",
"test": "jest"
"test": "jest",
"snapshot": "jest -u"
},
"devDependencies": {
"@babel/core": "^7.4.4",
Expand All @@ -23,6 +24,8 @@
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@ledgerhq/hw-transport-mocker": "^4.60.3",
"@ledgerhq/hw-transport-node-hid-noevents": "^4.60.3",
"@types/finalhandler": "^1.1.0",
"@types/hard-source-webpack-plugin": "^1.0.1",
"@types/hdkey": "^0.7.0",
Expand Down
58 changes: 58 additions & 0 deletions src/@types/ledgerhq/hw-transport-mocker/RecordStore.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
declare module '@ledgerhq/hw-transport-mocker/RecordStore' {
export type Queue = [string, string][];

export class RecordStore {
/**
* Create an instance of RecordStore from a string.
*
* @param {string} str The string to create the RecordStore from.
* @return {RecordStore} An instance of RecordStore;
*/
public static fromString(str: string): RecordStore;

public queue: Queue;

/**
* Create a new instance of the RecordStore class.
*
* @param {Queue} queue An optional queue to use.
*/
public constructor(queue?: Queue);

/**
* Get whether the queue is empty.
*
* @return {boolean} TRUE if the queue is empty, FALSE otherwise
*/
public isEmpty(): boolean;

/**
* Record an APDU exchange to the queue.
*
* @param {Buffer} apdu The input data.
* @param {Buffer} out The output data.
*/
public recordExchange(apdu: Buffer, out: Buffer): void;

/**
* Replay a previously recorded APDU exchange. Throws an error if the queue is empty or if the
* recorded APDU is invalid.
*
* @param {Buffer} apdu The input to replay
* @return {Buffer} A Buffer with the previously recorded output data.
*/
public replayExchange(apdu: Buffer): Buffer;

/**
* Ensure the queue is empty. Throws an error if the queue isn't empty.
*/
public ensureQueueEmpty(): void;

/**
* Get the current queue as string.
*
* @return {string} The queue as string.
*/
public toString(): string;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
declare module '@ledgerhq/hw-transport-mocker/createTransportRecorder' {
import Transport from '@ledgerhq/hw-transport';
import { RecordStore } from '@ledgerhq/hw-transport-mocker/RecordStore';

class TransportRecorder<T> extends Transport<T> {
public static recordStore: RecordStore;

public static isSupported: typeof Transport.isSupported;

public static list: typeof Transport.list;
}

type TransportConstructor<T> = new (...args: any[]) => Transport<T>;

type TransportRecorderConstructor<T> = typeof TransportRecorder &
(new (...args: any[]) => TransportRecorder<T>);

/**
* Create a decorated transport, which records any APDU exchanges.
*
* @param {TransportConstructor<T>} DecoratedTransport The transport class to decorate.
* @param {RecordStore} recordStore The RecordStore to record to.
* @return {TransportRecorder<T>} The decorated transport.
*/
export default function<T>(
DecoratedTransport: TransportConstructor<T>,
recordStore: RecordStore
): TransportRecorderConstructor<T>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
declare module '@ledgerhq/hw-transport-mocker/createTransportReplayer' {
import Transport, { Observer, Subscription } from '@ledgerhq/hw-transport';
import { RecordStore } from '@ledgerhq/hw-transport-mocker/RecordStore';

class TransportReplayer<T> extends Transport<T> {
public static isSupported(): Promise<true>;

public static list(): Promise<[null]>;

public static listen(observer: Observer<{ type: 'add'; descriptor: null }>): Subscription;

public static open(): Promise<TransportReplayer<any>>;
}

type TransportConstructor<T> = new (...args: any[]) => Transport<T>;

type TransportReplayerConstructor<T> = typeof TransportReplayer &
(new (...args: any[]) => TransportReplayer<T>);

/**
* Create a decorated transport, which replays any APDU exchanges.
*
* @param {TransportConstructor<T>} DecoratedTransport The transport class to decorate.
* @param {RecordStore} recordStore The RecordStore to replay from.
* @return {TransportReplayer<T>} The decorated transport.
*/
export default function<T>(
DecoratedTransport: TransportConstructor<T>,
recordStore: RecordStore
): TransportReplayerConstructor<T>;
}
12 changes: 12 additions & 0 deletions src/@types/ledgerhq/hw-transport-mocker/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Note: This stuff isn't documented at all, so these declarations may be inaccurate.
*/
declare module '@ledgerhq/hw-transport-mocker' {
export {
default as createTransportRecorder
} from '@ledgerhq/hw-transport-mocker/createTransportRecorder';
export {
default as createTransportReplayer
} from '@ledgerhq/hw-transport-mocker/createTransportReplayer';
export * from '@ledgerhq/hw-transport-mocker/RecordStore';
}
51 changes: 51 additions & 0 deletions src/@types/ledgerhq/hw-transport-node-hid-noevents/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
declare module '@ledgerhq/hw-transport-node-hid-noevents' {
import Transport, { DescriptorEvent, Observer, Subscription } from '@ledgerhq/hw-transport';
import { DeviceModel } from '@ledgerhq/devices';

export default class TransportNodeHid extends Transport<string> {
/**
* List all available descriptors. For a better granularity, use `listen()`.
*
* @return {Promise<string[]>} All available descriptors.
*/
public static list(): Promise<string[]>;

/**
* Listen to all device events for a given Transport. The method takes an Observer of
* DescriptorEvent and returns a Subscription (according to Observable paradigm
* https://github.com/tc39/proposal-observable). Each `listen()` call will first emit all
* potential devices already connected and then will emit events that can come over time, for
* instance if you plug a USB device after `listen()` or a Bluetooth device becomes
* discoverable.
*
* Must be called in the context of a UI click.
*
* @param {Observer} observer The observer object.
* @return A Subcription object on which you can `.unsubscribe()`, to stop listening to
* descriptors.
*/
public static listen(observer: Observer<DescriptorEvent<string>>): Subscription;

/**
* Attempt to create an instance of the Transport with the descriptor.
*
* @param {string} descriptor The descriptor to open the Transport with. If none provided, the
* first available device will be used.
* @return {Promise<Transport<TransportNodeHid>} A Promise with the Transport instance.
*/
public static open(descriptor?: string): Promise<TransportNodeHid>;

public readonly device: any;
public readonly deviceModel?: DeviceModel;
public readonly channel: number;
public readonly packetSize: number;
public readonly disconnected: boolean;

public constructor(device: USBDevice);

/**
* Not used by this specific Transport.
*/
public setScramblekey(): void;
}
}
32 changes: 32 additions & 0 deletions src/wallets/ledger/LedgerMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Transport from '@ledgerhq/hw-transport';
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid-noevents';
import EthereumApp from '@ledgerhq/hw-app-eth';
import Ledger from './Ledger';
import { createTransportRecorder, RecordStore } from '@ledgerhq/hw-transport-mocker';

/**
* Can in theory be used to unit test Ledger devices.
*/
export default class LedgerMock extends Ledger {
public readonly store: RecordStore = new RecordStore();
protected transport: Transport<any> | null = null;
protected app: EthereumApp | null = null;

protected async checkConnection(): Promise<void> {
if (this.transport === null) {
this.transport = await this.getTransport();
this.app = new EthereumApp(this.transport);
}
}

private async getTransport(): Promise<Transport<any>> {
const DecoratedTransport = createTransportRecorder(TransportNodeHid, this.store);
const recorder = await DecoratedTransport.open(undefined);

recorder.on('disconnect', () => {
this.transport = null;
});

return recorder;
}
}
19 changes: 19 additions & 0 deletions tests/utils/__snapshots__/tokens.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`fetches token metadata for ERC-20 tokens 1`] = `
Object {
"address": "0xa74476443119A942dE498590Fe1f2454d7D4aC0d",
"decimals": 18,
"name": "Golem Network Token",
"symbol": "GNT",
}
`;

exports[`fetches token metadata for non-compliant ERC-20 tokens 1`] = `
Object {
"address": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359",
"decimals": 18,
"name": "Dai Stablecoin v1.0",
"symbol": "DAI",
}
`;
59 changes: 59 additions & 0 deletions tests/utils/balance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { providers } from 'ethers';
import { Address } from '../../src/store/network';
import { getEtherBalances, getTokenBalances } from '../../src/utils';
import { Token } from '../../src/store/tokens';

const provider = new providers.InfuraProvider(1, 'bfea47cc97c440a687c8762553739a94');

it('fetches Ether balances for multiple addresses', async () => {
const addresses: Address[] = [
{
address: '0x0000000000000000000000000000000000000000',
path: `m/44'/60'/0'/0/0`
},
{
address: '0x0000000000000000000000000000000000000001',
path: `m/44'/60'/0'/0/1`
},
{
address: '0x0000000000000000000000000000000000000002',
path: `m/44'/60'/0'/0/2`
}
];

const balances = await getEtherBalances(provider, addresses);

expect(balances.length).toBe(3);
expect(balances[0].address).toBe(addresses[0].address);
expect(balances[0].balance).not.toBe('0');
});

it('fetches token balances for multiple addresses', async () => {
const addresses: Address[] = [
{
address: '0x0000000000000000000000000000000000000000',
path: `m/44'/60'/0'/0/0`
},
{
address: '0x0000000000000000000000000000000000000001',
path: `m/44'/60'/0'/0/1`
},
{
address: '0x0000000000000000000000000000000000000002',
path: `m/44'/60'/0'/0/2`
}
];

const token: Token = {
name: 'Dai Stablecoin v1.0',
symbol: 'DAI',
decimals: 18,
address: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359'
};

const balances = await getTokenBalances(provider, addresses, token);

expect(balances.length).toBe(3);
expect(balances[0].address).toBe(addresses[0].address);
expect(balances[0].balance).not.toBe('0');
});
28 changes: 28 additions & 0 deletions tests/utils/chunk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { chunk } from '../../src/utils';

it('creates chunks from an array with a fixed length', () => {
const array = ['foo', 'bar', 'baz', 'qux'];
const chunks = chunk(array, 2);

expect(chunks.length).toBe(2);
expect(chunks[0]).toStrictEqual(['foo', 'bar']);
expect(chunks[1]).toStrictEqual(['baz', 'qux']);
});

it('includes items if the length is uneven', () => {
const array = ['foo', 'bar', 'baz', 'qux'];
const chunks = chunk(array, 3);

expect(chunks.length).toBe(2);
expect(chunks[0]).toStrictEqual(['foo', 'bar', 'baz']);
expect(chunks[1]).toStrictEqual(['qux']);
});

it('does not mutate the original array', () => {
const array = ['foo', 'bar', 'baz', 'qux'];
const copy = [...array];
const chunks = chunk(array, 2);

expect(chunks.length).toBe(2);
expect(array).toStrictEqual(copy);
});
12 changes: 12 additions & 0 deletions tests/utils/dPaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getFullPath } from '../../src/utils';
import { DEFAULT_ETH, LEDGER_LIVE_ETH } from '../../src/config';

it('returns a derivation path with an address index (non-hardened)', () => {
expect(getFullPath(DEFAULT_ETH, 5)).toBe(`m/44'/60'/0'/0/5`);
expect(getFullPath(DEFAULT_ETH, 10)).toBe(`m/44'/60'/0'/0/10`);
});

it('returns a derivation path with an account index (hardened)', () => {
expect(getFullPath(LEDGER_LIVE_ETH, 5)).toBe(`m/44'/60'/5'/0/0`);
expect(getFullPath(LEDGER_LIVE_ETH, 10)).toBe(`m/44'/60'/10'/0/0`);
});
11 changes: 11 additions & 0 deletions tests/utils/ens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { isEnsName } from '../../src/utils';

it('checks for valid ens names', () => {
expect(isEnsName('foobarbaz.eth')).toEqual(true);
expect(isEnsName('abcde12345.eth')).toEqual(true);
});

it('checks for invalid ens names', () => {
expect(isEnsName('foobarbaz.ethfoo')).toEqual(false);
expect(isEnsName('0x0000000000000000000000000000000000000000')).toEqual(false);
});
24 changes: 24 additions & 0 deletions tests/utils/tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { providers } from 'ethers';
import { getTokenInfo } from '../../src/utils';

jest.setTimeout(100000);

const provider = new providers.InfuraProvider(1, 'bfea47cc97c440a687c8762553739a94');

it('fetches token metadata for ERC-20 tokens', async () => {
await expect(
getTokenInfo(provider, '0xa74476443119A942dE498590Fe1f2454d7D4aC0d')
).resolves.toMatchSnapshot();
});

it('fetches token metadata for non-compliant ERC-20 tokens', async () => {
await expect(
getTokenInfo(provider, '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359')
).resolves.toMatchSnapshot();
});

it('throws an error if an address is not a token', async () => {
await expect(
getTokenInfo(provider, '0x0000000000000000000000000000000000000000')
).rejects.toThrow();
});
2 changes: 2 additions & 0 deletions tests/wallets/Ledger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// TODO: Figure out if and how to unit test Ledger
test.todo('Ledger');
Loading

0 comments on commit 1234e98

Please sign in to comment.