Skip to content

Commit 1a1adb3

Browse files
test: add zebedee callback controller specs
1 parent 7d44d12 commit 1a1adb3

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import chai from 'chai'
2+
import chaiAsPromised from 'chai-as-promised'
3+
import sinon from 'sinon'
4+
import sinonChai from 'sinon-chai'
5+
6+
chai.use(sinonChai)
7+
chai.use(chaiAsPromised)
8+
const { expect } = chai
9+
10+
import * as httpUtils from '../../../../src/utils/http'
11+
import * as settingsFactory from '../../../../src/factories/settings-factory'
12+
import { InvoiceStatus, InvoiceUnit } from '../../../../src/@types/invoice'
13+
import { ZebedeeCallbackController } from '../../../../src/controllers/callbacks/zebedee-callback-controller'
14+
15+
const PUBKEY = 'a'.repeat(64)
16+
17+
const baseSettings: any = {
18+
payments: { processor: 'zebedee' },
19+
paymentsProcessors: {
20+
zebedee: { ipWhitelist: [] },
21+
},
22+
network: { remoteIpHeader: 'x-forwarded-for' },
23+
}
24+
25+
const validBody = {
26+
id: 'zebedee-invoice-id',
27+
status: 'completed',
28+
internalId: PUBKEY,
29+
amount: '1000',
30+
description: 'Zebedee callback',
31+
unit: 'msats',
32+
confirmedAt: '2030-01-01T00:01:00.000Z',
33+
invoice: {
34+
request: 'lnbc1zebedeeinvoice',
35+
},
36+
}
37+
38+
const makeRes = (): any => ({
39+
status: sinon.stub().returnsThis(),
40+
setHeader: sinon.stub().returnsThis(),
41+
send: sinon.stub().returnsThis(),
42+
})
43+
44+
const makeInvoice = (overrides: any = {}) => ({
45+
id: validBody.id,
46+
pubkey: PUBKEY,
47+
bolt11: validBody.invoice.request,
48+
amountRequested: 1000n,
49+
unit: InvoiceUnit.MSATS,
50+
status: InvoiceStatus.COMPLETED,
51+
description: validBody.description,
52+
confirmedAt: new Date('2030-01-01T00:01:00.000Z'),
53+
expiresAt: new Date('2030-01-01T00:15:00.000Z'),
54+
updatedAt: new Date('2030-01-01T00:01:00.000Z'),
55+
createdAt: new Date('2030-01-01T00:00:00.000Z'),
56+
...overrides,
57+
})
58+
59+
const makeController = (overrides: {
60+
paymentsService?: any
61+
} = {}) => {
62+
const paymentsService = overrides.paymentsService ?? {
63+
updateInvoiceStatus: sinon.stub().resolves(makeInvoice()),
64+
confirmInvoice: sinon.stub().resolves(),
65+
sendInvoiceUpdateNotification: sinon.stub().resolves(),
66+
}
67+
68+
return {
69+
controller: new ZebedeeCallbackController(paymentsService),
70+
paymentsService,
71+
}
72+
}
73+
74+
const makeReq = (overrides: any = {}): any => ({
75+
headers: {},
76+
body: validBody,
77+
socket: { remoteAddress: '1.2.3.4' },
78+
...overrides,
79+
})
80+
81+
describe('ZebedeeCallbackController', () => {
82+
let createSettingsStub: sinon.SinonStub
83+
let getRemoteAddressStub: sinon.SinonStub
84+
let consoleErrorStub: sinon.SinonStub
85+
86+
beforeEach(() => {
87+
createSettingsStub = sinon.stub(settingsFactory, 'createSettings').returns(baseSettings)
88+
getRemoteAddressStub = sinon.stub(httpUtils, 'getRemoteAddress').returns('1.2.3.4')
89+
consoleErrorStub = sinon.stub(console, 'error')
90+
})
91+
92+
afterEach(() => {
93+
createSettingsStub.restore()
94+
getRemoteAddressStub.restore()
95+
consoleErrorStub.restore()
96+
})
97+
98+
describe('authorization and validation', () => {
99+
it('allows request when zebedee whitelist settings are missing', async () => {
100+
createSettingsStub.returns({
101+
payments: { processor: 'zebedee' },
102+
network: { remoteIpHeader: 'x-forwarded-for' },
103+
})
104+
const { controller, paymentsService } = makeController()
105+
const res = makeRes()
106+
107+
await controller.handleRequest(makeReq(), res)
108+
109+
expect(paymentsService.updateInvoiceStatus).to.have.been.calledOnce
110+
expect(res.status).to.have.been.calledWith(200)
111+
expect(res.send).to.have.been.calledWith('OK')
112+
})
113+
114+
it('returns 400 for malformed request body', async () => {
115+
const { controller } = makeController()
116+
const res = makeRes()
117+
118+
await controller.handleRequest(
119+
makeReq({ body: { id: 'missing-required-fields' } }),
120+
res,
121+
)
122+
123+
expect(res.status).to.have.been.calledWith(400)
124+
expect(res.setHeader).to.have.been.calledWith('content-type', 'text/plain; charset=utf8')
125+
expect(res.send).to.have.been.calledWith('Malformed body')
126+
})
127+
128+
it('returns 403 when remote IP is not in whitelist', async () => {
129+
createSettingsStub.returns({
130+
...baseSettings,
131+
paymentsProcessors: {
132+
zebedee: { ipWhitelist: ['9.9.9.9'] },
133+
},
134+
})
135+
const { controller, paymentsService } = makeController()
136+
const res = makeRes()
137+
138+
await controller.handleRequest(makeReq(), res)
139+
140+
expect(res.status).to.have.been.calledWith(403)
141+
expect(res.send).to.have.been.calledWith('Forbidden')
142+
expect(paymentsService.updateInvoiceStatus).to.not.have.been.called
143+
})
144+
145+
it('returns 403 when zebedee is not the configured processor', async () => {
146+
createSettingsStub.returns({
147+
...baseSettings,
148+
payments: { processor: 'lnbits' },
149+
})
150+
const { controller, paymentsService } = makeController()
151+
const res = makeRes()
152+
153+
await controller.handleRequest(makeReq(), res)
154+
155+
expect(res.status).to.have.been.calledWith(403)
156+
expect(res.send).to.have.been.calledWith('Forbidden')
157+
expect(paymentsService.updateInvoiceStatus).to.not.have.been.called
158+
})
159+
})
160+
161+
describe('invoice state handling', () => {
162+
it('returns 200 without confirmation for pending invoices', async () => {
163+
const paymentsService = {
164+
updateInvoiceStatus: sinon.stub().resolves(makeInvoice({
165+
status: InvoiceStatus.PENDING,
166+
confirmedAt: null,
167+
})),
168+
confirmInvoice: sinon.stub().resolves(),
169+
sendInvoiceUpdateNotification: sinon.stub().resolves(),
170+
}
171+
const { controller } = makeController({ paymentsService })
172+
const res = makeRes()
173+
174+
await controller.handleRequest(
175+
makeReq({ body: { ...validBody, status: 'pending' } }),
176+
res,
177+
)
178+
179+
expect(res.status).to.have.been.calledWith(200)
180+
expect(paymentsService.confirmInvoice).to.not.have.been.called
181+
expect(paymentsService.sendInvoiceUpdateNotification).to.not.have.been.called
182+
})
183+
184+
it('confirms and notifies for completed invoices', async () => {
185+
const paymentsService = {
186+
updateInvoiceStatus: sinon.stub().resolves(makeInvoice({ status: InvoiceStatus.COMPLETED })),
187+
confirmInvoice: sinon.stub().resolves(),
188+
sendInvoiceUpdateNotification: sinon.stub().resolves(),
189+
}
190+
const { controller } = makeController({ paymentsService })
191+
const res = makeRes()
192+
193+
await controller.handleRequest(makeReq(), res)
194+
195+
expect(paymentsService.confirmInvoice).to.have.been.calledOnce
196+
expect(paymentsService.sendInvoiceUpdateNotification).to.have.been.calledOnce
197+
expect(paymentsService.confirmInvoice).to.have.been.calledWithMatch({
198+
id: validBody.id,
199+
pubkey: PUBKEY,
200+
status: InvoiceStatus.COMPLETED,
201+
amountPaid: 1000n,
202+
})
203+
204+
expect(res.status).to.have.been.calledWith(200)
205+
expect(res.setHeader).to.have.been.calledWith('content-type', 'text/plain; charset=utf8')
206+
expect(res.send).to.have.been.calledWith('OK')
207+
})
208+
})
209+
210+
describe('error propagation', () => {
211+
it('rejects when invoice update fails', async () => {
212+
const updateError = new Error('update failed')
213+
const paymentsService = {
214+
updateInvoiceStatus: sinon.stub().rejects(updateError),
215+
confirmInvoice: sinon.stub().resolves(),
216+
sendInvoiceUpdateNotification: sinon.stub().resolves(),
217+
}
218+
const { controller } = makeController({ paymentsService })
219+
220+
await expect(controller.handleRequest(makeReq(), makeRes())).to.eventually.be.rejectedWith(updateError)
221+
})
222+
223+
it('rejects when invoice confirmation fails', async () => {
224+
const confirmError = new Error('confirm failed')
225+
const paymentsService = {
226+
updateInvoiceStatus: sinon.stub().resolves(makeInvoice()),
227+
confirmInvoice: sinon.stub().rejects(confirmError),
228+
sendInvoiceUpdateNotification: sinon.stub().resolves(),
229+
}
230+
const { controller } = makeController({ paymentsService })
231+
232+
await expect(controller.handleRequest(makeReq(), makeRes())).to.eventually.be.rejectedWith(confirmError)
233+
})
234+
})
235+
})

0 commit comments

Comments
 (0)