|  | 
|  | 1 | +// Mock heavy deps before importing modules that depend on them | 
|  | 2 | +// Now import modules under test | 
|  | 3 | +import axios from 'axios' | 
|  | 4 | +import { Prisma } from '@prisma/client' | 
|  | 5 | +import prisma from 'prisma-local/clientInstance' | 
|  | 6 | +import { prismaMock } from 'prisma-local/mockedClient' | 
|  | 7 | +import { executeAddressTriggers } from 'services/triggerService' | 
|  | 8 | +import { parseTriggerPostData } from 'utils/validators' | 
|  | 9 | + | 
|  | 10 | +jest.mock('axios', () => ({ | 
|  | 11 | +  __esModule: true, | 
|  | 12 | +  default: { | 
|  | 13 | +    post: jest.fn() | 
|  | 14 | +  } | 
|  | 15 | +})) | 
|  | 16 | + | 
|  | 17 | +jest.mock('config', () => ({ | 
|  | 18 | +  __esModule: true, | 
|  | 19 | +  default: { | 
|  | 20 | +    triggerPOSTTimeout: 3000, | 
|  | 21 | +    networkBlockchainURLs: { | 
|  | 22 | +      ecash: ['https://xec.paybutton.org'], | 
|  | 23 | +      bitcoincash: ['https://bch.paybutton.org'] | 
|  | 24 | +    }, | 
|  | 25 | +    wsBaseURL: 'localhost:5000' | 
|  | 26 | +  } | 
|  | 27 | +})) | 
|  | 28 | + | 
|  | 29 | +// Also mock networkService to prevent it from importing and instantiating chronikService via relative path | 
|  | 30 | +jest.mock('services/networkService', () => ({ | 
|  | 31 | +  __esModule: true, | 
|  | 32 | +  getNetworkIdFromSlug: jest.fn((slug: string) => 1), | 
|  | 33 | +  getNetworkFromSlug: jest.fn(async (slug: string) => ({ id: 1, slug } as any)) | 
|  | 34 | +})) | 
|  | 35 | + | 
|  | 36 | +// Prevent real Chronik client initialization during this test suite | 
|  | 37 | +jest.mock('services/chronikService', () => ({ | 
|  | 38 | +  __esModule: true, | 
|  | 39 | +  multiBlockchainClient: { | 
|  | 40 | +    waitForStart: jest.fn(async () => {}), | 
|  | 41 | +    getUrls: jest.fn(() => ({ ecash: [], bitcoincash: [] })), | 
|  | 42 | +    getAllSubscribedAddresses: jest.fn(() => ({ ecash: [], bitcoincash: [] })), | 
|  | 43 | +    subscribeAddresses: jest.fn(async () => {}), | 
|  | 44 | +    syncAddresses: jest.fn(async () => ({ failedAddressesWithErrors: {}, successfulAddressesWithCount: {} })), | 
|  | 45 | +    getTransactionDetails: jest.fn(async () => ({ hash: '', version: 0, block: { hash: '', height: 0, timestamp: '0' }, inputs: [], outputs: [] })), | 
|  | 46 | +    getLastBlockTimestamp: jest.fn(async () => 0), | 
|  | 47 | +    getBalance: jest.fn(async () => 0n), | 
|  | 48 | +    syncAndSubscribeAddresses: jest.fn(async () => ({ failedAddressesWithErrors: {}, successfulAddressesWithCount: {} })) | 
|  | 49 | +  } | 
|  | 50 | +})) | 
|  | 51 | + | 
|  | 52 | +describe('Payment Trigger system', () => { | 
|  | 53 | +  beforeAll(() => { | 
|  | 54 | +    process.env.MASTER_SECRET_KEY = process.env.MASTER_SECRET_KEY ?? 'test-secret' | 
|  | 55 | +  }) | 
|  | 56 | + | 
|  | 57 | +  beforeEach(() => { | 
|  | 58 | +    jest.clearAllMocks() | 
|  | 59 | +  }) | 
|  | 60 | + | 
|  | 61 | +  it('parseTriggerPostData replaces <outputAddresses> and <address>, keeping index 0 as the primary address and preserves amounts', () => { | 
|  | 62 | +    const primaryAddress = 'ecash:qz3ye4namaqlca8zgvdju8uqa2wwx8twd5y8wjd9ru' | 
|  | 63 | +    const other = 'ecash:qrju9pgzn3m84q57ldjvxph30zrm8q7dlc8r8a3eyp' | 
|  | 64 | + | 
|  | 65 | +    const params = { | 
|  | 66 | +      amount: new Prisma.Decimal(12), | 
|  | 67 | +      currency: 'XEC', | 
|  | 68 | +      timestamp: 123456789, | 
|  | 69 | +      txId: 'mocked-txid', | 
|  | 70 | +      buttonName: 'Button Name', | 
|  | 71 | +      address: primaryAddress, | 
|  | 72 | +      opReturn: { message: '', paymentId: '', rawMessage: '' }, | 
|  | 73 | +      inputAddresses: [{ address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h', amount: new Prisma.Decimal(1) }], | 
|  | 74 | +      outputAddresses: [ | 
|  | 75 | +        { address: primaryAddress, amount: new Prisma.Decimal(5) }, | 
|  | 76 | +        { address: other, amount: new Prisma.Decimal(7) } | 
|  | 77 | +      ], | 
|  | 78 | +      value: '0.0002' | 
|  | 79 | +    } | 
|  | 80 | + | 
|  | 81 | +    const postData = '{"addr": <address>, "outs": <outputAddresses>}' | 
|  | 82 | +    const result = parseTriggerPostData({ | 
|  | 83 | +      userId: 'user-1', | 
|  | 84 | +      postData, | 
|  | 85 | +      postDataParameters: params | 
|  | 86 | +    }) | 
|  | 87 | + | 
|  | 88 | +  expect(result.addr).toBe(primaryAddress) | 
|  | 89 | +  expect(Array.isArray(result.outs)).toBe(true) | 
|  | 90 | +  expect(result.outs[0].address).toBe(primaryAddress) | 
|  | 91 | +  expect(result.outs.map((o: any) => o.address)).toEqual([primaryAddress, other]) | 
|  | 92 | +  // ensure amounts are present | 
|  | 93 | +  result.outs.forEach((o: any) => expect(o.amount).toBeDefined()) | 
|  | 94 | +  }) | 
|  | 95 | + | 
|  | 96 | +  it('executeAddressTriggers posts with outputAddresses containing primary at index 0', async () => { | 
|  | 97 | +    const primaryAddress = 'ecash:qz3ye4namaqlca8zgvdju8uqa2wwx8twd5y8wjd9ru' | 
|  | 98 | +    const other1 = 'ecash:qrju9pgzn3m84q57ldjvxph30zrm8q7dlc8r8a3eyp' | 
|  | 99 | +    const other2 = 'ecash:qrcn673f42dl4z8l3xpc0gr5kpxg7ea5mqhj3atxd3' | 
|  | 100 | + | 
|  | 101 | +    prismaMock.paybuttonTrigger.findMany.mockResolvedValue([ | 
|  | 102 | +      { | 
|  | 103 | +        id: 'trigger-1', | 
|  | 104 | +        isEmailTrigger: false, | 
|  | 105 | +        postURL: 'https://httpbin.org/post', | 
|  | 106 | +        postData: '{"address": <address>, "outputAddresses": <outputAddresses>}', | 
|  | 107 | +        paybutton: { | 
|  | 108 | +          name: 'My Paybutton', | 
|  | 109 | +          providerUserId: 'user-1' | 
|  | 110 | +        } | 
|  | 111 | +      } as any | 
|  | 112 | +    ]) | 
|  | 113 | +    prisma.paybuttonTrigger.findMany = prismaMock.paybuttonTrigger.findMany | 
|  | 114 | + | 
|  | 115 | +    prismaMock.paybutton.findFirstOrThrow.mockResolvedValue({ providerUserId: 'user-1' } as any) | 
|  | 116 | +    prisma.paybutton.findFirstOrThrow = prismaMock.paybutton.findFirstOrThrow | 
|  | 117 | + | 
|  | 118 | +    prismaMock.userProfile.findUniqueOrThrow.mockResolvedValue({ id: 'user-1', preferredCurrencyId: 1 } as any) | 
|  | 119 | +    prisma.userProfile.findUniqueOrThrow = prismaMock.userProfile.findUniqueOrThrow | 
|  | 120 | + | 
|  | 121 | +    prismaMock.triggerLog.create.mockResolvedValue({} as any) | 
|  | 122 | +    prisma.triggerLog.create = prismaMock.triggerLog.create | 
|  | 123 | + | 
|  | 124 | +    ;(axios as any).post.mockResolvedValue({ data: 'ok' }) | 
|  | 125 | + | 
|  | 126 | +    const broadcastTxData = { | 
|  | 127 | +      address: primaryAddress, | 
|  | 128 | +      messageType: 'NewTx', | 
|  | 129 | +      txs: [ | 
|  | 130 | +        { | 
|  | 131 | +          hash: 'mocked-hash', | 
|  | 132 | +          amount: new Prisma.Decimal(1), | 
|  | 133 | +          paymentId: '', | 
|  | 134 | +          confirmed: true, | 
|  | 135 | +          message: '', | 
|  | 136 | +          timestamp: 1700000000, | 
|  | 137 | +          address: primaryAddress, | 
|  | 138 | +          rawMessage: '', | 
|  | 139 | +          inputAddresses: [{ address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h', amount: new Prisma.Decimal(1) }], | 
|  | 140 | +          outputAddresses: [ | 
|  | 141 | +            { address: other1, amount: new Prisma.Decimal(2) }, | 
|  | 142 | +            { address: primaryAddress, amount: new Prisma.Decimal(3) }, | 
|  | 143 | +            { address: other2, amount: new Prisma.Decimal(4) } | 
|  | 144 | +          ], | 
|  | 145 | +          prices: [ | 
|  | 146 | +            { price: { value: new Prisma.Decimal('0.5'), quoteId: 1 } }, | 
|  | 147 | +            { price: { value: new Prisma.Decimal('0.6'), quoteId: 2 } } | 
|  | 148 | +          ] | 
|  | 149 | +        } | 
|  | 150 | +      ] | 
|  | 151 | +    } | 
|  | 152 | + | 
|  | 153 | +    await executeAddressTriggers(broadcastTxData as any, 1) | 
|  | 154 | + | 
|  | 155 | +    expect((axios as any).post).toHaveBeenCalledTimes(1) | 
|  | 156 | +    const postedBody = (axios as any).post.mock.calls[0][1] | 
|  | 157 | + | 
|  | 158 | +    expect(postedBody.address).toBe(primaryAddress) | 
|  | 159 | +  expect(Array.isArray(postedBody.outputAddresses)).toBe(true) | 
|  | 160 | +  expect(postedBody.outputAddresses[0].address).toBe(primaryAddress) | 
|  | 161 | +  expect(postedBody.outputAddresses.map((o: any) => o.address)).toEqual([primaryAddress, other1, other2]) | 
|  | 162 | +    // Ensure amounts carried over as decimals (stringifiable) | 
|  | 163 | +    postedBody.outputAddresses.forEach((o: any) => { | 
|  | 164 | +      expect(o.amount).toBeDefined() | 
|  | 165 | +    }) | 
|  | 166 | +  }) | 
|  | 167 | +}) | 
0 commit comments