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 ( 'rejects callbacks with mismatched hashed_order' , async ( ) => {
104+ request . body = {
105+ hashed_order : 'invalid' ,
106+ id : 'invoice-id' ,
107+ }
108+
109+ await controller . handleRequest ( request , response )
110+
111+ expect ( statusStub ) . to . have . been . calledOnceWithExactly ( 403 )
112+ expect ( sendStub ) . to . have . been . calledOnceWithExactly ( 'Forbidden' )
113+ expect ( updateInvoiceStatusStub ) . not . to . have . been . called
114+ } )
115+
116+ it ( 'accepts valid signed callbacks and processes the invoice update' , async ( ) => {
117+ request . body = {
118+ amount : 21 ,
119+ created_at : '2026-04-11T00:00:00.000Z' ,
120+ description : 'Admission fee' ,
121+ hashed_order : hmacSha256 ( 'test-api-key' , 'invoice-id' ) . toString ( 'hex' ) ,
122+ id : 'invoice-id' ,
123+ lightning : {
124+ expires_at : '2026-04-11T01:00:00.000Z' ,
125+ payreq : 'lnbc1test' ,
126+ } ,
127+ order_id : 'pubkey' ,
128+ status : 'unpaid' ,
129+ }
130+
131+ updateInvoiceStatusStub . resolves ( {
132+ confirmedAt : null ,
133+ status : InvoiceStatus . PENDING ,
134+ } )
135+
136+ await controller . handleRequest ( request , response )
137+
138+ expect ( updateInvoiceStatusStub ) . to . have . been . calledOnce
139+ expect ( confirmInvoiceStub ) . not . to . have . been . called
140+ expect ( sendInvoiceUpdateNotificationStub ) . not . to . have . been . called
141+ expect ( statusStub ) . to . have . been . calledOnceWithExactly ( 200 )
142+ expect ( sendStub ) . to . have . been . calledOnceWithExactly ( )
143+ } )
144+ } )
0 commit comments