Skip to content

Commit

Permalink
feat(store): add immutability and serializability runtime checks (#1613)
Browse files Browse the repository at this point in the history
Closes #857
  • Loading branch information
timdeschryver authored and brandonroberts committed Apr 1, 2019
1 parent 38290ba commit 60633b7
Show file tree
Hide file tree
Showing 18 changed files with 864 additions and 27 deletions.
16 changes: 14 additions & 2 deletions modules/router-store/spec/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,13 @@ describe('integration spec', () => {
} else {
const nextState = routerReducer(state, action);
if (nextState && nextState.state) {
nextState.state.root = <any>{};
return {
...nextState,
state: {
...nextState.state,
root: {} as any,
},
};
}
return nextState;
}
Expand Down Expand Up @@ -383,7 +389,13 @@ describe('integration spec', () => {
} else {
const nextState = routerReducer(state, action);
if (nextState && nextState.state) {
nextState.state.root = <any>{};
return {
...nextState,
state: {
...nextState.state,
root: {} as any,
},
};
}
return nextState;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { actionSerializationCheckMetaReducer } from '../../src/meta-reducers';

describe('actionSerializationCheckMetaReducer:', () => {
describe('valid action:', () => {
it('should not throw', () => {
expect(() =>
invokeReducer({ type: 'valid', payload: { id: 47 } })
).not.toThrow();
});
});

describe('invalid action:', () => {
it('should throw', () => {
expect(() =>
invokeReducer({ type: 'invalid', payload: { date: new Date() } })
).toThrow();
});
});

function invokeReducer(action: any) {
actionSerializationCheckMetaReducer(() => {})(undefined, action);
}
});
57 changes: 57 additions & 0 deletions modules/store/spec/meta-reducers/immutability_reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { immutabilityCheckMetaReducer } from '../../src/meta-reducers';

describe('immutabilityCheckMetaReducer:', () => {
describe('actions:', () => {
it('should not throw if left untouched', () => {
expect(() => invokeReducer((action: any) => action)).not.toThrow();
});

it('should throw when mutating an action', () => {
expect(() =>
invokeReducer((action: any) => {
action.foo = '123';
})
).toThrow();
expect(() =>
invokeReducer((action: any) => {
action.numbers.push(4);
})
).toThrow();
});

function invokeReducer(reduce: Function) {
immutabilityCheckMetaReducer((state, action) => {
reduce(action);
return state;
})({}, { type: 'invoke', numbers: [1, 2, 3] });
}
});

describe('state:', () => {
it('should not throw if left untouched', () => {
expect(() =>
invokeReducer((state: any) => ({ ...state, foo: 'bar' }))
).not.toThrow();
});

it('should throw when mutating state', () => {
expect(() =>
invokeReducer((state: any) => {
state.foo = '123';
})
).toThrow();
expect(() =>
invokeReducer((state: any) => {
state.numbers.push(4);
})
).toThrow();
});

function invokeReducer(reduce: Function) {
immutabilityCheckMetaReducer((state, _action) => reduce(state))(
{ numbers: [1, 2, 3] },
{ type: 'invoke' }
);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { stateSerializationCheckMetaReducer } from '../../src/meta-reducers';

describe('stateSerializationCheckMetaReducer:', () => {
describe('valid next state:', () => {
it('should not throw', () => {
expect(() =>
invokeReducer({
nested: { number: 1, null: null },
})
).not.toThrow();
});
});

describe('invalid next state:', () => {
it('should throw', () => {
expect(() => invokeReducer({ nested: { class: new Date() } })).toThrow();
});
});

function invokeReducer(nextState?: any) {
stateSerializationCheckMetaReducer(() => nextState)(undefined, {
type: 'invokeReducer',
});
}
});
82 changes: 82 additions & 0 deletions modules/store/spec/meta-reducers/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
getUnserializable,
throwIfUnserializable,
} from '../../src/meta-reducers/utils';

describe('getUnserializable:', () => {
describe('serializable value:', () => {
it('should not throw', () => {
expect(getUnserializable(1)).toBe(false);
expect(getUnserializable(true)).toBe(false);
expect(getUnserializable('string')).toBe(false);
expect(getUnserializable([1, 2, 3])).toBe(false);
expect(getUnserializable({})).toBe(false);
expect(
getUnserializable({
nested: { number: 1, undefined: undefined, null: null },
})
).toBe(false);
});
});

describe('unserializable value:', () => {
it('should throw', () => {
class TestClass {}

expect(getUnserializable()).toEqual({ value: undefined, path: ['root'] });
expect(getUnserializable(null)).toEqual({ value: null, path: ['root'] });

const date = new Date();
expect(getUnserializable({ date })).toEqual({
value: date,
path: ['date'],
});
expect(getUnserializable({ set: new Set([]) })).toEqual({
value: new Set([]),
path: ['set'],
});
expect(getUnserializable({ map: new Map([]) })).toEqual({
value: new Map([]),
path: ['map'],
});
expect(getUnserializable({ class: new TestClass() })).toEqual({
value: new TestClass(),
path: ['class'],
});
expect(
getUnserializable({
nested: { valid: true, class: new TestClass(), alsoValid: '' },
valid: [3],
})
).toEqual({ value: new TestClass(), path: ['nested', 'class'] });
});
});
});

describe('throwIfUnserializable', () => {
describe('serializable', () => {
it('should not throw an error', () => {
expect(() => throwIfUnserializable(false, 'state')).not.toThrow();
});
});

describe('unserializable', () => {
it('should throw an error', () => {
expect(() =>
throwIfUnserializable({ path: ['root'], value: undefined }, 'state')
).toThrowError(`Detected unserializable state at "root"`);
expect(() =>
throwIfUnserializable({ path: ['date'], value: new Date() }, 'action')
).toThrowError(`Detected unserializable action at "date"`);
expect(() =>
throwIfUnserializable(
{
path: ['one', 'two', 'three'],
value: new Date(),
},
'state'
)
).toThrowError(`Detected unserializable state at "one.two.three"`);
});
});
});
Loading

0 comments on commit 60633b7

Please sign in to comment.