Skip to content

Commit 60633b7

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(store): add immutability and serializability runtime checks (#1613)
Closes #857
1 parent 38290ba commit 60633b7

18 files changed

+864
-27
lines changed

modules/router-store/spec/integration.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,13 @@ describe('integration spec', () => {
253253
} else {
254254
const nextState = routerReducer(state, action);
255255
if (nextState && nextState.state) {
256-
nextState.state.root = <any>{};
256+
return {
257+
...nextState,
258+
state: {
259+
...nextState.state,
260+
root: {} as any,
261+
},
262+
};
257263
}
258264
return nextState;
259265
}
@@ -383,7 +389,13 @@ describe('integration spec', () => {
383389
} else {
384390
const nextState = routerReducer(state, action);
385391
if (nextState && nextState.state) {
386-
nextState.state.root = <any>{};
392+
return {
393+
...nextState,
394+
state: {
395+
...nextState.state,
396+
root: {} as any,
397+
},
398+
};
387399
}
388400
return nextState;
389401
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { actionSerializationCheckMetaReducer } from '../../src/meta-reducers';
2+
3+
describe('actionSerializationCheckMetaReducer:', () => {
4+
describe('valid action:', () => {
5+
it('should not throw', () => {
6+
expect(() =>
7+
invokeReducer({ type: 'valid', payload: { id: 47 } })
8+
).not.toThrow();
9+
});
10+
});
11+
12+
describe('invalid action:', () => {
13+
it('should throw', () => {
14+
expect(() =>
15+
invokeReducer({ type: 'invalid', payload: { date: new Date() } })
16+
).toThrow();
17+
});
18+
});
19+
20+
function invokeReducer(action: any) {
21+
actionSerializationCheckMetaReducer(() => {})(undefined, action);
22+
}
23+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { immutabilityCheckMetaReducer } from '../../src/meta-reducers';
2+
3+
describe('immutabilityCheckMetaReducer:', () => {
4+
describe('actions:', () => {
5+
it('should not throw if left untouched', () => {
6+
expect(() => invokeReducer((action: any) => action)).not.toThrow();
7+
});
8+
9+
it('should throw when mutating an action', () => {
10+
expect(() =>
11+
invokeReducer((action: any) => {
12+
action.foo = '123';
13+
})
14+
).toThrow();
15+
expect(() =>
16+
invokeReducer((action: any) => {
17+
action.numbers.push(4);
18+
})
19+
).toThrow();
20+
});
21+
22+
function invokeReducer(reduce: Function) {
23+
immutabilityCheckMetaReducer((state, action) => {
24+
reduce(action);
25+
return state;
26+
})({}, { type: 'invoke', numbers: [1, 2, 3] });
27+
}
28+
});
29+
30+
describe('state:', () => {
31+
it('should not throw if left untouched', () => {
32+
expect(() =>
33+
invokeReducer((state: any) => ({ ...state, foo: 'bar' }))
34+
).not.toThrow();
35+
});
36+
37+
it('should throw when mutating state', () => {
38+
expect(() =>
39+
invokeReducer((state: any) => {
40+
state.foo = '123';
41+
})
42+
).toThrow();
43+
expect(() =>
44+
invokeReducer((state: any) => {
45+
state.numbers.push(4);
46+
})
47+
).toThrow();
48+
});
49+
50+
function invokeReducer(reduce: Function) {
51+
immutabilityCheckMetaReducer((state, _action) => reduce(state))(
52+
{ numbers: [1, 2, 3] },
53+
{ type: 'invoke' }
54+
);
55+
}
56+
});
57+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { stateSerializationCheckMetaReducer } from '../../src/meta-reducers';
2+
3+
describe('stateSerializationCheckMetaReducer:', () => {
4+
describe('valid next state:', () => {
5+
it('should not throw', () => {
6+
expect(() =>
7+
invokeReducer({
8+
nested: { number: 1, null: null },
9+
})
10+
).not.toThrow();
11+
});
12+
});
13+
14+
describe('invalid next state:', () => {
15+
it('should throw', () => {
16+
expect(() => invokeReducer({ nested: { class: new Date() } })).toThrow();
17+
});
18+
});
19+
20+
function invokeReducer(nextState?: any) {
21+
stateSerializationCheckMetaReducer(() => nextState)(undefined, {
22+
type: 'invokeReducer',
23+
});
24+
}
25+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {
2+
getUnserializable,
3+
throwIfUnserializable,
4+
} from '../../src/meta-reducers/utils';
5+
6+
describe('getUnserializable:', () => {
7+
describe('serializable value:', () => {
8+
it('should not throw', () => {
9+
expect(getUnserializable(1)).toBe(false);
10+
expect(getUnserializable(true)).toBe(false);
11+
expect(getUnserializable('string')).toBe(false);
12+
expect(getUnserializable([1, 2, 3])).toBe(false);
13+
expect(getUnserializable({})).toBe(false);
14+
expect(
15+
getUnserializable({
16+
nested: { number: 1, undefined: undefined, null: null },
17+
})
18+
).toBe(false);
19+
});
20+
});
21+
22+
describe('unserializable value:', () => {
23+
it('should throw', () => {
24+
class TestClass {}
25+
26+
expect(getUnserializable()).toEqual({ value: undefined, path: ['root'] });
27+
expect(getUnserializable(null)).toEqual({ value: null, path: ['root'] });
28+
29+
const date = new Date();
30+
expect(getUnserializable({ date })).toEqual({
31+
value: date,
32+
path: ['date'],
33+
});
34+
expect(getUnserializable({ set: new Set([]) })).toEqual({
35+
value: new Set([]),
36+
path: ['set'],
37+
});
38+
expect(getUnserializable({ map: new Map([]) })).toEqual({
39+
value: new Map([]),
40+
path: ['map'],
41+
});
42+
expect(getUnserializable({ class: new TestClass() })).toEqual({
43+
value: new TestClass(),
44+
path: ['class'],
45+
});
46+
expect(
47+
getUnserializable({
48+
nested: { valid: true, class: new TestClass(), alsoValid: '' },
49+
valid: [3],
50+
})
51+
).toEqual({ value: new TestClass(), path: ['nested', 'class'] });
52+
});
53+
});
54+
});
55+
56+
describe('throwIfUnserializable', () => {
57+
describe('serializable', () => {
58+
it('should not throw an error', () => {
59+
expect(() => throwIfUnserializable(false, 'state')).not.toThrow();
60+
});
61+
});
62+
63+
describe('unserializable', () => {
64+
it('should throw an error', () => {
65+
expect(() =>
66+
throwIfUnserializable({ path: ['root'], value: undefined }, 'state')
67+
).toThrowError(`Detected unserializable state at "root"`);
68+
expect(() =>
69+
throwIfUnserializable({ path: ['date'], value: new Date() }, 'action')
70+
).toThrowError(`Detected unserializable action at "date"`);
71+
expect(() =>
72+
throwIfUnserializable(
73+
{
74+
path: ['one', 'two', 'three'],
75+
value: new Date(),
76+
},
77+
'state'
78+
)
79+
).toThrowError(`Detected unserializable state at "one.two.three"`);
80+
});
81+
});
82+
});

0 commit comments

Comments
 (0)