Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add BaseControllerV2 state metadata #371

Merged
merged 1 commit into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 297 additions & 12 deletions src/BaseControllerV2.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import type { Draft } from 'immer';
import * as sinon from 'sinon';

import { BaseController } from './BaseControllerV2';
import { BaseController, getAnonymizedState, getPersistentState } from './BaseControllerV2';

type MockControllerState = {
count: number;
};

const mockControllerStateMetadata = {
count: {
persist: true,
anonymous: true,
},
};

class MockController extends BaseController<MockControllerState> {
update(callback: (state: Draft<MockControllerState>) => void | MockControllerState) {
super.update(callback);
Expand All @@ -19,21 +26,27 @@ class MockController extends BaseController<MockControllerState> {

describe('BaseController', () => {
it('should set initial state', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);

expect(controller.state).toEqual({ count: 0 });
});

it('should set initial schema', () => {
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);

expect(controller.metadata).toEqual(mockControllerStateMetadata);
});

it('should not allow mutating state directly', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);

expect(() => {
controller.state = { count: 1 };
}).toThrow();
});

it('should allow updating state by modifying draft', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);

controller.update((draft) => {
draft.count += 1;
Expand All @@ -43,7 +56,7 @@ describe('BaseController', () => {
});

it('should allow updating state by return a value', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);

controller.update(() => {
return { count: 1 };
Expand All @@ -53,7 +66,7 @@ describe('BaseController', () => {
});

it('should throw an error if update callback modifies draft and returns value', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);

expect(() => {
controller.update((draft) => {
Expand All @@ -64,7 +77,7 @@ describe('BaseController', () => {
});

it('should inform subscribers of state changes', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);
const listener1 = sinon.stub();
const listener2 = sinon.stub();

Expand All @@ -81,7 +94,7 @@ describe('BaseController', () => {
});

it('should inform a subscriber of each state change once even after multiple subscriptions', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);
const listener1 = sinon.stub();

controller.subscribe(listener1);
Expand All @@ -95,7 +108,7 @@ describe('BaseController', () => {
});

it('should no longer inform a subscriber about state changes after unsubscribing', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);
const listener1 = sinon.stub();

controller.subscribe(listener1);
Expand All @@ -108,7 +121,7 @@ describe('BaseController', () => {
});

it('should no longer inform a subscriber about state changes after unsubscribing once, even if they subscribed many times', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);
const listener1 = sinon.stub();

controller.subscribe(listener1);
Expand All @@ -122,7 +135,7 @@ describe('BaseController', () => {
});

it('should allow unsubscribing listeners who were never subscribed', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);
const listener1 = sinon.stub();

expect(() => {
Expand All @@ -131,7 +144,7 @@ describe('BaseController', () => {
});

it('should no longer update subscribers after being destroyed', () => {
const controller = new MockController({ count: 0 });
const controller = new MockController({ count: 0 }, mockControllerStateMetadata);
const listener1 = sinon.stub();
const listener2 = sinon.stub();

Expand All @@ -146,3 +159,275 @@ describe('BaseController', () => {
expect(listener2.callCount).toEqual(0);
});
});

describe('getAnonymizedState', () => {
it('should return empty state', () => {
expect(getAnonymizedState({}, {})).toEqual({});
});

it('should return empty state when no properties are anonymized', () => {
const anonymizedState = getAnonymizedState({ count: 1 }, { count: { anonymous: false, persist: false } });
expect(anonymizedState).toEqual({});
});

it('should return state that is already anonymized', () => {
const anonymizedState = getAnonymizedState(
{
password: 'secret password',
privateKey: '123',
network: 'mainnet',
tokens: ['DAI', 'USDC'],
},
{
password: {
anonymous: false,
persist: false,
},
privateKey: {
anonymous: false,
persist: false,
},
network: {
anonymous: true,
persist: false,
},
tokens: {
anonymous: true,
persist: false,
},
},
);
expect(anonymizedState).toEqual({ network: 'mainnet', tokens: ['DAI', 'USDC'] });
});

it('should use anonymizing function to anonymize state', () => {
const anonymizeTransactionHash = (hash: string) => {
return hash.split('').reverse().join('');
};

const anonymizedState = getAnonymizedState(
{
transactionHash: '0x1234',
},
{
transactionHash: {
anonymous: anonymizeTransactionHash,
persist: false,
},
},
);

expect(anonymizedState).toEqual({ transactionHash: '4321x0' });
});

it('should allow returning a partial object from an anonymizing function', () => {
const anonymizeTxMeta = (txMeta: { hash: string; value: number }) => {
return { value: txMeta.value };
};

const anonymizedState = getAnonymizedState(
{
txMeta: {
hash: '0x123',
value: 10,
},
},
{
txMeta: {
anonymous: anonymizeTxMeta,
persist: false,
},
},
);

expect(anonymizedState).toEqual({ txMeta: { value: 10 } });
});

it('should allow returning a nested partial object from an anonymizing function', () => {
const anonymizeTxMeta = (txMeta: { hash: string; value: number; history: { hash: string; value: number }[] }) => {
return {
history: txMeta.history.map((entry) => {
return { value: entry.value };
}),
value: txMeta.value,
};
};

const anonymizedState = getAnonymizedState(
{
txMeta: {
hash: '0x123',
history: [
{
hash: '0x123',
value: 9,
},
],
value: 10,
},
},
{
txMeta: {
anonymous: anonymizeTxMeta,
persist: false,
},
},
);

expect(anonymizedState).toEqual({ txMeta: { history: [{ value: 9 }], value: 10 } });
});

it('should allow transforming types in an anonymizing function', () => {
const anonymizedState = getAnonymizedState(
{
count: '1',
},
{
count: {
anonymous: (count) => Number(count),
persist: false,
},
},
);

expect(anonymizedState).toEqual({ count: 1 });
});
});

describe('getPersistentState', () => {
it('should return empty state', () => {
expect(getPersistentState({}, {})).toEqual({});
});

it('should return empty state when no properties are persistent', () => {
const persistentState = getPersistentState({ count: 1 }, { count: { anonymous: false, persist: false } });
expect(persistentState).toEqual({});
});

it('should return persistent state', () => {
const persistentState = getPersistentState(
{
password: 'secret password',
privateKey: '123',
network: 'mainnet',
tokens: ['DAI', 'USDC'],
},
{
password: {
anonymous: false,
persist: true,
},
privateKey: {
anonymous: false,
persist: true,
},
network: {
anonymous: false,
persist: false,
},
tokens: {
anonymous: false,
persist: false,
},
},
);
expect(persistentState).toEqual({ password: 'secret password', privateKey: '123' });
});

it('should use function to derive persistent state', () => {
const normalizeTransacitonHash = (hash: string) => {
return hash.toLowerCase();
};

const persistentState = getPersistentState(
{
transactionHash: '0X1234',
},
{
transactionHash: {
anonymous: false,
persist: normalizeTransacitonHash,
},
},
);

expect(persistentState).toEqual({ transactionHash: '0x1234' });
});

it('should allow returning a partial object from a persist function', () => {
const getPersistentTxMeta = (txMeta: { hash: string; value: number }) => {
return { value: txMeta.value };
};

const persistentState = getPersistentState(
{
txMeta: {
hash: '0x123',
value: 10,
},
},
{
txMeta: {
anonymous: false,
persist: getPersistentTxMeta,
},
},
);

expect(persistentState).toEqual({ txMeta: { value: 10 } });
});

it('should allow returning a nested partial object from a persist function', () => {
const getPersistentTxMeta = (txMeta: {
hash: string;
value: number;
history: { hash: string; value: number }[];
}) => {
return {
history: txMeta.history.map((entry) => {
return { value: entry.value };
}),
value: txMeta.value,
};
};

const persistentState = getPersistentState(
{
txMeta: {
hash: '0x123',
history: [
{
hash: '0x123',
value: 9,
},
],
value: 10,
},
},
{
txMeta: {
anonymous: false,
persist: getPersistentTxMeta,
},
},
);

expect(persistentState).toEqual({ txMeta: { history: [{ value: 9 }], value: 10 } });
});

it('should allow transforming types in a persist function', () => {
const persistentState = getPersistentState(
{
count: '1',
},
{
count: {
anonymous: false,
persist: (count) => Number(count),
},
},
);

expect(persistentState).toEqual({ count: 1 });
});
});
Loading