Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 118 additions & 169 deletions app/store/migrations/104.test.ts
Original file line number Diff line number Diff line change
@@ -1,213 +1,162 @@
import migrate from './104';
import FilesystemStorage from 'redux-persist-filesystem-storage';
import Device from '../../util/device';
import { ensureValidState } from './util';
import { captureException } from '@sentry/react-native';

jest.mock('redux-persist-filesystem-storage');
const mockFilesystemStorage = FilesystemStorage as jest.Mocked<
typeof FilesystemStorage
>;
jest.mock('@sentry/react-native', () => ({
captureException: jest.fn(),
}));

jest.mock('../../util/device');
const mockDevice = Device as jest.Mocked<typeof Device>;
jest.mock('./util', () => ({
ensureValidState: jest.fn(),
}));

describe('Migration 104', () => {
const mockedCaptureException = jest.mocked(captureException);
const mockedEnsureValidState = jest.mocked(ensureValidState);

describe('Migration 104: Reset PhishingController urlScanCache', () => {
beforeEach(() => {
jest.clearAllMocks();
mockDevice.isIos.mockReturnValue(true);
mockFilesystemStorage.setItem.mockResolvedValue();
jest.resetAllMocks();
});

it('should migrate existing engine data to individual controller storage', async () => {
const mockState = {
engine: {
backgroundState: {
KeyringController: {
vault: 'encrypted-vault-data',
isUnlocked: false,
},
NetworkController: {
network: 'mainnet',
chainId: '1',
},
TransactionController: {
transactions: [{ id: '1', status: 'pending' }],
methodData: { '0x123': { method: 'transfer' } },
},
},
},
};

const result = await migrate(mockState);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(3);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith(
'persist:KeyringController',
JSON.stringify({
vault: 'encrypted-vault-data',
isUnlocked: false,
}),
true,
);
it('returns state unchanged if ensureValidState fails', () => {
const state = { some: 'state' };

expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith(
'persist:NetworkController',
JSON.stringify({
network: 'mainnet',
chainId: '1',
}),
true,
);
mockedEnsureValidState.mockReturnValue(false);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith(
'persist:TransactionController',
JSON.stringify({
transactions: [{ id: '1', status: 'pending' }],
methodData: { '0x123': { method: 'transfer' } },
}),
true,
);
const migratedState = migrate(state);

expect(result).toEqual({
engine: {
backgroundState: {},
},
});
expect(migratedState).toBe(state);
expect(mockedCaptureException).not.toHaveBeenCalled();
});

it('should completely clear backgroundState when all controllers migrate successfully', async () => {
const mockState = {
it('captures exception if PhishingController state is invalid', () => {
const state = {
engine: {
backgroundState: {
KeyringController: {
vault: 'encrypted-vault-data',
},
NetworkController: {
network: 'mainnet',
},
// PhishingController is missing
},
},
};

// All migrations succeed
mockFilesystemStorage.setItem.mockResolvedValue();
mockedEnsureValidState.mockReturnValue(true);

const result = await migrate(mockState);
const migratedState = migrate(state);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(2);

// Should completely clear backgroundState when all controllers migrate successfully
expect(result).toEqual({
engine: {
backgroundState: {},
},
});
expect(migratedState).toEqual(state);
expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
expect(mockedCaptureException.mock.calls[0][0].message).toContain(
'Migration 104: Invalid PhishingController state',
);
});

it('should handle empty engine data gracefully', async () => {
const mockState = {
it('resets PhishingController urlScanCache to empty object while preserving other fields', () => {
interface TestState {
engine: {
backgroundState: {},
},
};

const result = await migrate(mockState);

expect(mockFilesystemStorage.setItem).not.toHaveBeenCalled();

expect(result).toEqual(mockState);
});

it('should handle missing engine data gracefully', async () => {
const mockState = {
engine: {},
};

const result = await migrate(mockState);

expect(mockFilesystemStorage.setItem).not.toHaveBeenCalled();
// Should return state unchanged
expect(result).toEqual(mockState);
});

it('should handle partial controller data', async () => {
const mockState = {
backgroundState: {
PhishingController: {
c2DomainBlocklistLastFetched: number;
phishingLists: string[];
whitelist: string[];
hotlistLastFetched: number;
stalelistLastFetched: number;
urlScanCache: Record<string, unknown>;
extraProperty?: string;
};
OtherController: {
shouldStayUntouched: boolean;
};
};
};
}

const state: TestState = {
engine: {
backgroundState: {
KeyringController: {
vault: 'encrypted-vault-data',
PhishingController: {
c2DomainBlocklistLastFetched: 123456789,
phishingLists: ['list1', 'list2'],
whitelist: ['site1', 'site2'],
hotlistLastFetched: 987654321,
stalelistLastFetched: 123123123,
urlScanCache: {
'example.com': { result: 'safe', timestamp: 1234567890 },
'phishing.com': { result: 'malicious', timestamp: 9876543210 },
},
extraProperty: 'should remain',
},
TransactionController: {
transactions: [],
OtherController: {
shouldStayUntouched: true,
},
},
},
};

const result = await migrate(mockState);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(2);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith(
'persist:KeyringController',
JSON.stringify({ vault: 'encrypted-vault-data' }),
true,
);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith(
'persist:TransactionController',
JSON.stringify({ transactions: [] }),
true,
);

expect(result).toEqual({
engine: {
backgroundState: {},
},
mockedEnsureValidState.mockReturnValue(true);

const migratedState = migrate(state) as typeof state;

// urlScanCache should be reset to empty object
expect(
migratedState.engine.backgroundState.PhishingController.urlScanCache,
).toEqual({});

// Other fields should remain unchanged
expect(
migratedState.engine.backgroundState.PhishingController
.c2DomainBlocklistLastFetched,
).toBe(123456789);
expect(
migratedState.engine.backgroundState.PhishingController.phishingLists,
).toEqual(['list1', 'list2']);
expect(
migratedState.engine.backgroundState.PhishingController.whitelist,
).toEqual(['site1', 'site2']);
expect(
migratedState.engine.backgroundState.PhishingController
.hotlistLastFetched,
).toBe(987654321);
expect(
migratedState.engine.backgroundState.PhishingController
.stalelistLastFetched,
).toBe(123123123);
expect(
migratedState.engine.backgroundState.PhishingController.extraProperty,
).toBe('should remain');

expect(migratedState.engine.backgroundState.OtherController).toEqual({
shouldStayUntouched: true,
});

expect(mockedCaptureException).not.toHaveBeenCalled();
});

it('should handle storage errors gracefully and preserve failed controller state', async () => {
const mockState = {
it('handles error during migration', () => {
// Create state with a PhishingController that throws when urlScanCache is accessed
const state = {
engine: {
backgroundState: {
KeyringController: {
vault: 'encrypted-vault-data',
},
NetworkController: {
network: 'mainnet',
},
PhishingController: Object.defineProperty({}, 'urlScanCache', {
get: () => {
throw new Error('Test error');
},
set: () => {
throw new Error('Test error');
},
configurable: true,
enumerable: true,
}),
},
},
};

mockFilesystemStorage.setItem
.mockRejectedValueOnce(new Error('Storage error'))
.mockResolvedValueOnce();

const result = await migrate(mockState);
mockedEnsureValidState.mockReturnValue(true);

expect(mockFilesystemStorage.setItem).toHaveBeenCalledTimes(2);
const migratedState = migrate(state);

// Should preserve failed controller state to prevent data loss
expect(result).toEqual({
engine: {
backgroundState: {
KeyringController: {
vault: 'encrypted-vault-data',
},
// NetworkController should be migrated successfully and removed from backgroundState
},
},
});
});

it('should handle invalid state gracefully', async () => {
const invalidState = null;

const result = await migrate(invalidState);

expect(result).toBe(invalidState);
expect(mockFilesystemStorage.setItem).not.toHaveBeenCalled();
expect(migratedState).toEqual(state);
expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
expect(mockedCaptureException.mock.calls[0][0].message).toContain(
'Migration 104: cleaning PhishingController state failed with error',
);
});
});
Loading