Skip to content

Commit c337d7a

Browse files
test: add opennode callback integration scenarios
1 parent e9f148c commit c337d7a

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@opennode-callback
2+
Feature: OpenNode callback endpoint
3+
Scenario: rejects malformed callback body
4+
Given OpenNode callback processing is enabled
5+
When I post a malformed OpenNode callback
6+
Then the OpenNode callback response status is 400
7+
And the OpenNode callback response body is "Malformed body"
8+
9+
Scenario: rejects callback with invalid signature
10+
Given OpenNode callback processing is enabled
11+
When I post an OpenNode callback with an invalid signature
12+
Then the OpenNode callback response status is 403
13+
And the OpenNode callback response body is "Forbidden"
14+
15+
Scenario: accepts valid signed callback for pending invoice
16+
Given OpenNode callback processing is enabled
17+
And a pending OpenNode invoice exists
18+
When I post a signed OpenNode callback with status "processing"
19+
Then the OpenNode callback response status is 200
20+
And the OpenNode callback response body is empty
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { After, Given, Then, When } from '@cucumber/cucumber'
2+
import axios, { AxiosResponse } from 'axios'
3+
import { expect } from 'chai'
4+
import { randomUUID } from 'crypto'
5+
6+
import { getMasterDbClient } from '../../../../src/database/client'
7+
import { hmacSha256 } from '../../../../src/utils/secret'
8+
import { SettingsStatic } from '../../../../src/utils/settings'
9+
10+
const CALLBACK_URL = 'http://localhost:18808/callbacks/opennode'
11+
const OPENNODE_TEST_API_KEY = 'integration-opennode-api-key'
12+
const TEST_PUBKEY = 'a'.repeat(64)
13+
14+
const postOpenNodeCallback = async (body: Record<string, string>) => {
15+
const encodedBody = new URLSearchParams(body).toString()
16+
17+
return axios.post(
18+
CALLBACK_URL,
19+
encodedBody,
20+
{
21+
headers: {
22+
'content-type': 'application/x-www-form-urlencoded',
23+
},
24+
validateStatus: () => true,
25+
},
26+
)
27+
}
28+
29+
Given('OpenNode callback processing is enabled', function () {
30+
const settings = SettingsStatic._settings as any
31+
32+
this.parameters.previousOpenNodeCallbackSettings = settings
33+
this.parameters.previousOpenNodeApiKey = process.env.OPENNODE_API_KEY
34+
35+
SettingsStatic._settings = {
36+
...settings,
37+
payments: {
38+
...(settings?.payments ?? {}),
39+
processor: 'opennode',
40+
},
41+
}
42+
43+
process.env.OPENNODE_API_KEY = OPENNODE_TEST_API_KEY
44+
})
45+
46+
Given('a pending OpenNode invoice exists', async function () {
47+
const dbClient = getMasterDbClient()
48+
const invoiceId = `integration-opennode-${randomUUID()}`
49+
50+
await dbClient('invoices').insert({
51+
id: invoiceId,
52+
pubkey: Buffer.from(TEST_PUBKEY, 'hex'),
53+
bolt11: 'lnbc210n1integration',
54+
amount_requested: '21000',
55+
unit: 'sats',
56+
status: 'pending',
57+
description: 'open node integration callback test',
58+
expires_at: new Date(Date.now() + 15 * 60 * 1000),
59+
updated_at: new Date(),
60+
created_at: new Date(),
61+
})
62+
63+
this.parameters.openNodeInvoiceId = invoiceId
64+
this.parameters.openNodeInvoiceIds = [
65+
...(this.parameters.openNodeInvoiceIds ?? []),
66+
invoiceId,
67+
]
68+
})
69+
70+
When('I post a malformed OpenNode callback', async function () {
71+
this.parameters.openNodeResponse = await postOpenNodeCallback({
72+
id: 'missing-required-fields',
73+
})
74+
})
75+
76+
When('I post an OpenNode callback with an invalid signature', async function () {
77+
this.parameters.openNodeResponse = await postOpenNodeCallback({
78+
hashed_order: '0'.repeat(64),
79+
id: `integration-opennode-${randomUUID()}`,
80+
status: 'paid',
81+
})
82+
})
83+
84+
When('I post a signed OpenNode callback with status {string}', async function (status: string) {
85+
const id = this.parameters.openNodeInvoiceId
86+
const hashedOrder = hmacSha256(OPENNODE_TEST_API_KEY, id).toString('hex')
87+
88+
this.parameters.openNodeResponse = await postOpenNodeCallback({
89+
hashed_order: hashedOrder,
90+
id,
91+
status,
92+
})
93+
})
94+
95+
Then('the OpenNode callback response status is {int}', function (statusCode: number) {
96+
const response = this.parameters.openNodeResponse as AxiosResponse
97+
98+
expect(response.status).to.equal(statusCode)
99+
})
100+
101+
Then('the OpenNode callback response body is {string}', function (expectedBody: string) {
102+
const response = this.parameters.openNodeResponse as AxiosResponse
103+
104+
expect(response.data).to.equal(expectedBody)
105+
})
106+
107+
Then('the OpenNode callback response body is empty', function () {
108+
const response = this.parameters.openNodeResponse as AxiosResponse
109+
110+
expect(['', undefined, null]).to.include(response.data)
111+
})
112+
113+
After({ tags: '@opennode-callback' }, async function () {
114+
SettingsStatic._settings = this.parameters.previousOpenNodeCallbackSettings
115+
116+
if (typeof this.parameters.previousOpenNodeApiKey === 'undefined') {
117+
delete process.env.OPENNODE_API_KEY
118+
} else {
119+
process.env.OPENNODE_API_KEY = this.parameters.previousOpenNodeApiKey
120+
}
121+
122+
const invoiceIds = this.parameters.openNodeInvoiceIds ?? []
123+
if (invoiceIds.length > 0) {
124+
const dbClient = getMasterDbClient()
125+
await dbClient('invoices').whereIn('id', invoiceIds).delete()
126+
}
127+
128+
this.parameters.openNodeInvoiceId = undefined
129+
this.parameters.openNodeInvoiceIds = []
130+
this.parameters.openNodeResponse = undefined
131+
this.parameters.previousOpenNodeApiKey = undefined
132+
this.parameters.previousOpenNodeCallbackSettings = undefined
133+
})

0 commit comments

Comments
 (0)