UnifiedSigner.unit.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. /**
  2. * Unit Tests for UnifiedSigner
  3. *
  4. * Tests the UnifiedSigner service implementation in isolation.
  5. */
  6. import { UnifiedSigner, BatchSignRequest } from '@/core/credential-manager/UnifiedSigner'
  7. import { PacificaSigner } from '@/core/credential-manager/signers/PacificaSigner'
  8. import { AsterSigner } from '@/core/credential-manager/signers/AsterSigner'
  9. import { BinanceSigner } from '@/core/credential-manager/signers/BinanceSigner'
  10. import { Platform } from '@/types/credential'
  11. describe('UnifiedSigner Unit Tests', () => {
  12. let unifiedSigner: UnifiedSigner
  13. let mockGetAccount: jest.Mock
  14. beforeEach(() => {
  15. unifiedSigner = new UnifiedSigner()
  16. mockGetAccount = jest.fn()
  17. unifiedSigner.setAccountGetter(mockGetAccount)
  18. })
  19. afterEach(() => {
  20. jest.clearAllMocks()
  21. })
  22. describe('Initialization and Configuration', () => {
  23. test('should initialize with default strategies', () => {
  24. const supportedPlatforms = unifiedSigner.getSupportedPlatforms()
  25. expect(supportedPlatforms).toContain(Platform.PACIFICA)
  26. expect(supportedPlatforms).toContain(Platform.ASTER)
  27. expect(supportedPlatforms).toContain(Platform.BINANCE)
  28. expect(supportedPlatforms).toHaveLength(3)
  29. })
  30. test('should allow registering custom strategies', () => {
  31. const customSigner = new PacificaSigner()
  32. const customPlatform = 'CUSTOM' as Platform
  33. // Mock the platform property for testing
  34. Object.defineProperty(customSigner, 'platform', {
  35. value: customPlatform,
  36. writable: false
  37. })
  38. unifiedSigner.registerStrategy(customPlatform, customSigner)
  39. const supportedPlatforms = unifiedSigner.getSupportedPlatforms()
  40. expect(supportedPlatforms).toContain(customPlatform)
  41. })
  42. test('should throw error for mismatched platform in strategy registration', () => {
  43. const pacificaSigner = new PacificaSigner()
  44. expect(() => {
  45. unifiedSigner.registerStrategy(Platform.ASTER, pacificaSigner)
  46. }).toThrow('Strategy platform mismatch')
  47. })
  48. test('should throw error for invalid strategy registration parameters', () => {
  49. expect(() => {
  50. unifiedSigner.registerStrategy(null as any, null as any)
  51. }).toThrow('Platform and strategy are required')
  52. })
  53. test('should require account getter to be set', async () => {
  54. const signerWithoutGetter = new UnifiedSigner()
  55. const message = new TextEncoder().encode('test')
  56. await expect(signerWithoutGetter.sign('test-account', message))
  57. .rejects.toThrow('Account getter function not configured')
  58. })
  59. })
  60. describe('Signing Operations', () => {
  61. test('should sign with Pacifica account', async () => {
  62. const pacificaAccount = {
  63. id: 'pac-account',
  64. platform: Platform.PACIFICA,
  65. credentials: {
  66. type: 'ed25519' as const,
  67. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  68. }
  69. }
  70. mockGetAccount.mockResolvedValue(pacificaAccount)
  71. const message = new TextEncoder().encode('test message')
  72. const result = await unifiedSigner.sign('pac-account', message)
  73. expect(result.success).toBe(true)
  74. expect(result.algorithm).toBe('ed25519')
  75. expect(result.metadata?.platform).toBe(Platform.PACIFICA)
  76. expect(mockGetAccount).toHaveBeenCalledWith('pac-account')
  77. })
  78. test('should sign with Aster account', async () => {
  79. const asterAccount = {
  80. id: 'ast-account',
  81. platform: Platform.ASTER,
  82. credentials: {
  83. type: 'secp256k1' as const,
  84. privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
  85. }
  86. }
  87. mockGetAccount.mockResolvedValue(asterAccount)
  88. const message = new TextEncoder().encode('test message')
  89. const result = await unifiedSigner.sign('ast-account', message)
  90. expect(result.success).toBe(true)
  91. expect(result.algorithm).toBe('eip191')
  92. expect(result.metadata?.platform).toBe(Platform.ASTER)
  93. })
  94. test('should sign with Binance account', async () => {
  95. const binanceAccount = {
  96. id: 'bnb-account',
  97. platform: Platform.BINANCE,
  98. credentials: {
  99. type: 'hmac' as const,
  100. apiKey: 'test-api-key',
  101. secretKey: 'test-secret-key'
  102. }
  103. }
  104. mockGetAccount.mockResolvedValue(binanceAccount)
  105. const message = new TextEncoder().encode('test message')
  106. const result = await unifiedSigner.sign('bnb-account', message)
  107. expect(result.success).toBe(true)
  108. expect(result.algorithm).toBe('hmac-sha256')
  109. expect(result.metadata?.platform).toBe(Platform.BINANCE)
  110. })
  111. test('should handle account not found error', async () => {
  112. mockGetAccount.mockResolvedValue(null)
  113. const message = new TextEncoder().encode('test message')
  114. const result = await unifiedSigner.sign('non-existent', message)
  115. expect(result.success).toBe(false)
  116. expect(result.error).toContain('Account not found')
  117. })
  118. test('should handle platform detection failure', async () => {
  119. const accountWithoutPlatform = {
  120. id: 'invalid-account',
  121. credentials: {
  122. type: 'unknown' as any
  123. }
  124. }
  125. mockGetAccount.mockResolvedValue(accountWithoutPlatform)
  126. const message = new TextEncoder().encode('test message')
  127. const result = await unifiedSigner.sign('invalid-account', message)
  128. expect(result.success).toBe(false)
  129. expect(result.error).toContain('Cannot determine platform')
  130. })
  131. test('should handle unsupported platform', async () => {
  132. const accountWithUnsupportedPlatform = {
  133. id: 'unsupported-account',
  134. platform: 'UNSUPPORTED' as Platform,
  135. credentials: {
  136. type: 'unknown' as any
  137. }
  138. }
  139. mockGetAccount.mockResolvedValue(accountWithUnsupportedPlatform)
  140. const message = new TextEncoder().encode('test message')
  141. const result = await unifiedSigner.sign('unsupported-account', message)
  142. expect(result.success).toBe(false)
  143. expect(result.error).toContain('No strategy available')
  144. })
  145. test('should respect force platform option', async () => {
  146. const asterAccount = {
  147. id: 'ast-account',
  148. platform: Platform.ASTER,
  149. credentials: {
  150. type: 'secp256k1' as const,
  151. privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
  152. }
  153. }
  154. mockGetAccount.mockResolvedValue(asterAccount)
  155. const message = new TextEncoder().encode('test message')
  156. const result = await unifiedSigner.sign('ast-account', message, {
  157. forcePlatform: Platform.ASTER
  158. })
  159. expect(result.success).toBe(true)
  160. expect(result.metadata?.platform).toBe(Platform.ASTER)
  161. })
  162. })
  163. describe('Verification Operations', () => {
  164. test('should verify Pacifica signatures', async () => {
  165. const pacificaAccount = {
  166. id: 'pac-account',
  167. platform: Platform.PACIFICA,
  168. credentials: {
  169. type: 'ed25519' as const,
  170. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  171. }
  172. }
  173. mockGetAccount.mockResolvedValue(pacificaAccount)
  174. const message = new TextEncoder().encode('verification test')
  175. // First sign the message
  176. const signResult = await unifiedSigner.sign('pac-account', message)
  177. expect(signResult.success).toBe(true)
  178. // Then verify it
  179. const isValid = await unifiedSigner.verify(
  180. 'pac-account',
  181. message,
  182. signResult.signature!
  183. )
  184. expect(isValid).toBe(true)
  185. })
  186. test('should reject invalid signatures during verification', async () => {
  187. const pacificaAccount = {
  188. id: 'pac-account',
  189. platform: Platform.PACIFICA,
  190. credentials: {
  191. type: 'ed25519' as const,
  192. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  193. }
  194. }
  195. mockGetAccount.mockResolvedValue(pacificaAccount)
  196. const message = new TextEncoder().encode('verification test')
  197. const invalidSignature = 'invalid-signature'
  198. const isValid = await unifiedSigner.verify('pac-account', message, invalidSignature)
  199. expect(isValid).toBe(false)
  200. })
  201. test('should handle verification errors gracefully', async () => {
  202. mockGetAccount.mockResolvedValue(null)
  203. const message = new TextEncoder().encode('test')
  204. const signature = 'some-signature'
  205. const isValid = await unifiedSigner.verify('non-existent', message, signature)
  206. expect(isValid).toBe(false)
  207. })
  208. })
  209. describe('Batch Operations', () => {
  210. test('should handle batch signing requests', async () => {
  211. const pacificaAccount = {
  212. id: 'pac-account',
  213. platform: Platform.PACIFICA,
  214. credentials: {
  215. type: 'ed25519' as const,
  216. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  217. }
  218. }
  219. const asterAccount = {
  220. id: 'ast-account',
  221. platform: Platform.ASTER,
  222. credentials: {
  223. type: 'secp256k1' as const,
  224. privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
  225. }
  226. }
  227. mockGetAccount
  228. .mockResolvedValueOnce(pacificaAccount)
  229. .mockResolvedValueOnce(asterAccount)
  230. const batchRequests: BatchSignRequest[] = [
  231. {
  232. accountId: 'pac-account',
  233. message: new TextEncoder().encode('pacifica message')
  234. },
  235. {
  236. accountId: 'ast-account',
  237. message: new TextEncoder().encode('aster message')
  238. }
  239. ]
  240. const results = await unifiedSigner.signBatch(batchRequests)
  241. expect(results).toHaveLength(2)
  242. expect(results[0].success).toBe(true)
  243. expect(results[0].accountId).toBe('pac-account')
  244. expect(results[1].success).toBe(true)
  245. expect(results[1].accountId).toBe('ast-account')
  246. })
  247. test('should handle batch requests with failures', async () => {
  248. const validAccount = {
  249. id: 'valid-account',
  250. platform: Platform.PACIFICA,
  251. credentials: {
  252. type: 'ed25519' as const,
  253. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  254. }
  255. }
  256. mockGetAccount
  257. .mockResolvedValueOnce(validAccount)
  258. .mockResolvedValueOnce(null) // Account not found
  259. const batchRequests: BatchSignRequest[] = [
  260. {
  261. accountId: 'valid-account',
  262. message: new TextEncoder().encode('valid message')
  263. },
  264. {
  265. accountId: 'invalid-account',
  266. message: new TextEncoder().encode('invalid message')
  267. }
  268. ]
  269. const results = await unifiedSigner.signBatch(batchRequests)
  270. expect(results).toHaveLength(2)
  271. expect(results[0].success).toBe(true)
  272. expect(results[0].accountId).toBe('valid-account')
  273. expect(results[1].success).toBe(false)
  274. expect(results[1].accountId).toBe('invalid-account')
  275. })
  276. test('should maintain order in batch results', async () => {
  277. const account = {
  278. id: 'test-account',
  279. platform: Platform.PACIFICA,
  280. credentials: {
  281. type: 'ed25519' as const,
  282. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  283. }
  284. }
  285. mockGetAccount.mockResolvedValue(account)
  286. const batchRequests: BatchSignRequest[] = [
  287. {
  288. accountId: 'test-account',
  289. message: new TextEncoder().encode('message 1')
  290. },
  291. {
  292. accountId: 'test-account',
  293. message: new TextEncoder().encode('message 2')
  294. },
  295. {
  296. accountId: 'test-account',
  297. message: new TextEncoder().encode('message 3')
  298. }
  299. ]
  300. const results = await unifiedSigner.signBatch(batchRequests)
  301. expect(results).toHaveLength(3)
  302. expect(results[0].batchIndex).toBe(0)
  303. expect(results[1].batchIndex).toBe(1)
  304. expect(results[2].batchIndex).toBe(2)
  305. })
  306. test('should group requests by platform for optimal processing', async () => {
  307. const pacificaAccount = {
  308. id: 'pac-account',
  309. platform: Platform.PACIFICA,
  310. credentials: {
  311. type: 'ed25519' as const,
  312. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  313. }
  314. }
  315. mockGetAccount.mockResolvedValue(pacificaAccount)
  316. const batchRequests: BatchSignRequest[] = [
  317. {
  318. accountId: 'pac-account',
  319. message: new TextEncoder().encode('message 1'),
  320. options: { forcePlatform: Platform.PACIFICA }
  321. },
  322. {
  323. accountId: 'pac-account',
  324. message: new TextEncoder().encode('message 2'),
  325. options: { forcePlatform: Platform.PACIFICA }
  326. }
  327. ]
  328. const results = await unifiedSigner.signBatch(batchRequests)
  329. expect(results).toHaveLength(2)
  330. expect(results.every(r => r.success)).toBe(true)
  331. })
  332. })
  333. describe('Performance Metrics', () => {
  334. test('should collect and provide metrics', async () => {
  335. const account = {
  336. id: 'test-account',
  337. platform: Platform.PACIFICA,
  338. credentials: {
  339. type: 'ed25519' as const,
  340. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  341. }
  342. }
  343. mockGetAccount.mockResolvedValue(account)
  344. // Reset metrics
  345. unifiedSigner.resetMetrics()
  346. const message = new TextEncoder().encode('metrics test')
  347. // Perform some operations
  348. await unifiedSigner.sign('test-account', message, { collectMetrics: true })
  349. await unifiedSigner.sign('test-account', message, { collectMetrics: true })
  350. const metrics = unifiedSigner.getMetrics()
  351. expect(metrics.totalOperations).toBe(2)
  352. expect(metrics.successfulOperations).toBe(2)
  353. expect(metrics.failedOperations).toBe(0)
  354. expect(metrics.averageSigningTime).toBeGreaterThan(0)
  355. expect(metrics.platformBreakdown[Platform.PACIFICA].operations).toBe(2)
  356. expect(metrics.platformBreakdown[Platform.PACIFICA].successRate).toBe(1)
  357. })
  358. test('should track failures in metrics', async () => {
  359. mockGetAccount.mockResolvedValue(null) // Always fail
  360. unifiedSigner.resetMetrics()
  361. const message = new TextEncoder().encode('failure test')
  362. // Perform failing operations
  363. await unifiedSigner.sign('non-existent', message, { collectMetrics: true })
  364. await unifiedSigner.sign('non-existent', message, { collectMetrics: true })
  365. const metrics = unifiedSigner.getMetrics()
  366. expect(metrics.totalOperations).toBe(2)
  367. expect(metrics.successfulOperations).toBe(0)
  368. expect(metrics.failedOperations).toBe(2)
  369. })
  370. test('should reset metrics correctly', () => {
  371. unifiedSigner.resetMetrics()
  372. const metrics = unifiedSigner.getMetrics()
  373. expect(metrics.totalOperations).toBe(0)
  374. expect(metrics.successfulOperations).toBe(0)
  375. expect(metrics.failedOperations).toBe(0)
  376. expect(metrics.averageSigningTime).toBe(0)
  377. expect(metrics.lastResetAt).toBeInstanceOf(Date)
  378. })
  379. test('should calculate platform-specific metrics correctly', async () => {
  380. const pacificaAccount = {
  381. id: 'pac-account',
  382. platform: Platform.PACIFICA,
  383. credentials: {
  384. type: 'ed25519' as const,
  385. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  386. }
  387. }
  388. const asterAccount = {
  389. id: 'ast-account',
  390. platform: Platform.ASTER,
  391. credentials: {
  392. type: 'secp256k1' as const,
  393. privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
  394. }
  395. }
  396. mockGetAccount
  397. .mockResolvedValueOnce(pacificaAccount)
  398. .mockResolvedValueOnce(asterAccount)
  399. .mockResolvedValueOnce(null) // Failure
  400. unifiedSigner.resetMetrics()
  401. const message = new TextEncoder().encode('platform metrics test')
  402. await unifiedSigner.sign('pac-account', message, { collectMetrics: true })
  403. await unifiedSigner.sign('ast-account', message, { collectMetrics: true })
  404. await unifiedSigner.sign('non-existent', message, { collectMetrics: true })
  405. const metrics = unifiedSigner.getMetrics()
  406. expect(metrics.platformBreakdown[Platform.PACIFICA].operations).toBe(1)
  407. expect(metrics.platformBreakdown[Platform.PACIFICA].successRate).toBe(1)
  408. expect(metrics.platformBreakdown[Platform.ASTER].operations).toBe(1)
  409. expect(metrics.platformBreakdown[Platform.ASTER].successRate).toBe(1)
  410. })
  411. })
  412. describe('Timeout Handling', () => {
  413. test('should respect custom timeout settings', async () => {
  414. const slowAccount = {
  415. id: 'slow-account',
  416. platform: Platform.PACIFICA,
  417. credentials: {
  418. type: 'ed25519' as const,
  419. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  420. }
  421. }
  422. // Mock a slow account getter
  423. mockGetAccount.mockImplementation(() =>
  424. new Promise(resolve => setTimeout(() => resolve(slowAccount), 100))
  425. )
  426. const message = new TextEncoder().encode('timeout test')
  427. // Test with very short timeout
  428. const result = await unifiedSigner.sign('slow-account', message, {
  429. timeout: 50 // 50ms timeout, but getter takes 100ms
  430. })
  431. expect(result.success).toBe(false)
  432. expect(result.error).toContain('timed out')
  433. })
  434. test('should use default timeout when not specified', async () => {
  435. const account = {
  436. id: 'normal-account',
  437. platform: Platform.PACIFICA,
  438. credentials: {
  439. type: 'ed25519' as const,
  440. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  441. }
  442. }
  443. mockGetAccount.mockResolvedValue(account)
  444. const message = new TextEncoder().encode('default timeout test')
  445. const result = await unifiedSigner.sign('normal-account', message)
  446. expect(result.success).toBe(true) // Should succeed with default timeout
  447. })
  448. })
  449. describe('Error Handling', () => {
  450. test('should handle account getter errors gracefully', async () => {
  451. mockGetAccount.mockRejectedValue(new Error('Database connection failed'))
  452. const message = new TextEncoder().encode('error test')
  453. const result = await unifiedSigner.sign('test-account', message)
  454. expect(result.success).toBe(false)
  455. expect(result.error).toContain('Database connection failed')
  456. })
  457. test('should handle signing errors gracefully', async () => {
  458. const invalidAccount = {
  459. id: 'invalid-account',
  460. platform: Platform.PACIFICA,
  461. credentials: {
  462. type: 'ed25519' as const,
  463. privateKey: 'invalid-key' // This will cause signing to fail
  464. }
  465. }
  466. mockGetAccount.mockResolvedValue(invalidAccount)
  467. const message = new TextEncoder().encode('signing error test')
  468. const result = await unifiedSigner.sign('invalid-account', message)
  469. expect(result.success).toBe(false)
  470. expect(result.error).toBeDefined()
  471. })
  472. test('should provide meaningful error messages', async () => {
  473. const testCases = [
  474. {
  475. mockReturn: null,
  476. expectedError: 'Account not found'
  477. },
  478. {
  479. mockReturn: { id: 'test', credentials: {} },
  480. expectedError: 'Cannot determine platform'
  481. }
  482. ]
  483. for (const testCase of testCases) {
  484. mockGetAccount.mockResolvedValueOnce(testCase.mockReturn)
  485. const message = new TextEncoder().encode('error message test')
  486. const result = await unifiedSigner.sign('test-account', message)
  487. expect(result.success).toBe(false)
  488. expect(result.error).toContain(testCase.expectedError)
  489. }
  490. })
  491. })
  492. describe('Integration with Platform Detectors', () => {
  493. test('should use platform detector when no platform specified', async () => {
  494. const pacificaAccount = {
  495. id: 'auto-detect-account',
  496. credentials: {
  497. type: 'ed25519' as const,
  498. privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5'
  499. }
  500. }
  501. mockGetAccount.mockResolvedValue(pacificaAccount)
  502. const message = new TextEncoder().encode('auto-detect test')
  503. const result = await unifiedSigner.sign('auto-detect-account', message)
  504. expect(result.success).toBe(true)
  505. expect(result.metadata?.platform).toBe(Platform.PACIFICA)
  506. })
  507. test('should handle platform detection failures', async () => {
  508. const unknownAccount = {
  509. id: 'unknown-account',
  510. credentials: {
  511. type: 'unknown-type' as any,
  512. privateKey: 'some-key'
  513. }
  514. }
  515. mockGetAccount.mockResolvedValue(unknownAccount)
  516. const message = new TextEncoder().encode('detection failure test')
  517. const result = await unifiedSigner.sign('unknown-account', message)
  518. expect(result.success).toBe(false)
  519. expect(result.error).toContain('Cannot determine platform')
  520. })
  521. })
  522. })