|
1 | 1 | import migrate from './104'; |
2 | | -import FilesystemStorage from 'redux-persist-filesystem-storage'; |
3 | | -import Device from '../../util/device'; |
| 2 | +import { ensureValidState } from './util'; |
| 3 | +import { captureException } from '@sentry/react-native'; |
4 | 4 |
|
5 | | -jest.mock('redux-persist-filesystem-storage'); |
6 | | -const mockFilesystemStorage = FilesystemStorage as jest.Mocked< |
7 | | - typeof FilesystemStorage |
8 | | ->; |
| 5 | +jest.mock('@sentry/react-native', () => ({ |
| 6 | + captureException: jest.fn(), |
| 7 | +})); |
9 | 8 |
|
10 | | -jest.mock('../../util/device'); |
11 | | -const mockDevice = Device as jest.Mocked<typeof Device>; |
| 9 | +jest.mock('./util', () => ({ |
| 10 | + ensureValidState: jest.fn(), |
| 11 | +})); |
12 | 12 |
|
13 | | -describe('Migration 104', () => { |
| 13 | +const mockedCaptureException = jest.mocked(captureException); |
| 14 | +const mockedEnsureValidState = jest.mocked(ensureValidState); |
| 15 | + |
| 16 | +describe('Migration 104: Reset PhishingController urlScanCache', () => { |
14 | 17 | beforeEach(() => { |
15 | | - jest.clearAllMocks(); |
16 | | - mockDevice.isIos.mockReturnValue(true); |
17 | | - mockFilesystemStorage.setItem.mockResolvedValue(); |
| 18 | + jest.resetAllMocks(); |
18 | 19 | }); |
19 | 20 |
|
20 | | - it('should migrate existing engine data to individual controller storage', async () => { |
21 | | - const mockState = { |
22 | | - engine: { |
23 | | - backgroundState: { |
24 | | - KeyringController: { |
25 | | - vault: 'encrypted-vault-data', |
26 | | - isUnlocked: false, |
27 | | - }, |
28 | | - NetworkController: { |
29 | | - network: 'mainnet', |
30 | | - chainId: '1', |
31 | | - }, |
32 | | - TransactionController: { |
33 | | - transactions: [{ id: '1', status: 'pending' }], |
34 | | - methodData: { '0x123': { method: 'transfer' } }, |
35 | | - }, |
36 | | - }, |
37 | | - }, |
38 | | - }; |
39 | | - |
40 | | - const result = await migrate(mockState); |
41 | | - |
42 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(3); |
43 | | - |
44 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( |
45 | | - 'persist:KeyringController', |
46 | | - JSON.stringify({ |
47 | | - vault: 'encrypted-vault-data', |
48 | | - isUnlocked: false, |
49 | | - }), |
50 | | - true, |
51 | | - ); |
| 21 | + it('returns state unchanged if ensureValidState fails', () => { |
| 22 | + const state = { some: 'state' }; |
52 | 23 |
|
53 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( |
54 | | - 'persist:NetworkController', |
55 | | - JSON.stringify({ |
56 | | - network: 'mainnet', |
57 | | - chainId: '1', |
58 | | - }), |
59 | | - true, |
60 | | - ); |
| 24 | + mockedEnsureValidState.mockReturnValue(false); |
61 | 25 |
|
62 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( |
63 | | - 'persist:TransactionController', |
64 | | - JSON.stringify({ |
65 | | - transactions: [{ id: '1', status: 'pending' }], |
66 | | - methodData: { '0x123': { method: 'transfer' } }, |
67 | | - }), |
68 | | - true, |
69 | | - ); |
| 26 | + const migratedState = migrate(state); |
70 | 27 |
|
71 | | - expect(result).toEqual({ |
72 | | - engine: { |
73 | | - backgroundState: {}, |
74 | | - }, |
75 | | - }); |
| 28 | + expect(migratedState).toBe(state); |
| 29 | + expect(mockedCaptureException).not.toHaveBeenCalled(); |
76 | 30 | }); |
77 | 31 |
|
78 | | - it('should completely clear backgroundState when all controllers migrate successfully', async () => { |
79 | | - const mockState = { |
| 32 | + it('captures exception if PhishingController state is invalid', () => { |
| 33 | + const state = { |
80 | 34 | engine: { |
81 | 35 | backgroundState: { |
82 | | - KeyringController: { |
83 | | - vault: 'encrypted-vault-data', |
84 | | - }, |
85 | | - NetworkController: { |
86 | | - network: 'mainnet', |
87 | | - }, |
| 36 | + // PhishingController is missing |
88 | 37 | }, |
89 | 38 | }, |
90 | 39 | }; |
91 | 40 |
|
92 | | - // All migrations succeed |
93 | | - mockFilesystemStorage.setItem.mockResolvedValue(); |
| 41 | + mockedEnsureValidState.mockReturnValue(true); |
94 | 42 |
|
95 | | - const result = await migrate(mockState); |
| 43 | + const migratedState = migrate(state); |
96 | 44 |
|
97 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(2); |
98 | | - |
99 | | - // Should completely clear backgroundState when all controllers migrate successfully |
100 | | - expect(result).toEqual({ |
101 | | - engine: { |
102 | | - backgroundState: {}, |
103 | | - }, |
104 | | - }); |
| 45 | + expect(migratedState).toEqual(state); |
| 46 | + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); |
| 47 | + expect(mockedCaptureException.mock.calls[0][0].message).toContain( |
| 48 | + 'Migration 104: Invalid PhishingController state', |
| 49 | + ); |
105 | 50 | }); |
106 | 51 |
|
107 | | - it('should handle empty engine data gracefully', async () => { |
108 | | - const mockState = { |
| 52 | + it('resets PhishingController urlScanCache to empty object while preserving other fields', () => { |
| 53 | + interface TestState { |
109 | 54 | engine: { |
110 | | - backgroundState: {}, |
111 | | - }, |
112 | | - }; |
113 | | - |
114 | | - const result = await migrate(mockState); |
115 | | - |
116 | | - expect(mockFilesystemStorage.setItem).not.toHaveBeenCalled(); |
117 | | - |
118 | | - expect(result).toEqual(mockState); |
119 | | - }); |
120 | | - |
121 | | - it('should handle missing engine data gracefully', async () => { |
122 | | - const mockState = { |
123 | | - engine: {}, |
124 | | - }; |
125 | | - |
126 | | - const result = await migrate(mockState); |
127 | | - |
128 | | - expect(mockFilesystemStorage.setItem).not.toHaveBeenCalled(); |
129 | | - // Should return state unchanged |
130 | | - expect(result).toEqual(mockState); |
131 | | - }); |
132 | | - |
133 | | - it('should handle partial controller data', async () => { |
134 | | - const mockState = { |
| 55 | + backgroundState: { |
| 56 | + PhishingController: { |
| 57 | + c2DomainBlocklistLastFetched: number; |
| 58 | + phishingLists: string[]; |
| 59 | + whitelist: string[]; |
| 60 | + hotlistLastFetched: number; |
| 61 | + stalelistLastFetched: number; |
| 62 | + urlScanCache: Record<string, unknown>; |
| 63 | + extraProperty?: string; |
| 64 | + }; |
| 65 | + OtherController: { |
| 66 | + shouldStayUntouched: boolean; |
| 67 | + }; |
| 68 | + }; |
| 69 | + }; |
| 70 | + } |
| 71 | + |
| 72 | + const state: TestState = { |
135 | 73 | engine: { |
136 | 74 | backgroundState: { |
137 | | - KeyringController: { |
138 | | - vault: 'encrypted-vault-data', |
| 75 | + PhishingController: { |
| 76 | + c2DomainBlocklistLastFetched: 123456789, |
| 77 | + phishingLists: ['list1', 'list2'], |
| 78 | + whitelist: ['site1', 'site2'], |
| 79 | + hotlistLastFetched: 987654321, |
| 80 | + stalelistLastFetched: 123123123, |
| 81 | + urlScanCache: { |
| 82 | + 'example.com': { result: 'safe', timestamp: 1234567890 }, |
| 83 | + 'phishing.com': { result: 'malicious', timestamp: 9876543210 }, |
| 84 | + }, |
| 85 | + extraProperty: 'should remain', |
139 | 86 | }, |
140 | | - TransactionController: { |
141 | | - transactions: [], |
| 87 | + OtherController: { |
| 88 | + shouldStayUntouched: true, |
142 | 89 | }, |
143 | 90 | }, |
144 | 91 | }, |
145 | 92 | }; |
146 | 93 |
|
147 | | - const result = await migrate(mockState); |
148 | | - |
149 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(2); |
150 | | - |
151 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( |
152 | | - 'persist:KeyringController', |
153 | | - JSON.stringify({ vault: 'encrypted-vault-data' }), |
154 | | - true, |
155 | | - ); |
156 | | - |
157 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( |
158 | | - 'persist:TransactionController', |
159 | | - JSON.stringify({ transactions: [] }), |
160 | | - true, |
161 | | - ); |
162 | | - |
163 | | - expect(result).toEqual({ |
164 | | - engine: { |
165 | | - backgroundState: {}, |
166 | | - }, |
| 94 | + mockedEnsureValidState.mockReturnValue(true); |
| 95 | + |
| 96 | + const migratedState = migrate(state) as typeof state; |
| 97 | + |
| 98 | + // urlScanCache should be reset to empty object |
| 99 | + expect( |
| 100 | + migratedState.engine.backgroundState.PhishingController.urlScanCache, |
| 101 | + ).toEqual({}); |
| 102 | + |
| 103 | + // Other fields should remain unchanged |
| 104 | + expect( |
| 105 | + migratedState.engine.backgroundState.PhishingController |
| 106 | + .c2DomainBlocklistLastFetched, |
| 107 | + ).toBe(123456789); |
| 108 | + expect( |
| 109 | + migratedState.engine.backgroundState.PhishingController.phishingLists, |
| 110 | + ).toEqual(['list1', 'list2']); |
| 111 | + expect( |
| 112 | + migratedState.engine.backgroundState.PhishingController.whitelist, |
| 113 | + ).toEqual(['site1', 'site2']); |
| 114 | + expect( |
| 115 | + migratedState.engine.backgroundState.PhishingController |
| 116 | + .hotlistLastFetched, |
| 117 | + ).toBe(987654321); |
| 118 | + expect( |
| 119 | + migratedState.engine.backgroundState.PhishingController |
| 120 | + .stalelistLastFetched, |
| 121 | + ).toBe(123123123); |
| 122 | + expect( |
| 123 | + migratedState.engine.backgroundState.PhishingController.extraProperty, |
| 124 | + ).toBe('should remain'); |
| 125 | + |
| 126 | + expect(migratedState.engine.backgroundState.OtherController).toEqual({ |
| 127 | + shouldStayUntouched: true, |
167 | 128 | }); |
| 129 | + |
| 130 | + expect(mockedCaptureException).not.toHaveBeenCalled(); |
168 | 131 | }); |
169 | 132 |
|
170 | | - it('should handle storage errors gracefully and preserve failed controller state', async () => { |
171 | | - const mockState = { |
| 133 | + it('handles error during migration', () => { |
| 134 | + // Create state with a PhishingController that throws when urlScanCache is accessed |
| 135 | + const state = { |
172 | 136 | engine: { |
173 | 137 | backgroundState: { |
174 | | - KeyringController: { |
175 | | - vault: 'encrypted-vault-data', |
176 | | - }, |
177 | | - NetworkController: { |
178 | | - network: 'mainnet', |
179 | | - }, |
| 138 | + PhishingController: Object.defineProperty({}, 'urlScanCache', { |
| 139 | + get: () => { |
| 140 | + throw new Error('Test error'); |
| 141 | + }, |
| 142 | + set: () => { |
| 143 | + throw new Error('Test error'); |
| 144 | + }, |
| 145 | + configurable: true, |
| 146 | + enumerable: true, |
| 147 | + }), |
180 | 148 | }, |
181 | 149 | }, |
182 | 150 | }; |
183 | 151 |
|
184 | | - mockFilesystemStorage.setItem |
185 | | - .mockRejectedValueOnce(new Error('Storage error')) |
186 | | - .mockResolvedValueOnce(); |
187 | | - |
188 | | - const result = await migrate(mockState); |
| 152 | + mockedEnsureValidState.mockReturnValue(true); |
189 | 153 |
|
190 | | - expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(2); |
| 154 | + const migratedState = migrate(state); |
191 | 155 |
|
192 | | - // Should preserve failed controller state to prevent data loss |
193 | | - expect(result).toEqual({ |
194 | | - engine: { |
195 | | - backgroundState: { |
196 | | - KeyringController: { |
197 | | - vault: 'encrypted-vault-data', |
198 | | - }, |
199 | | - // NetworkController should be migrated successfully and removed from backgroundState |
200 | | - }, |
201 | | - }, |
202 | | - }); |
203 | | - }); |
204 | | - |
205 | | - it('should handle invalid state gracefully', async () => { |
206 | | - const invalidState = null; |
207 | | - |
208 | | - const result = await migrate(invalidState); |
209 | | - |
210 | | - expect(result).toBe(invalidState); |
211 | | - expect(mockFilesystemStorage.setItem).not.toHaveBeenCalled(); |
| 156 | + expect(migratedState).toEqual(state); |
| 157 | + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); |
| 158 | + expect(mockedCaptureException.mock.calls[0][0].message).toContain( |
| 159 | + 'Migration 104: cleaning PhishingController state failed with error', |
| 160 | + ); |
212 | 161 | }); |
213 | 162 | }); |
0 commit comments