• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cameri / nostream / 25601018106

09 May 2026 12:22PM UTC coverage: 33.99% (-31.1%) from 65.107%
25601018106

Pull #615

github

web-flow
Merge 1ef509ec3 into 36e5af87e
Pull Request #615: test: add unit tests for remaining app workers (#489)

788 of 3170 branches covered (24.86%)

Branch coverage included in aggregate %.

0 of 8 new or added lines in 2 files covered. (0.0%)

1822 existing lines in 87 files now uncovered.

2352 of 6068 relevant lines covered (38.76%)

13.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.69
/src/controllers/callbacks/opennode-callback-controller.ts
1
import { timingSafeEqual } from 'crypto'
1✔
2

3
import { Request, Response } from 'express'
4

5
import { Invoice, InvoiceStatus } from '../../@types/invoice'
1✔
6
import { createLogger } from '../../factories/logger-factory'
1✔
7
import { createSettings } from '../../factories/settings-factory'
1✔
8
import { getRemoteAddress } from '../../utils/http'
1✔
9
import { hmacSha256 } from '../../utils/secret'
1✔
10
import { IController } from '../../@types/controllers'
11
import { IPaymentsService } from '../../@types/services'
12
import { opennodeWebhookCallbackBodySchema } from '../../schemas/opennode-callback-schema'
1✔
13
import { validateSchema } from '../../utils/validation'
1✔
14

15
const logger = createLogger('opennode-callback-controller')
1✔
16

17
export class OpenNodeCallbackController implements IController {
1✔
18
  public constructor(private readonly paymentsService: IPaymentsService) {}
4✔
19

20
  public async handleRequest(request: Request, response: Response) {
21
    logger('request headers: %o', request.headers)
4✔
22

23
    const settings = createSettings()
4✔
24
    const remoteAddress = getRemoteAddress(request, settings)
4✔
25

26
    const bodyValidation = validateSchema(opennodeWebhookCallbackBodySchema)(request.body)
4✔
27
    if (bodyValidation.error) {
4✔
28
      logger('opennode callback request rejected: invalid body %o', bodyValidation.error)
1✔
29
      response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Malformed body')
1✔
30
      return
1✔
31
    }
32

33
    const body = bodyValidation.value
3✔
34
    logger(
3✔
35
      'request body metadata: hasId=%s hasHashedOrder=%s status=%s',
36
      typeof body.id === 'string',
37
      typeof body.hashed_order === 'string',
38
      body.status,
39
    )
40

41
    const openNodeApiKey = process.env.OPENNODE_API_KEY
3✔
42
    if (!openNodeApiKey) {
3!
UNCOV
43
      logger('OPENNODE_API_KEY is not configured; unable to verify OpenNode callback from %s', remoteAddress)
×
UNCOV
44
      response
×
45
        .status(500)
46
        .setHeader('content-type', 'text/plain; charset=utf8')
47
        .send('Internal Server Error')
UNCOV
48
      return
×
49
    }
50

51
    const expectedBuf = hmacSha256(openNodeApiKey, body.id)
3✔
52
    const actualHex = body.hashed_order
3✔
53
    const expectedHexLength = expectedBuf.length * 2
3✔
54

55
    if (
3!
56
      actualHex.length !== expectedHexLength
6✔
57
      || !/^[0-9a-f]+$/i.test(actualHex)
58
    ) {
UNCOV
59
      logger('invalid hashed_order format from %s to /callbacks/opennode', remoteAddress)
×
UNCOV
60
      response
×
61
        .status(400)
62
        .setHeader('content-type', 'text/plain; charset=utf8')
63
        .send('Bad Request')
UNCOV
64
      return
×
65
    }
66

67
    const actualBuf = Buffer.from(actualHex, 'hex')
3✔
68

69
    if (
3✔
70
      !timingSafeEqual(expectedBuf, actualBuf)
71
    ) {
72
      logger('unauthorized request from %s to /callbacks/opennode: hashed_order mismatch', remoteAddress)
1✔
73
      response
1✔
74
        .status(403)
75
        .send('Forbidden')
76
      return
1✔
77
    }
78

79
    const statusMap: Record<string, InvoiceStatus> = {
2✔
80
      expired: InvoiceStatus.EXPIRED,
81
      refunded: InvoiceStatus.EXPIRED,
82
      unpaid: InvoiceStatus.PENDING,
83
      processing: InvoiceStatus.PENDING,
84
      underpaid: InvoiceStatus.PENDING,
85
      paid: InvoiceStatus.COMPLETED,
86
    }
87

88
    const invoice: Pick<Invoice, 'id' | 'status'> = {
2✔
89
      id: body.id,
90
      status: statusMap[body.status],
91
    }
92

93
    logger('invoice', invoice)
2✔
94

95
    let updatedInvoice: Invoice
96
    try {
2✔
97
      updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice)
2✔
98
    } catch (error) {
UNCOV
99
      logger.error(`Unable to persist invoice ${invoice.id}`, error)
×
100

UNCOV
101
      throw error
×
102
    }
103

104
    if (updatedInvoice.status !== InvoiceStatus.COMPLETED) {
2✔
105
      response
1✔
106
        .status(200)
107
        .send()
108

109
      return
1✔
110
    }
111

112
    if (!updatedInvoice.confirmedAt) {
1!
113
      updatedInvoice.confirmedAt = new Date()
1✔
114
    }
115
    updatedInvoice.amountPaid = updatedInvoice.amountRequested
1✔
116

117
    try {
1✔
118
      await this.paymentsService.confirmInvoice({
1✔
119
        id: updatedInvoice.id,
120
        pubkey: updatedInvoice.pubkey,
121
        status: updatedInvoice.status,
122
        amountPaid: updatedInvoice.amountPaid,
123
        confirmedAt: updatedInvoice.confirmedAt,
124
      })
125
      await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice)
1✔
126
    } catch (error) {
UNCOV
127
      logger.error(`Unable to confirm invoice ${invoice.id}`, error)
×
128

UNCOV
129
      throw error
×
130
    }
131

132
    response.status(200).setHeader('content-type', 'text/plain; charset=utf8').send('OK')
1✔
133
  }
134
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc