|
8 | 8 | createMockContractInfo, |
9 | 9 | makeNetworkMock, |
10 | 10 | } from '@/__tests__/mocks/mocks'; |
11 | | -import { NetworkError, NotFoundError } from '@/core/errors'; |
| 11 | +import { NetworkError, NotFoundError, ValidationError } from '@/core/errors'; |
12 | 12 | import { HederaMirrornodeServiceDefaultImpl } from '@/core/services/mirrornode/hedera-mirrornode-service'; |
13 | 13 | import { |
14 | 14 | AccountBalanceOperator, |
@@ -395,6 +395,216 @@ describe('HederaMirrornodeServiceDefaultImpl', () => { |
395 | 395 | }); |
396 | 396 | }); |
397 | 397 |
|
| 398 | + describe('allowance endpoints', () => { |
| 399 | + it('should fetch HBAR allowances with correct URL', async () => { |
| 400 | + const { service } = setupService(); |
| 401 | + const mockResponse = { |
| 402 | + allowances: [ |
| 403 | + { |
| 404 | + owner: TEST_ACCOUNT_ID, |
| 405 | + spender: '0.0.5678', |
| 406 | + amount: 100000000, |
| 407 | + }, |
| 408 | + ], |
| 409 | + links: { next: null }, |
| 410 | + }; |
| 411 | + (global.fetch as jest.Mock).mockResolvedValue({ |
| 412 | + ok: true, |
| 413 | + json: jest.fn().mockResolvedValue(mockResponse), |
| 414 | + }); |
| 415 | + |
| 416 | + const result = await service.getHbarAllowances(TEST_ACCOUNT_ID); |
| 417 | + |
| 418 | + expect(global.fetch).toHaveBeenCalledWith( |
| 419 | + `${TESTNET_API_URL}/accounts/${TEST_ACCOUNT_ID}/allowances/crypto`, |
| 420 | + ); |
| 421 | + expect(result.allowances).toHaveLength(1); |
| 422 | + expect(result.allowances[0].amount).toBe(100000000n); |
| 423 | + }); |
| 424 | + |
| 425 | + it('should fetch token allowances with pagination params', async () => { |
| 426 | + const { service } = setupService(); |
| 427 | + const mockResponse = { |
| 428 | + allowances: [ |
| 429 | + { |
| 430 | + owner: TEST_ACCOUNT_ID, |
| 431 | + spender: '0.0.5678', |
| 432 | + token_id: TEST_TOKEN_ID, |
| 433 | + amount: '1000', |
| 434 | + }, |
| 435 | + ], |
| 436 | + links: { next: '/api/v1/accounts/0.0.1234/allowances/tokens?cursor=2' }, |
| 437 | + }; |
| 438 | + (global.fetch as jest.Mock).mockResolvedValue({ |
| 439 | + ok: true, |
| 440 | + json: jest.fn().mockResolvedValue(mockResponse), |
| 441 | + }); |
| 442 | + |
| 443 | + const result = await service.getTokenAllowances(TEST_ACCOUNT_ID, { |
| 444 | + limit: 100, |
| 445 | + cursor: 'abc123', |
| 446 | + }); |
| 447 | + |
| 448 | + expect(global.fetch).toHaveBeenCalledWith( |
| 449 | + `${TESTNET_API_URL}/accounts/${TEST_ACCOUNT_ID}/allowances/tokens?limit=100&cursor=abc123`, |
| 450 | + ); |
| 451 | + expect(result.links?.next).toBe( |
| 452 | + '/api/v1/accounts/0.0.1234/allowances/tokens?cursor=2', |
| 453 | + ); |
| 454 | + expect(result.allowances[0].amount).toBe(1000n); |
| 455 | + }); |
| 456 | + |
| 457 | + it('should reject unsafe numeric allowance amounts', async () => { |
| 458 | + const { service } = setupService(); |
| 459 | + (global.fetch as jest.Mock).mockResolvedValue({ |
| 460 | + ok: true, |
| 461 | + json: jest.fn().mockResolvedValue({ |
| 462 | + allowances: [ |
| 463 | + { |
| 464 | + owner: TEST_ACCOUNT_ID, |
| 465 | + spender: '0.0.5678', |
| 466 | + amount: Number.MAX_SAFE_INTEGER + 1, |
| 467 | + }, |
| 468 | + ], |
| 469 | + links: { next: null }, |
| 470 | + }), |
| 471 | + }); |
| 472 | + |
| 473 | + await expect(service.getHbarAllowances(TEST_ACCOUNT_ID)).rejects.toThrow( |
| 474 | + ValidationError, |
| 475 | + ); |
| 476 | + }); |
| 477 | + |
| 478 | + it('should fetch all HBAR allowance pages', async () => { |
| 479 | + const { service } = setupService(); |
| 480 | + (global.fetch as jest.Mock) |
| 481 | + .mockResolvedValueOnce({ |
| 482 | + ok: true, |
| 483 | + json: jest.fn().mockResolvedValue({ |
| 484 | + allowances: [ |
| 485 | + { owner: TEST_ACCOUNT_ID, spender: '0.0.5678', amount: '1' }, |
| 486 | + ], |
| 487 | + links: { |
| 488 | + next: '/api/v1/accounts/0.0.1234/allowances/crypto?cursor=page2', |
| 489 | + }, |
| 490 | + }), |
| 491 | + }) |
| 492 | + .mockResolvedValueOnce({ |
| 493 | + ok: true, |
| 494 | + json: jest.fn().mockResolvedValue({ |
| 495 | + allowances: [ |
| 496 | + { owner: TEST_ACCOUNT_ID, spender: '0.0.9999', amount: 2 }, |
| 497 | + ], |
| 498 | + links: { next: null }, |
| 499 | + }), |
| 500 | + }); |
| 501 | + |
| 502 | + const result = await service.getAllHbarAllowances(TEST_ACCOUNT_ID); |
| 503 | + |
| 504 | + expect(result.allowances.map((allowance) => allowance.amount)).toEqual([ |
| 505 | + 1n, |
| 506 | + 2n, |
| 507 | + ]); |
| 508 | + expect(global.fetch).toHaveBeenNthCalledWith( |
| 509 | + 1, |
| 510 | + `${TESTNET_API_URL}/accounts/${TEST_ACCOUNT_ID}/allowances/crypto?limit=100`, |
| 511 | + ); |
| 512 | + expect(global.fetch).toHaveBeenNthCalledWith( |
| 513 | + 2, |
| 514 | + `${TESTNET_API_URL}/accounts/${TEST_ACCOUNT_ID}/allowances/crypto?limit=100&cursor=page2`, |
| 515 | + ); |
| 516 | + }); |
| 517 | + |
| 518 | + it('should throw NetworkError on repeated allowance cursor', async () => { |
| 519 | + const { service } = setupService(); |
| 520 | + (global.fetch as jest.Mock) |
| 521 | + .mockResolvedValueOnce({ |
| 522 | + ok: true, |
| 523 | + json: jest.fn().mockResolvedValue({ |
| 524 | + allowances: [], |
| 525 | + links: { |
| 526 | + next: '/api/v1/accounts/0.0.1234/allowances/tokens?cursor=page2', |
| 527 | + }, |
| 528 | + }), |
| 529 | + }) |
| 530 | + .mockResolvedValueOnce({ |
| 531 | + ok: true, |
| 532 | + json: jest.fn().mockResolvedValue({ |
| 533 | + allowances: [], |
| 534 | + links: { |
| 535 | + next: '/api/v1/accounts/0.0.1234/allowances/tokens?cursor=page2', |
| 536 | + }, |
| 537 | + }), |
| 538 | + }); |
| 539 | + |
| 540 | + await expect( |
| 541 | + service.getAllTokenAllowances(TEST_ACCOUNT_ID), |
| 542 | + ).rejects.toThrow(NetworkError); |
| 543 | + }); |
| 544 | + |
| 545 | + it('should throw NetworkError when allowance pagination limit is exceeded', async () => { |
| 546 | + const { service } = setupService(); |
| 547 | + (global.fetch as jest.Mock).mockResolvedValue({ |
| 548 | + ok: true, |
| 549 | + json: jest.fn().mockImplementation(() => |
| 550 | + Promise.resolve({ |
| 551 | + allowances: [], |
| 552 | + links: { |
| 553 | + next: `/api/v1/accounts/0.0.1234/allowances/nfts?cursor=${String( |
| 554 | + (global.fetch as jest.Mock).mock.calls.length, |
| 555 | + )}`, |
| 556 | + }, |
| 557 | + }), |
| 558 | + ), |
| 559 | + }); |
| 560 | + |
| 561 | + await expect( |
| 562 | + service.getAllNftAllowances(TEST_ACCOUNT_ID), |
| 563 | + ).rejects.toThrow(NetworkError); |
| 564 | + expect(global.fetch).toHaveBeenCalledTimes(100); |
| 565 | + }); |
| 566 | + |
| 567 | + it('should fetch NFT allowances and parse all-serial approval', async () => { |
| 568 | + const { service } = setupService(); |
| 569 | + const mockResponse = { |
| 570 | + allowances: [ |
| 571 | + { |
| 572 | + owner: TEST_ACCOUNT_ID, |
| 573 | + spender: '0.0.5678', |
| 574 | + token_id: TEST_TOKEN_ID, |
| 575 | + serial_number: null, |
| 576 | + approved_for_all: true, |
| 577 | + }, |
| 578 | + ], |
| 579 | + links: { next: null }, |
| 580 | + }; |
| 581 | + (global.fetch as jest.Mock).mockResolvedValue({ |
| 582 | + ok: true, |
| 583 | + json: jest.fn().mockResolvedValue(mockResponse), |
| 584 | + }); |
| 585 | + |
| 586 | + const result = await service.getNftAllowances(TEST_ACCOUNT_ID); |
| 587 | + |
| 588 | + expect(global.fetch).toHaveBeenCalledWith( |
| 589 | + `${TESTNET_API_URL}/accounts/${TEST_ACCOUNT_ID}/allowances/nfts`, |
| 590 | + ); |
| 591 | + expect(result.allowances[0].approved_for_all).toBe(true); |
| 592 | + }); |
| 593 | + |
| 594 | + it('should throw NotFoundError for allowance HTTP 404', async () => { |
| 595 | + const { service } = setupService(); |
| 596 | + (global.fetch as jest.Mock).mockResolvedValue({ |
| 597 | + ok: false, |
| 598 | + status: 404, |
| 599 | + statusText: 'Not Found', |
| 600 | + }); |
| 601 | + |
| 602 | + await expect(service.getHbarAllowances(TEST_ACCOUNT_ID)).rejects.toThrow( |
| 603 | + NotFoundError, |
| 604 | + ); |
| 605 | + }); |
| 606 | + }); |
| 607 | + |
398 | 608 | describe('getAccounts', () => { |
399 | 609 | it('should fetch accounts without params and return mapped DTO', async () => { |
400 | 610 | const { service } = setupService(); |
|
0 commit comments