1+ import { expect } from 'chai'
2+ import chai from 'chai'
3+ import sinonChai from 'sinon-chai'
4+ import Sinon from 'sinon'
5+
6+ import * as httpUtils from '../../../../src/utils/http'
7+ import * as settingsFactory from '../../../../src/factories/settings-factory'
8+
9+ import { InvoiceStatus } from '../../../../src/@types/invoice'
10+ import { OpenNodeCallbackController } from '../../../../src/controllers/callbacks/opennode-callback-controller'
11+ import { hmacSha256 } from '../../../../src/utils/secret'
12+
13+ chai . use ( sinonChai )
14+
15+ describe ( 'OpenNodeCallbackController' , ( ) => {
16+ let createSettingsStub : Sinon . SinonStub
17+ let getRemoteAddressStub : Sinon . SinonStub
18+ let updateInvoiceStatusStub : Sinon . SinonStub
19+ let confirmInvoiceStub : Sinon . SinonStub
20+ let sendInvoiceUpdateNotificationStub : Sinon . SinonStub
21+ let statusStub : Sinon . SinonStub
22+ let setHeaderStub : Sinon . SinonStub
23+ let sendStub : Sinon . SinonStub
24+ let controller : OpenNodeCallbackController
25+ let request : any
26+ let response : any
27+ let previousOpenNodeApiKey : string | undefined
28+
29+ beforeEach ( ( ) => {
30+ previousOpenNodeApiKey = process . env . OPENNODE_API_KEY
31+ process . env . OPENNODE_API_KEY = 'test-api-key'
32+
33+ createSettingsStub = Sinon . stub ( settingsFactory , 'createSettings' ) . returns ( {
34+ payments : { processor : 'opennode' } ,
35+ } as any )
36+ getRemoteAddressStub = Sinon . stub ( httpUtils , 'getRemoteAddress' ) . returns ( '127.0.0.1' )
37+
38+ updateInvoiceStatusStub = Sinon . stub ( )
39+ confirmInvoiceStub = Sinon . stub ( )
40+ sendInvoiceUpdateNotificationStub = Sinon . stub ( )
41+
42+ controller = new OpenNodeCallbackController ( {
43+ updateInvoiceStatus : updateInvoiceStatusStub ,
44+ confirmInvoice : confirmInvoiceStub ,
45+ sendInvoiceUpdateNotification : sendInvoiceUpdateNotificationStub ,
46+ } as any )
47+
48+ statusStub = Sinon . stub ( )
49+ setHeaderStub = Sinon . stub ( )
50+ sendStub = Sinon . stub ( )
51+
52+ response = {
53+ send : sendStub ,
54+ setHeader : setHeaderStub ,
55+ status : statusStub ,
56+ }
57+
58+ statusStub . returns ( response )
59+ setHeaderStub . returns ( response )
60+ sendStub . returns ( response )
61+
62+ request = {
63+ body : { } ,
64+ headers : { } ,
65+ }
66+ } )
67+
68+ afterEach ( ( ) => {
69+ getRemoteAddressStub . restore ( )
70+ createSettingsStub . restore ( )
71+
72+ if ( typeof previousOpenNodeApiKey === 'undefined' ) {
73+ delete process . env . OPENNODE_API_KEY
74+ } else {
75+ process . env . OPENNODE_API_KEY = previousOpenNodeApiKey
76+ }
77+ } )
78+
79+ it ( 'rejects requests when OpenNode is not the configured payment processor' , async ( ) => {
80+ createSettingsStub . returns ( {
81+ payments : { processor : 'lnbits' } ,
82+ } as any )
83+
84+ await controller . handleRequest ( request , response )
85+
86+ expect ( statusStub ) . to . have . been . calledOnceWithExactly ( 403 )
87+ expect ( sendStub ) . to . have . been . calledOnceWithExactly ( 'Forbidden' )
88+ expect ( updateInvoiceStatusStub ) . not . to . have . been . called
89+ } )
90+
91+ it ( 'returns bad request for malformed callback bodies' , async ( ) => {
92+ request . body = {
93+ id : 'invoice-id' ,
94+ }
95+
96+ await controller . handleRequest ( request , response )
97+
98+ expect ( statusStub ) . to . have . been . calledOnceWithExactly ( 400 )
99+ expect ( setHeaderStub ) . to . have . been . calledOnceWithExactly ( 'content-type' , 'text/plain; charset=utf8' )
100+ expect ( sendStub ) . to . have . been . calledOnceWithExactly ( 'Bad Request' )
101+ expect ( updateInvoiceStatusStub ) . not . to . have . been . called
102+ } )
103+
104+ it ( 'rejects callbacks with mismatched hashed_order' , async ( ) => {
105+ request . body = {
106+ hashed_order : 'invalid' ,
107+ id : 'invoice-id' ,
108+ }
109+
110+ await controller . handleRequest ( request , response )
111+
112+ expect ( statusStub ) . to . have . been . calledOnceWithExactly ( 403 )
113+ expect ( sendStub ) . to . have . been . calledOnceWithExactly ( 'Forbidden' )
114+ expect ( updateInvoiceStatusStub ) . not . to . have . been . called
115+ } )
116+
117+ it ( 'accepts valid signed callbacks and processes the invoice update' , async ( ) => {
118+ request . body = {
119+ amount : 21 ,
120+ created_at : '2026-04-11T00:00:00.000Z' ,
121+ description : 'Admission fee' ,
122+ hashed_order : hmacSha256 ( 'test-api-key' , 'invoice-id' ) . toString ( 'hex' ) ,
123+ id : 'invoice-id' ,
124+ lightning : {
125+ expires_at : '2026-04-11T01:00:00.000Z' ,
126+ payreq : 'lnbc1test' ,
127+ } ,
128+ order_id : 'pubkey' ,
129+ status : 'unpaid' ,
130+ }
131+
132+ updateInvoiceStatusStub . resolves ( {
133+ confirmedAt : null ,
134+ status : InvoiceStatus . PENDING ,
135+ } )
136+
137+ await controller . handleRequest ( request , response )
138+
139+ expect ( updateInvoiceStatusStub ) . to . have . been . calledOnce
140+ expect ( confirmInvoiceStub ) . not . to . have . been . called
141+ expect ( sendInvoiceUpdateNotificationStub ) . not . to . have . been . called
142+ expect ( statusStub ) . to . have . been . calledOnceWithExactly ( 200 )
143+ expect ( sendStub ) . to . have . been . calledOnceWithExactly ( )
144+ } )
145+ } )
0 commit comments