Skip to content

Commit 308c12c

Browse files
committed
chore: bump redux migration
1 parent 38b9ee6 commit 308c12c

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

app/store/migrations/135.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { captureException } from '@sentry/react-native';
2+
import migrate, { migrationVersion } from './135';
3+
import { ensureValidState } from './util';
4+
5+
jest.mock('@sentry/react-native', () => ({
6+
captureException: jest.fn(),
7+
}));
8+
9+
jest.mock('./util', () => ({
10+
ensureValidState: jest.fn(),
11+
}));
12+
13+
const mockedEnsureValidState = jest.mocked(ensureValidState);
14+
const mockedCaptureException = jest.mocked(captureException);
15+
16+
interface TestState {
17+
banners?: {
18+
dismissedBanners?: string[];
19+
lastDismissedBrazeBanner?: string | null;
20+
[key: string]: unknown;
21+
};
22+
[key: string]: unknown;
23+
}
24+
25+
describe(`Migration ${migrationVersion}: Add lastDismissedBrazeBanner to banners slice`, () => {
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
mockedEnsureValidState.mockReturnValue(true);
29+
});
30+
31+
it('returns state unchanged if ensureValidState returns false', () => {
32+
const state = { some: 'state' };
33+
mockedEnsureValidState.mockReturnValue(false);
34+
35+
const result = migrate(state);
36+
37+
expect(result).toBe(state);
38+
});
39+
40+
it('returns state unchanged if banners slice is missing', () => {
41+
const state = { engine: {} };
42+
43+
const result = migrate(state);
44+
45+
expect(result).toBe(state);
46+
});
47+
48+
it('returns state unchanged if state is not an object', () => {
49+
const result = migrate(null);
50+
51+
expect(result).toBeNull();
52+
});
53+
54+
it('adds lastDismissedBrazeBanner with null when the field is absent', () => {
55+
const state: TestState = {
56+
banners: {
57+
dismissedBanners: ['some-banner'],
58+
},
59+
};
60+
61+
const result = migrate(state);
62+
63+
expect(result).toBe(state);
64+
expect((result as TestState).banners?.lastDismissedBrazeBanner).toBeNull();
65+
});
66+
67+
it('does not overwrite lastDismissedBrazeBanner when it already exists', () => {
68+
const state: TestState = {
69+
banners: {
70+
dismissedBanners: [],
71+
lastDismissedBrazeBanner: 'existing-banner-id',
72+
},
73+
};
74+
75+
migrate(state);
76+
77+
expect(state.banners?.lastDismissedBrazeBanner).toBe('existing-banner-id');
78+
});
79+
80+
it('does not overwrite lastDismissedBrazeBanner when it is already null', () => {
81+
const state: TestState = {
82+
banners: {
83+
lastDismissedBrazeBanner: null,
84+
},
85+
};
86+
87+
migrate(state);
88+
89+
expect(state.banners?.lastDismissedBrazeBanner).toBeNull();
90+
});
91+
92+
it('captures exceptions and returns state on unexpected errors', () => {
93+
// Proxy has no own `lastDismissedBrazeBanner` (so hasProperty returns false)
94+
// but throws when any property is set.
95+
const banners = new Proxy({} as Record<string, unknown>, {
96+
set() {
97+
throw new Error('Unexpected migration failure');
98+
},
99+
});
100+
101+
const state: TestState = { banners };
102+
103+
const result = migrate(state);
104+
105+
expect(result).toBe(state);
106+
expect(mockedCaptureException).toHaveBeenCalledWith(
107+
expect.objectContaining({
108+
message: expect.stringContaining(
109+
'Migration 135: Failed to add lastDismissedBrazeBanner',
110+
),
111+
}),
112+
);
113+
});
114+
});

app/store/migrations/135.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { captureException } from '@sentry/react-native';
2+
import { hasProperty, isObject } from '@metamask/utils';
3+
4+
import { ensureValidState } from './util';
5+
6+
export const migrationVersion = 135;
7+
8+
/**
9+
* Migration 132: Add `lastDismissedBrazeBanner` field to the `banners` slice.
10+
*
11+
* Existing installs only have `dismissedBanners`; this migration adds the new
12+
* field with its default value of `null` so the reducer's initial state and
13+
* any rehydrated state are always in sync.
14+
*/
15+
const migration = (state: unknown): unknown => {
16+
if (!ensureValidState(state, migrationVersion)) {
17+
return state;
18+
}
19+
20+
try {
21+
if (!isObject(state) || !isObject(state.banners)) {
22+
return state;
23+
}
24+
25+
const banners = state.banners as Record<string, unknown>;
26+
if (!hasProperty(banners, 'lastDismissedBrazeBanner')) {
27+
banners.lastDismissedBrazeBanner = null;
28+
}
29+
} catch (error) {
30+
captureException(
31+
new Error(
32+
`Migration ${migrationVersion}: Failed to add lastDismissedBrazeBanner: ${String(error)}`,
33+
),
34+
);
35+
}
36+
37+
return state;
38+
};
39+
40+
export default migration;

app/store/migrations/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ import migration131 from './131';
135135
import migration132 from './132';
136136
import migration133 from './133';
137137
import migration134 from './134';
138+
import migration135 from './135';
138139

139140
// Add migrations above this line
140141
import { ControllerStorage } from '../persistConfig';
@@ -289,6 +290,7 @@ export const migrationList: MigrationsList = {
289290
132: migration132,
290291
133: migration133,
291292
134: migration134,
293+
135: migration135,
292294
};
293295

294296
// Enable both synchronous and asynchronous migrations

0 commit comments

Comments
 (0)