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