BinanceSigner.unit.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. /**
  2. * Unit Tests for BinanceSigner
  3. *
  4. * Tests the Binance signing strategy implementation in isolation.
  5. */
  6. import { BinanceSigner } from '@/core/credential-manager/signers/BinanceSigner'
  7. import { Platform } from '@/types/credential'
  8. describe('BinanceSigner Unit Tests', () => {
  9. let signer: BinanceSigner
  10. beforeEach(() => {
  11. signer = new BinanceSigner()
  12. })
  13. describe('Platform Properties', () => {
  14. test('should have correct platform identifier', () => {
  15. expect(signer.platform).toBe(Platform.BINANCE)
  16. })
  17. test('should have correct algorithm identifier', () => {
  18. expect(signer.algorithm).toBe('hmac-sha256')
  19. })
  20. })
  21. describe('HMAC Signing Operations', () => {
  22. test('should sign message with valid credentials', async () => {
  23. const message = new TextEncoder().encode('test message')
  24. const credentials = {
  25. type: 'hmac' as const,
  26. apiKey: 'test-api-key',
  27. secretKey: 'test-secret-key'
  28. }
  29. const signature = await signer.sign(message, credentials)
  30. expect(signature).toBeDefined()
  31. expect(typeof signature).toBe('string')
  32. expect(signature.length).toBe(64) // SHA256 hash is 32 bytes = 64 hex chars
  33. })
  34. test('should produce deterministic signatures', async () => {
  35. const message = new TextEncoder().encode('deterministic test')
  36. const credentials = {
  37. type: 'hmac' as const,
  38. apiKey: 'test-api-key',
  39. secretKey: 'test-secret-key'
  40. }
  41. const signature1 = await signer.sign(message, credentials)
  42. const signature2 = await signer.sign(message, credentials)
  43. expect(signature1).toBe(signature2)
  44. })
  45. test('should produce different signatures for different messages', async () => {
  46. const message1 = new TextEncoder().encode('message one')
  47. const message2 = new TextEncoder().encode('message two')
  48. const credentials = {
  49. type: 'hmac' as const,
  50. apiKey: 'test-api-key',
  51. secretKey: 'test-secret-key'
  52. }
  53. const signature1 = await signer.sign(message1, credentials)
  54. const signature2 = await signer.sign(message2, credentials)
  55. expect(signature1).not.toBe(signature2)
  56. })
  57. test('should produce different signatures for different secret keys', async () => {
  58. const message = new TextEncoder().encode('same message')
  59. const credentials1 = {
  60. type: 'hmac' as const,
  61. apiKey: 'test-api-key',
  62. secretKey: 'secret-key-one'
  63. }
  64. const credentials2 = {
  65. type: 'hmac' as const,
  66. apiKey: 'test-api-key',
  67. secretKey: 'secret-key-two'
  68. }
  69. const signature1 = await signer.sign(message, credentials1)
  70. const signature2 = await signer.sign(message, credentials2)
  71. expect(signature1).not.toBe(signature2)
  72. })
  73. test('should handle empty messages', async () => {
  74. const emptyMessage = new Uint8Array(0)
  75. const credentials = {
  76. type: 'hmac' as const,
  77. apiKey: 'test-api-key',
  78. secretKey: 'test-secret-key'
  79. }
  80. const signature = await signer.sign(emptyMessage, credentials)
  81. expect(signature).toBeDefined()
  82. expect(typeof signature).toBe('string')
  83. expect(signature.length).toBe(64)
  84. })
  85. test('should handle large messages', async () => {
  86. const largeMessage = new Uint8Array(1024 * 1024) // 1MB
  87. largeMessage.fill(42)
  88. const credentials = {
  89. type: 'hmac' as const,
  90. apiKey: 'test-api-key',
  91. secretKey: 'test-secret-key'
  92. }
  93. const signature = await signer.sign(largeMessage, credentials)
  94. expect(signature).toBeDefined()
  95. expect(typeof signature).toBe('string')
  96. expect(signature.length).toBe(64)
  97. })
  98. test('should handle special characters in keys', async () => {
  99. const message = new TextEncoder().encode('test message')
  100. const credentials = {
  101. type: 'hmac' as const,
  102. apiKey: 'api-key-with-special-chars!@#$%^&*()',
  103. secretKey: 'secret-key-with-unicode-世界-and-emojis-🚀'
  104. }
  105. const signature = await signer.sign(message, credentials)
  106. expect(signature).toBeDefined()
  107. expect(typeof signature).toBe('string')
  108. expect(signature.length).toBe(64)
  109. })
  110. test('should throw error for invalid credentials', async () => {
  111. const message = new TextEncoder().encode('test message')
  112. const invalidCredentials = [
  113. {
  114. type: 'hmac' as const,
  115. apiKey: '', // Empty API key
  116. secretKey: 'test-secret-key'
  117. },
  118. {
  119. type: 'hmac' as const,
  120. apiKey: 'test-api-key',
  121. secretKey: '' // Empty secret key
  122. },
  123. {
  124. type: 'hmac' as const,
  125. apiKey: 'test-api-key'
  126. // Missing secret key
  127. },
  128. {
  129. type: 'wrong-type' as any,
  130. apiKey: 'test-api-key',
  131. secretKey: 'test-secret-key'
  132. }
  133. ]
  134. for (const invalidCred of invalidCredentials) {
  135. await expect(signer.sign(message, invalidCred))
  136. .rejects.toThrow()
  137. }
  138. })
  139. })
  140. describe('Verification Operations', () => {
  141. test('should verify valid signature', async () => {
  142. const message = new TextEncoder().encode('verification test')
  143. const credentials = {
  144. type: 'hmac' as const,
  145. apiKey: 'test-api-key',
  146. secretKey: 'test-secret-key'
  147. }
  148. const signature = await signer.sign(message, credentials)
  149. // For HMAC, verification is just re-computing and comparing
  150. const isValid = await signer.verify(message, signature, credentials.secretKey)
  151. expect(isValid).toBe(true)
  152. })
  153. test('should reject invalid signature', async () => {
  154. const message = new TextEncoder().encode('verification test')
  155. const credentials = {
  156. type: 'hmac' as const,
  157. apiKey: 'test-api-key',
  158. secretKey: 'test-secret-key'
  159. }
  160. const signature = await signer.sign(message, credentials)
  161. // Modify signature to make it invalid
  162. const invalidSignature = signature.slice(0, -2) + '00'
  163. const isValid = await signer.verify(message, invalidSignature, credentials.secretKey)
  164. expect(isValid).toBe(false)
  165. })
  166. test('should reject signature with wrong secret key', async () => {
  167. const message = new TextEncoder().encode('verification test')
  168. const credentials = {
  169. type: 'hmac' as const,
  170. apiKey: 'test-api-key',
  171. secretKey: 'correct-secret-key'
  172. }
  173. const signature = await signer.sign(message, credentials)
  174. const isValid = await signer.verify(message, signature, 'wrong-secret-key')
  175. expect(isValid).toBe(false)
  176. })
  177. test('should reject signature with wrong message', async () => {
  178. const message1 = new TextEncoder().encode('original message')
  179. const message2 = new TextEncoder().encode('different message')
  180. const credentials = {
  181. type: 'hmac' as const,
  182. apiKey: 'test-api-key',
  183. secretKey: 'test-secret-key'
  184. }
  185. const signature = await signer.sign(message1, credentials)
  186. const isValid = await signer.verify(message2, signature, credentials.secretKey)
  187. expect(isValid).toBe(false)
  188. })
  189. test('should handle malformed signature gracefully', async () => {
  190. const message = new TextEncoder().encode('test message')
  191. const secretKey = 'test-secret-key'
  192. const malformedSignatures = [
  193. '', // Empty
  194. 'invalid', // Not hex
  195. '12345', // Too short
  196. 'g'.repeat(64) // Invalid hex characters
  197. ]
  198. for (const malformedSig of malformedSignatures) {
  199. const isValid = await signer.verify(message, malformedSig, secretKey)
  200. expect(isValid).toBe(false)
  201. }
  202. })
  203. })
  204. describe('Binance-Specific Query String Signing', () => {
  205. test('should sign query parameters correctly', async () => {
  206. const queryParams = 'symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559'
  207. const message = new TextEncoder().encode(queryParams)
  208. const credentials = {
  209. type: 'hmac' as const,
  210. apiKey: 'vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A',
  211. secretKey: 'NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j'
  212. }
  213. const signature = await signer.sign(message, credentials)
  214. // This should match Binance's expected signature format
  215. expect(signature).toBeDefined()
  216. expect(signature.length).toBe(64)
  217. expect(/^[0-9a-f]{64}$/i.test(signature)).toBe(true)
  218. })
  219. test('should handle URL encoding properly', async () => {
  220. const queryWithEncoding = 'symbol=BTC%2FUSDT&side=BUY&quantity=1.0&timestamp=1635724800000'
  221. const message = new TextEncoder().encode(queryWithEncoding)
  222. const credentials = {
  223. type: 'hmac' as const,
  224. apiKey: 'test-api-key',
  225. secretKey: 'test-secret-key'
  226. }
  227. const signature = await signer.sign(message, credentials)
  228. expect(signature).toBeDefined()
  229. expect(signature.length).toBe(64)
  230. })
  231. test('should handle timestamp parameter correctly', async () => {
  232. const timestamp = Date.now()
  233. const queryWithTimestamp = `symbol=BTCUSDT&side=BUY&type=MARKET&quantity=0.001&timestamp=${timestamp}`
  234. const message = new TextEncoder().encode(queryWithTimestamp)
  235. const credentials = {
  236. type: 'hmac' as const,
  237. apiKey: 'test-api-key',
  238. secretKey: 'test-secret-key'
  239. }
  240. const signature = await signer.sign(message, credentials)
  241. expect(signature).toBeDefined()
  242. expect(signature.length).toBe(64)
  243. // Should be reproducible with same timestamp
  244. const signature2 = await signer.sign(message, credentials)
  245. expect(signature).toBe(signature2)
  246. })
  247. })
  248. describe('Performance Requirements', () => {
  249. test('should sign within performance requirements', async () => {
  250. const message = new TextEncoder().encode('performance test')
  251. const credentials = {
  252. type: 'hmac' as const,
  253. apiKey: 'test-api-key',
  254. secretKey: 'test-secret-key'
  255. }
  256. const startTime = performance.now()
  257. await signer.sign(message, credentials)
  258. const duration = performance.now() - startTime
  259. expect(duration).toBeLessThan(25) // HMAC should be very fast
  260. })
  261. test('should verify within performance requirements', async () => {
  262. const message = new TextEncoder().encode('verification performance test')
  263. const credentials = {
  264. type: 'hmac' as const,
  265. apiKey: 'test-api-key',
  266. secretKey: 'test-secret-key'
  267. }
  268. const signature = await signer.sign(message, credentials)
  269. const startTime = performance.now()
  270. await signer.verify(message, signature, credentials.secretKey)
  271. const duration = performance.now() - startTime
  272. expect(duration).toBeLessThan(25) // HMAC verification should be very fast
  273. })
  274. test('should handle concurrent operations efficiently', async () => {
  275. const message = new TextEncoder().encode('concurrent test')
  276. const credentials = {
  277. type: 'hmac' as const,
  278. apiKey: 'test-api-key',
  279. secretKey: 'test-secret-key'
  280. }
  281. const startTime = performance.now()
  282. const promises = Array.from({ length: 20 }, () => signer.sign(message, credentials))
  283. const signatures = await Promise.all(promises)
  284. const duration = performance.now() - startTime
  285. expect(signatures).toHaveLength(20)
  286. expect(signatures.every(sig => sig === signatures[0])).toBe(true) // Deterministic
  287. expect(duration).toBeLessThan(100) // 20 concurrent operations should be very fast
  288. })
  289. test('should handle high-frequency trading scenarios', async () => {
  290. const credentials = {
  291. type: 'hmac' as const,
  292. apiKey: 'test-api-key',
  293. secretKey: 'test-secret-key'
  294. }
  295. // Simulate rapid order signing
  296. const startTime = performance.now()
  297. const signatures: string[] = []
  298. for (let i = 0; i < 100; i++) {
  299. const timestamp = Date.now() + i
  300. const queryString = `symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=0.001&price=50000&timestamp=${timestamp}`
  301. const message = new TextEncoder().encode(queryString)
  302. const signature = await signer.sign(message, credentials)
  303. signatures.push(signature)
  304. }
  305. const duration = performance.now() - startTime
  306. expect(signatures).toHaveLength(100)
  307. expect(new Set(signatures).size).toBe(100) // All should be unique due to timestamps
  308. expect(duration).toBeLessThan(200) // 100 signatures in 200ms for HFT
  309. })
  310. })
  311. describe('Credential Validation', () => {
  312. test('should accept valid HMAC credentials', () => {
  313. const validCredentials = [
  314. {
  315. type: 'hmac' as const,
  316. apiKey: 'test-api-key',
  317. secretKey: 'test-secret-key'
  318. },
  319. {
  320. type: 'hmac' as const,
  321. apiKey: 'API_KEY_WITH_UNDERSCORES',
  322. secretKey: 'SECRET_KEY_WITH_UNDERSCORES'
  323. },
  324. {
  325. type: 'hmac' as const,
  326. apiKey: 'api-key-with-dashes',
  327. secretKey: 'secret-key-with-dashes'
  328. },
  329. {
  330. type: 'hmac' as const,
  331. apiKey: '1234567890',
  332. secretKey: 'abcdefghij'
  333. }
  334. ]
  335. validCredentials.forEach(creds => {
  336. expect(() => signer.validateCredentials(creds)).not.toThrow()
  337. })
  338. })
  339. test('should reject invalid credential types', () => {
  340. const invalidCredentials = [
  341. {
  342. type: 'ed25519' as any,
  343. apiKey: 'test-api-key',
  344. secretKey: 'test-secret-key'
  345. },
  346. {
  347. type: 'secp256k1' as any,
  348. privateKey: '0x1234567890abcdef'
  349. },
  350. {
  351. type: 'rsa' as any,
  352. apiKey: 'test-api-key',
  353. secretKey: 'test-secret-key'
  354. }
  355. ]
  356. invalidCredentials.forEach(creds => {
  357. expect(() => signer.validateCredentials(creds)).toThrow()
  358. })
  359. })
  360. test('should reject credentials with missing fields', () => {
  361. const incompleteCredentials = [
  362. {
  363. type: 'hmac' as const,
  364. apiKey: 'test-api-key'
  365. // Missing secretKey
  366. },
  367. {
  368. type: 'hmac' as const,
  369. secretKey: 'test-secret-key'
  370. // Missing apiKey
  371. },
  372. {
  373. type: 'hmac' as const,
  374. apiKey: '',
  375. secretKey: 'test-secret-key'
  376. },
  377. {
  378. type: 'hmac' as const,
  379. apiKey: 'test-api-key',
  380. secretKey: ''
  381. }
  382. ]
  383. incompleteCredentials.forEach(creds => {
  384. expect(() => signer.validateCredentials(creds)).toThrow()
  385. })
  386. })
  387. })
  388. describe('Edge Cases', () => {
  389. test('should handle null and undefined inputs gracefully', async () => {
  390. const credentials = {
  391. type: 'hmac' as const,
  392. apiKey: 'test-api-key',
  393. secretKey: 'test-secret-key'
  394. }
  395. await expect(signer.sign(null as any, credentials)).rejects.toThrow()
  396. await expect(signer.sign(undefined as any, credentials)).rejects.toThrow()
  397. const message = new TextEncoder().encode('test')
  398. await expect(signer.sign(message, null as any)).rejects.toThrow()
  399. await expect(signer.sign(message, undefined as any)).rejects.toThrow()
  400. })
  401. test('should handle very large messages efficiently', async () => {
  402. const megaMessage = new Uint8Array(10 * 1024 * 1024) // 10MB
  403. megaMessage.fill(123)
  404. const credentials = {
  405. type: 'hmac' as const,
  406. apiKey: 'test-api-key',
  407. secretKey: 'test-secret-key'
  408. }
  409. const startTime = performance.now()
  410. const signature = await signer.sign(megaMessage, credentials)
  411. const duration = performance.now() - startTime
  412. expect(signature).toBeDefined()
  413. expect(duration).toBeLessThan(500) // HMAC should handle large messages efficiently
  414. })
  415. test('should maintain consistency across multiple instances', async () => {
  416. const signer1 = new BinanceSigner()
  417. const signer2 = new BinanceSigner()
  418. const message = new TextEncoder().encode('consistency test')
  419. const credentials = {
  420. type: 'hmac' as const,
  421. apiKey: 'test-api-key',
  422. secretKey: 'test-secret-key'
  423. }
  424. const signature1 = await signer1.sign(message, credentials)
  425. const signature2 = await signer2.sign(message, credentials)
  426. expect(signature1).toBe(signature2)
  427. })
  428. test('should handle binary data in messages', async () => {
  429. const binaryMessage = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252])
  430. const credentials = {
  431. type: 'hmac' as const,
  432. apiKey: 'test-api-key',
  433. secretKey: 'test-secret-key'
  434. }
  435. const signature = await signer.sign(binaryMessage, credentials)
  436. expect(signature).toBeDefined()
  437. expect(signature.length).toBe(64)
  438. expect(/^[0-9a-f]{64}$/i.test(signature)).toBe(true)
  439. })
  440. test('should handle keys with various encodings', async () => {
  441. const message = new TextEncoder().encode('encoding test')
  442. const credentials = [
  443. {
  444. type: 'hmac' as const,
  445. apiKey: 'base64-style-key==',
  446. secretKey: 'base64-style-secret=='
  447. },
  448. {
  449. type: 'hmac' as const,
  450. apiKey: 'hex-style-key-123abc',
  451. secretKey: 'hex-style-secret-456def'
  452. },
  453. {
  454. type: 'hmac' as const,
  455. apiKey: 'unicode-key-世界',
  456. secretKey: 'unicode-secret-🌍'
  457. }
  458. ]
  459. for (const creds of credentials) {
  460. const signature = await signer.sign(message, creds)
  461. expect(signature).toBeDefined()
  462. expect(signature.length).toBe(64)
  463. }
  464. })
  465. })
  466. describe('Binance API Compatibility', () => {
  467. test('should produce signatures compatible with Binance API format', async () => {
  468. // Test with realistic Binance API parameters
  469. const params = {
  470. symbol: 'BTCUSDT',
  471. side: 'BUY',
  472. type: 'LIMIT',
  473. timeInForce: 'GTC',
  474. quantity: '0.001',
  475. price: '50000.00',
  476. timestamp: '1635724800000',
  477. recvWindow: '5000'
  478. }
  479. const queryString = Object.entries(params)
  480. .sort(([a], [b]) => a.localeCompare(b)) // Binance requires sorted params
  481. .map(([key, value]) => `${key}=${value}`)
  482. .join('&')
  483. const message = new TextEncoder().encode(queryString)
  484. const credentials = {
  485. type: 'hmac' as const,
  486. apiKey: 'test-binance-api-key',
  487. secretKey: 'test-binance-secret-key'
  488. }
  489. const signature = await signer.sign(message, credentials)
  490. expect(signature).toBeDefined()
  491. expect(signature.length).toBe(64)
  492. expect(/^[0-9a-f]{64}$/i.test(signature)).toBe(true)
  493. // Signature should be verifiable
  494. const isValid = await signer.verify(message, signature, credentials.secretKey)
  495. expect(isValid).toBe(true)
  496. })
  497. test('should handle order parameter variations', async () => {
  498. const orderTypes = [
  499. 'symbol=ETHUSDT&side=BUY&type=MARKET&quantity=0.1',
  500. 'symbol=ADAUSDT&side=SELL&type=LIMIT&quantity=100&price=1.5',
  501. 'symbol=BNBUSDT&side=BUY&type=STOP_LOSS_LIMIT&quantity=10&price=400&stopPrice=390&timeInForce=GTC'
  502. ]
  503. const credentials = {
  504. type: 'hmac' as const,
  505. apiKey: 'test-api-key',
  506. secretKey: 'test-secret-key'
  507. }
  508. for (const orderParams of orderTypes) {
  509. const fullParams = `${orderParams}&timestamp=${Date.now()}`
  510. const message = new TextEncoder().encode(fullParams)
  511. const signature = await signer.sign(message, credentials)
  512. expect(signature).toBeDefined()
  513. expect(signature.length).toBe(64)
  514. // Should be verifiable
  515. const isValid = await signer.verify(message, signature, credentials.secretKey)
  516. expect(isValid).toBe(true)
  517. }
  518. })
  519. })
  520. })