Skip to content

Commit c81c7eb

Browse files
chore(runway): cherry-pick fix: migration order to cherry pick the phishing controller migration in 7.47 (#16149)
- fix: cp-7.47.0 migration order to cherry pick the phishing controller migration in 7.47 (#16137)
1 parent a063bf7 commit c81c7eb

3 files changed

Lines changed: 192 additions & 2 deletions

File tree

app/store/migrations/078.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import migrate from './078';
2+
import { ensureValidState } from './util';
3+
import { captureException } from '@sentry/react-native';
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 mockedCaptureException = jest.mocked(captureException);
14+
const mockedEnsureValidState = jest.mocked(ensureValidState);
15+
16+
describe('Migration 078: Reset PhishingController phishingLists', () => {
17+
beforeEach(() => {
18+
jest.resetAllMocks();
19+
});
20+
21+
it('returns state unchanged if ensureValidState fails', () => {
22+
const state = { some: 'state' };
23+
24+
mockedEnsureValidState.mockReturnValue(false);
25+
26+
const migratedState = migrate(state);
27+
28+
expect(migratedState).toBe(state);
29+
expect(mockedCaptureException).not.toHaveBeenCalled();
30+
});
31+
32+
it('captures exception if PhishingController state is invalid', () => {
33+
const state = {
34+
engine: {
35+
backgroundState: {
36+
// PhishingController is missing
37+
},
38+
},
39+
};
40+
41+
mockedEnsureValidState.mockReturnValue(true);
42+
43+
const migratedState = migrate(state);
44+
45+
expect(migratedState).toEqual(state);
46+
expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
47+
expect(mockedCaptureException.mock.calls[0][0].message).toContain(
48+
'Migration 078: Invalid PhishingController state',
49+
);
50+
});
51+
52+
it('resets PhishingController phishingLists to empty array and stalelistLastFetched to 0 while preserving other fields', () => {
53+
interface TestState {
54+
engine: {
55+
backgroundState: {
56+
PhishingController: {
57+
c2DomainBlocklistLastFetched: number;
58+
phishingLists: string[];
59+
whitelist: string[];
60+
hotlistLastFetched: number;
61+
stalelistLastFetched: number;
62+
extraProperty?: string;
63+
};
64+
OtherController: {
65+
shouldStayUntouched: boolean;
66+
};
67+
};
68+
};
69+
}
70+
71+
const state: TestState = {
72+
engine: {
73+
backgroundState: {
74+
PhishingController: {
75+
c2DomainBlocklistLastFetched: 123456789,
76+
phishingLists: ['list1', 'list2'],
77+
whitelist: ['site1', 'site2'],
78+
hotlistLastFetched: 987654321,
79+
stalelistLastFetched: 123123123,
80+
extraProperty: 'should remain',
81+
},
82+
OtherController: {
83+
shouldStayUntouched: true,
84+
},
85+
},
86+
},
87+
};
88+
89+
mockedEnsureValidState.mockReturnValue(true);
90+
91+
const migratedState = migrate(state) as typeof state;
92+
93+
// PhishingLists should be reset to empty array and stalelistLastFetched to 0
94+
expect(migratedState.engine.backgroundState.PhishingController.phishingLists).toEqual([]);
95+
expect(migratedState.engine.backgroundState.PhishingController.stalelistLastFetched).toBe(0);
96+
97+
// Other fields should remain unchanged
98+
expect(migratedState.engine.backgroundState.PhishingController.c2DomainBlocklistLastFetched).toBe(123456789);
99+
expect(migratedState.engine.backgroundState.PhishingController.whitelist).toEqual(['site1', 'site2']);
100+
expect(migratedState.engine.backgroundState.PhishingController.hotlistLastFetched).toBe(987654321);
101+
expect(migratedState.engine.backgroundState.PhishingController.extraProperty).toBe('should remain');
102+
103+
// Other controllers should remain untouched
104+
expect(migratedState.engine.backgroundState.OtherController).toEqual({
105+
shouldStayUntouched: true,
106+
});
107+
108+
expect(mockedCaptureException).not.toHaveBeenCalled();
109+
});
110+
111+
it('handles error during migration', () => {
112+
// Create state with a PhishingController that throws when phishingLists is accessed
113+
const state = {
114+
engine: {
115+
backgroundState: {
116+
PhishingController: Object.defineProperty({}, 'phishingLists', {
117+
get: () => {
118+
throw new Error('Test error');
119+
},
120+
set: () => {
121+
throw new Error('Test error');
122+
},
123+
configurable: true,
124+
enumerable: true,
125+
}),
126+
},
127+
},
128+
};
129+
130+
mockedEnsureValidState.mockReturnValue(true);
131+
132+
const migratedState = migrate(state);
133+
134+
expect(migratedState).toEqual(state);
135+
expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
136+
expect(mockedCaptureException.mock.calls[0][0].message).toContain(
137+
'Migration 078: cleaning PhishingController state failed with error',
138+
);
139+
});
140+
});

app/store/migrations/078.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { hasProperty, isObject } from '@metamask/utils';
2+
import { ensureValidState } from './util';
3+
import { captureException } from '@sentry/react-native';
4+
5+
/**
6+
* Migration 78: Reset PhishingController phishingLists
7+
*
8+
* This migration resets only the phishingLists array in the PhishingController state
9+
* while preserving all other state properties. This allows the app to rebuild the lists
10+
* while maintaining user preferences and configuration.
11+
*/
12+
const migration = (state: unknown): unknown => {
13+
const migrationVersion = 78;
14+
15+
if (!ensureValidState(state, migrationVersion)) {
16+
return state;
17+
}
18+
19+
try {
20+
if (
21+
!hasProperty(state.engine.backgroundState, 'PhishingController') ||
22+
!isObject(state.engine.backgroundState.PhishingController)
23+
) {
24+
captureException(
25+
new Error(
26+
`Migration 078: Invalid PhishingController state: '${JSON.stringify(
27+
state.engine.backgroundState.PhishingController,
28+
)}'`,
29+
),
30+
);
31+
return state;
32+
}
33+
34+
// Only reset the phishingLists field to an empty array
35+
// while preserving all other fields
36+
state.engine.backgroundState.PhishingController.phishingLists = [];
37+
state.engine.backgroundState.PhishingController.stalelistLastFetched = 0;
38+
39+
return state;
40+
} catch (error) {
41+
captureException(
42+
new Error(
43+
`Migration 078: cleaning PhishingController state failed with error: ${error}`,
44+
),
45+
);
46+
return state;
47+
}
48+
};
49+
50+
export default migration;

app/store/migrations/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ import migration74 from './074';
7878
import migration75 from './075';
7979
import migration76 from './076';
8080
import migration77 from './077';
81-
import migration82 from './082';
81+
import migration78 from './078';
8282

8383
// Add migrations above this line
8484
import { validatePostMigrationState } from '../validateMigration/validateMigration';
@@ -173,7 +173,7 @@ export const migrationList: MigrationsList = {
173173
75: migration75,
174174
76: migration76,
175175
77: migration77,
176-
82: migration82,
176+
78: migration78,
177177
};
178178

179179
// Enable both synchronous and asynchronous migrations

0 commit comments

Comments
 (0)