Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 300
test(perps): migrate controller unit tests to core#8840
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
ca157fb1621655ea538eef59d60d24ce9a13ff0529File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /* eslint-disable */ | ||
| /** | ||
| * Shared provider mocks for Perps tests | ||
| * Provides reusable mock implementations for HyperLiquidProvider and related interfaces | ||
| */ | ||
| import { type HyperLiquidProvider } from '@metamask/perps-controller'; | ||
| export const createMockHyperLiquidProvider = | ||
| (): jest.Mocked<HyperLiquidProvider> => | ||
| ({ | ||
| protocolId: 'hyperliquid', | ||
| initialize: jest.fn(), | ||
| isReadyToTrade: jest.fn(), | ||
| toggleTestnet: jest.fn(), | ||
| getPositions: jest.fn(), | ||
| getAccountState: jest.fn(), | ||
| getHistoricalPortfolio: jest.fn().mockResolvedValue({ | ||
| totalBalance24hAgo: '10000', | ||
| totalBalance7dAgo: '9500', | ||
| totalBalance30dAgo: '9000', | ||
| }), | ||
| getMarkets: jest.fn(), | ||
| placeOrder: jest.fn(), | ||
| editOrder: jest.fn(), | ||
| cancelOrder: jest.fn(), | ||
| cancelOrders: jest.fn(), | ||
| closePosition: jest.fn(), | ||
| closePositions: jest.fn(), | ||
| withdraw: jest.fn(), | ||
| getDepositRoutes: jest.fn(), | ||
| getWithdrawalRoutes: jest.fn(), | ||
| validateDeposit: jest.fn().mockResolvedValue({ isValid: true }), | ||
| validateOrder: jest.fn().mockResolvedValue({ isValid: true }), | ||
| validateClosePosition: jest.fn().mockResolvedValue({ isValid: true }), | ||
| validateWithdrawal: jest.fn().mockResolvedValue({ isValid: true }), | ||
| subscribeToPrices: jest.fn(), | ||
| subscribeToPositions: jest.fn(), | ||
| subscribeToOrderFills: jest.fn(), | ||
| setLiveDataConfig: jest.fn(), | ||
| disconnect: jest.fn(), | ||
| updatePositionTPSL: jest.fn(), | ||
| calculateLiquidationPrice: jest.fn(), | ||
| calculateMaintenanceMargin: jest.fn(), | ||
| getMaxLeverage: jest.fn(), | ||
| calculateFees: jest.fn(), | ||
| getMarketDataWithPrices: jest.fn(), | ||
| getBlockExplorerUrl: jest.fn(), | ||
| getOrderFills: jest.fn(), | ||
| getOrders: jest.fn(), | ||
| getFunding: jest.fn(), | ||
| getCurrentAccountId: jest | ||
| .fn() | ||
| .mockResolvedValue( | ||
| 'eip155:1:0x0000000000000000000000000000000000000001', | ||
| ), | ||
| getIsFirstTimeUser: jest.fn(), | ||
| getOpenOrders: jest.fn(), | ||
| subscribeToOrders: jest.fn(), | ||
| subscribeToAccount: jest.fn(), | ||
| setUserFeeDiscount: jest.fn(), | ||
| // WebSocket connection state methods | ||
| getWebSocketConnectionState: jest.fn(), | ||
| subscribeToConnectionState: jest.fn().mockReturnValue(() => undefined), | ||
| reconnect: jest.fn().mockResolvedValue(undefined), | ||
| }) as unknown as jest.Mocked<HyperLiquidProvider>; | ||
| export const createMockOrder = (overrides = {}) => ({ | ||
| orderId: 'order-1', | ||
| symbol: 'BTC', | ||
| side: 'buy' as const, | ||
| orderType: 'limit' as const, | ||
| size: '0.1', | ||
| originalSize: '0.1', | ||
| price: '50000', | ||
| filledSize: '0', | ||
| remainingSize: '0.1', | ||
| status: 'open' as const, | ||
| timestamp: Date.now(), | ||
| ...overrides, | ||
| }); | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| export const createMockPosition = (overrides = {}) => ({ | ||
| symbol: 'BTC', | ||
| size: '0.5', | ||
| entryPrice: '50000', | ||
| positionValue: '25000', | ||
| unrealizedPnl: '100', | ||
| marginUsed: '1000', | ||
| leverage: { type: 'cross' as const, value: 25 }, | ||
| liquidationPrice: '48000', | ||
| maxLeverage: 50, | ||
| returnOnEquity: '10', | ||
| cumulativeFunding: { | ||
| allTime: '0', | ||
| sinceOpen: '0', | ||
| sinceChange: '0', | ||
| }, | ||
| roi: '10', | ||
| takeProfitPrice: undefined, | ||
| stopLossPrice: undefined, | ||
| takeProfitCount: 0, | ||
| stopLossCount: 0, | ||
| marketPrice: '50200', | ||
| timestamp: Date.now(), | ||
| ...overrides, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| /* eslint-disable */ | ||
| /** | ||
| * Shared service mocks for Perps service tests | ||
| * Provides reusable mock implementations for ServiceContext and related types | ||
| */ | ||
| import { | ||
| type ServiceContext, | ||
| type PerpsControllerState, | ||
| type InitializationState, | ||
| type PerpsControllerMessenger, | ||
| type PerpsPlatformDependencies, | ||
| } from '@metamask/perps-controller'; | ||
| /** | ||
| * Create a mock EVM account (KeyringAccount) | ||
| */ | ||
| export const createMockEvmAccount = () => ({ | ||
| id: '00000000-0000-0000-0000-000000000000', | ||
| address: '0x1234567890abcdef1234567890abcdef12345678' as `0x${string}`, | ||
| type: 'eip155:eoa' as const, | ||
| options: {}, | ||
| scopes: ['eip155:1'], | ||
| methods: ['eth_signTransaction', 'eth_sign'], | ||
| metadata: { | ||
| name: 'Test Account', | ||
| importTime: Date.now(), | ||
| keyring: { type: 'HD Key Tree' }, | ||
| }, | ||
| }); | ||
| /** | ||
| * Create a mock PerpsPlatformDependencies instance. | ||
| * Returns a type-safe mock with jest.Mock functions for all methods. | ||
| * Uses `as unknown as jest.Mocked<PerpsPlatformDependencies>` pattern | ||
| * to ensure compatibility with both the interface contract and Jest mock APIs. | ||
| * | ||
| * Architecture: | ||
| * - Observability: logger, debugLogger, metrics, performance, tracer (stateless utilities) | ||
| * - Platform: streamManager (mobile/extension specific capabilities) | ||
| * - Controllers: consolidated access to all external controllers | ||
| */ | ||
| export const createMockInfrastructure = | ||
| (): jest.Mocked<PerpsPlatformDependencies> => | ||
| ({ | ||
| // === Observability (stateless utilities) === | ||
| logger: { | ||
| error: jest.fn(), | ||
| }, | ||
| debugLogger: { | ||
| log: jest.fn(), | ||
| }, | ||
| metrics: { | ||
| trackEvent: jest.fn(), | ||
| isEnabled: jest.fn(() => true), | ||
| trackPerpsEvent: jest.fn(), | ||
| }, | ||
| performance: { | ||
| now: jest.fn(() => Date.now()), | ||
| }, | ||
| tracer: { | ||
| trace: jest.fn(() => undefined), | ||
| endTrace: jest.fn(), | ||
| setMeasurement: jest.fn(), | ||
| addBreadcrumb: jest.fn(), | ||
| }, | ||
| // === Platform Services === | ||
| streamManager: { | ||
| pauseChannel: jest.fn(), | ||
| resumeChannel: jest.fn(), | ||
| clearAllChannels: jest.fn(), | ||
| }, | ||
| // === Feature Flags (platform-specific version gating) === | ||
| featureFlags: { | ||
| validateVersionGated: jest.fn().mockReturnValue(undefined), | ||
| }, | ||
| // === Market Data Formatting === | ||
| marketDataFormatters: { | ||
| formatVolume: jest.fn((v: number) => `$${v.toFixed(0)}`), | ||
| formatPerpsFiat: jest.fn((v: number) => `$${v.toFixed(2)}`), | ||
| formatPercentage: jest.fn((p: number) => `${p.toFixed(2)}%`), | ||
| priceRangesUniversal: [], | ||
| }, | ||
| // === Cache Invalidation === | ||
| cacheInvalidator: { | ||
| invalidate: jest.fn(), | ||
| invalidateAll: jest.fn(), | ||
| }, | ||
| // === Rewards (DI — no RewardsController in Core yet) === | ||
| rewards: { | ||
| getPerpsDiscountForAccount: jest.fn().mockResolvedValue(0), | ||
| }, | ||
| // === Disk Cache (cold-start persistence) === | ||
| diskCache: { | ||
| getItem: jest.fn().mockResolvedValue(null), | ||
| getItemSync: jest.fn().mockReturnValue(null), | ||
| setItem: jest.fn().mockResolvedValue(undefined), | ||
| removeItem: jest.fn().mockResolvedValue(undefined), | ||
| }, | ||
| }) as unknown as jest.Mocked<PerpsPlatformDependencies>; | ||
| /** | ||
| * Create a mock PerpsControllerState | ||
| */ | ||
| export const createMockPerpsControllerState = ( | ||
| overrides: Partial<PerpsControllerState> = {}, | ||
| ): PerpsControllerState => ({ | ||
| activeProvider: 'hyperliquid', | ||
| isTestnet: false, | ||
| initializationState: 'initialized' as InitializationState, | ||
| initializationError: null, | ||
| initializationAttempts: 0, | ||
| accountState: null, | ||
| perpsBalances: {}, | ||
| depositInProgress: false, | ||
| lastDepositTransactionId: null, | ||
| lastDepositResult: null, | ||
| withdrawInProgress: false, | ||
| lastWithdrawResult: null, | ||
| lastCompletedWithdrawalTimestamp: null, | ||
| lastCompletedWithdrawalTxHashes: [], | ||
| withdrawalRequests: [], | ||
| withdrawalProgress: { | ||
| progress: 0, | ||
| lastUpdated: 0, | ||
| activeWithdrawalId: null, | ||
| }, | ||
| depositRequests: [], | ||
| isEligible: true, | ||
| isFirstTimeUser: { | ||
| testnet: true, | ||
| mainnet: true, | ||
| }, | ||
| hasPlacedFirstOrder: { | ||
| testnet: false, | ||
| mainnet: false, | ||
| }, | ||
| watchlistMarkets: { | ||
| testnet: [], | ||
| mainnet: [], | ||
| }, | ||
| tradeConfigurations: { | ||
| testnet: {}, | ||
| mainnet: {}, | ||
| }, | ||
| marketFilterPreferences: { | ||
| optionId: 'volume', | ||
| direction: 'desc', | ||
| }, | ||
| lastError: null, | ||
| lastUpdateTimestamp: Date.now(), | ||
| hip3ConfigVersion: 0, | ||
| selectedPaymentToken: null, | ||
| cachedMarketDataByProvider: {}, | ||
| cachedUserDataByProvider: {}, | ||
| ...overrides, | ||
| }); | ||
| /** | ||
| * Create a mock ServiceContext with optional overrides | ||
| * Note: infrastructure is no longer part of ServiceContext - it's now injected | ||
| * into service instances via constructor. | ||
| */ | ||
| export const createMockServiceContext = ( | ||
| overrides: Partial<ServiceContext> = {}, | ||
| ): ServiceContext => ({ | ||
| tracingContext: { | ||
| provider: 'hyperliquid', | ||
| isTestnet: false, | ||
| }, | ||
| errorContext: { | ||
| controller: 'TestService', | ||
| method: 'testMethod', | ||
| }, | ||
| stateManager: { | ||
| update: jest.fn(), | ||
| getState: jest.fn(() => createMockPerpsControllerState()), | ||
| }, | ||
| ...overrides, | ||
| }); | ||
| /** | ||
| * Create a mock PerpsControllerMessenger for testing inter-controller communication. | ||
| * The messenger.call() method should be configured in each test to return appropriate values. | ||
| * | ||
| * Common messenger actions used: | ||
| * - 'AccountTreeController:getAccountsFromSelectedAccountGroup' - returns array of accounts | ||
| * - 'KeyringController:signTypedMessage' - returns signature string | ||
| * - 'NetworkController:getState' - returns { selectedNetworkClientId: string } | ||
| * - 'NetworkController:getNetworkClientById' - returns { configuration: { chainId: string } } | ||
| * - 'AuthenticationController:getBearerToken' - returns bearer token string | ||
| * | ||
| * @param overrides - Optional partial messenger to override default behavior | ||
| */ | ||
| export const createMockMessenger = ( | ||
| overrides?: Partial<PerpsControllerMessenger>, | ||
| ): jest.Mocked<PerpsControllerMessenger> => { | ||
| const mockEvmAccount = createMockEvmAccount(); | ||
| const base = { | ||
| call: jest.fn().mockImplementation((action: string) => { | ||
| // Default implementations for common actions | ||
| if ( | ||
| action === 'AccountTreeController:getAccountsFromSelectedAccountGroup' | ||
| ) { | ||
| return [mockEvmAccount]; | ||
| } | ||
| if (action === 'KeyringController:getState') { | ||
| return { isUnlocked: true }; | ||
| } | ||
| if (action === 'KeyringController:signTypedMessage') { | ||
| return Promise.resolve('0xSignatureResult'); | ||
| } | ||
| if (action === 'NetworkController:getState') { | ||
| return { selectedNetworkClientId: 'mainnet' }; | ||
| } | ||
| if (action === 'NetworkController:getNetworkClientById') { | ||
| return { configuration: { chainId: '0x1' } }; | ||
| } | ||
| if (action === 'AuthenticationController:getBearerToken') { | ||
| return Promise.resolve('mock-bearer-token'); | ||
| } | ||
| return undefined; | ||
| }), | ||
| publish: jest.fn(), | ||
| subscribe: jest.fn(), | ||
| unsubscribe: jest.fn(), | ||
| registerActionHandler: jest.fn(), | ||
| registerMethodActionHandlers: jest.fn(), | ||
| unregisterActionHandler: jest.fn(), | ||
| // Additional methods used by PerpsController | ||
| registerEventHandler: jest.fn(), | ||
| registerInitialEventPayload: jest.fn(), | ||
| unregisterEventHandler: jest.fn(), | ||
| clearEventSubscriptions: jest.fn(), | ||
| }; | ||
| return { | ||
| ...base, | ||
| ...overrides, | ||
| } as unknown as jest.Mocked<PerpsControllerMessenger>; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.