Skip to content

Commit 0139bea

Browse files
committed
test: add unit tests for invoice and user repositories
1 parent f5093cb commit 0139bea

4 files changed

Lines changed: 544 additions & 1 deletion

File tree

.mocharc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
2+
spec: 'test/**/*.spec.ts',
23
extension: ['ts'],
34
require: ['ts-node/register'],
45
reporter: 'mochawesome',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"db:migrate:rollback": "knex migrate:rollback",
4444
"db:seed": "knex seed:run",
4545
"pretest:unit": "node -e \"require('fs').mkdirSync('.test-reports/unit', {recursive: true})\"",
46-
"test:unit": "mocha 'test/**/*.spec.ts'",
46+
"test:unit": "mocha",
4747
"test:unit:watch": "npm run test:unit -- --min --watch --watch-files src/**/*,test/**/*",
4848
"cover:unit": "nyc --report-dir .coverage/unit npm run test:unit",
4949
"docker:build": "docker build -t nostream .",
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import * as chai from 'chai'
2+
import * as sinon from 'sinon'
3+
import knex from 'knex'
4+
import sinonChai from 'sinon-chai'
5+
6+
import { DatabaseClient } from '../../../src/@types/base'
7+
import { DBInvoice, Invoice, InvoiceStatus, InvoiceUnit } from '../../../src/@types/invoice'
8+
import { IInvoiceRepository } from '../../../src/@types/repositories'
9+
import { InvoiceRepository } from '../../../src/repositories/invoice-repository'
10+
11+
chai.use(sinonChai)
12+
const { expect } = chai
13+
14+
const PUBKEY = '22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793'
15+
16+
function makeInvoice(overrides: Partial<Invoice> = {}): Invoice {
17+
const now = new Date()
18+
return {
19+
id: 'test-invoice-id',
20+
pubkey: PUBKEY,
21+
bolt11: 'lnbctest',
22+
amountRequested: 1000n,
23+
unit: InvoiceUnit.MSATS,
24+
status: InvoiceStatus.PENDING,
25+
description: 'test invoice',
26+
expiresAt: null,
27+
updatedAt: now,
28+
createdAt: now,
29+
...overrides,
30+
}
31+
}
32+
33+
function makeDBInvoice(overrides: Partial<DBInvoice> = {}): DBInvoice {
34+
const now = new Date()
35+
return {
36+
id: 'test-invoice-id',
37+
pubkey: Buffer.from(PUBKEY, 'hex'),
38+
bolt11: 'lnbctest',
39+
amount_requested: 1000n,
40+
amount_paid: null as any,
41+
unit: InvoiceUnit.MSATS,
42+
status: InvoiceStatus.PENDING,
43+
description: 'test invoice',
44+
confirmed_at: null as any,
45+
expires_at: null as any,
46+
updated_at: now,
47+
created_at: now,
48+
verify_url: '',
49+
...overrides,
50+
}
51+
}
52+
53+
describe('InvoiceRepository', () => {
54+
let repository: IInvoiceRepository
55+
let sandbox: sinon.SinonSandbox
56+
let dbClient: DatabaseClient
57+
58+
beforeEach(() => {
59+
sandbox = sinon.createSandbox()
60+
dbClient = knex({ client: 'pg' })
61+
repository = new InvoiceRepository(dbClient)
62+
})
63+
64+
afterEach(() => {
65+
dbClient.destroy()
66+
sandbox.restore()
67+
})
68+
69+
describe('.updateStatus', () => {
70+
it('returns a thenable with then, catch, and toString', () => {
71+
const result = repository.updateStatus(makeInvoice())
72+
73+
expect(result).to.have.property('then')
74+
expect(result).to.have.property('catch')
75+
expect(result).to.have.property('toString')
76+
})
77+
78+
it('toString generates UPDATE query targeting the invoice id', () => {
79+
const sql = repository.updateStatus(makeInvoice({ id: 'inv-123', status: InvoiceStatus.COMPLETED })).toString()
80+
81+
expect(sql).to.include('"invoices"')
82+
expect(sql).to.include("'completed'")
83+
expect(sql).to.include("'inv-123'")
84+
expect(sql).to.include('returning')
85+
})
86+
})
87+
88+
describe('.upsert', () => {
89+
it('returns a thenable with then, catch, and toString', () => {
90+
const result = repository.upsert(makeInvoice())
91+
92+
expect(result).to.have.property('then')
93+
expect(result).to.have.property('catch')
94+
expect(result).to.have.property('toString')
95+
})
96+
97+
it('uses the existing id when invoice has a string id', () => {
98+
const sql = repository.upsert(makeInvoice({ id: 'my-specific-id' })).toString()
99+
100+
expect(sql).to.include("'my-specific-id'")
101+
})
102+
103+
it('generates a UUID when invoice has no id', () => {
104+
const invoice = makeInvoice()
105+
delete (invoice as any).id
106+
107+
const sql = repository.upsert(invoice).toString()
108+
109+
expect(sql).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
110+
})
111+
112+
it('toString contains INSERT … on conflict merge for "invoices"', () => {
113+
const sql = repository.upsert(makeInvoice()).toString()
114+
115+
expect(sql).to.include('"invoices"')
116+
expect(sql).to.include('on conflict')
117+
expect(sql).to.include("'1000'")
118+
})
119+
})
120+
121+
describe('.findById', () => {
122+
it('returns undefined when invoice not found', async () => {
123+
const mockSelect = sandbox.stub().resolves([])
124+
const mockWhere = sandbox.stub().returns({ select: mockSelect })
125+
const mockClient = sandbox.stub().returns({ where: mockWhere }) as unknown as DatabaseClient
126+
127+
const repo = new InvoiceRepository(mockClient)
128+
const result = await repo.findById('nonexistent-id')
129+
130+
expect(result).to.be.undefined
131+
expect(mockWhere).to.have.been.calledWith('id', 'nonexistent-id')
132+
})
133+
134+
it('returns mapped Invoice when found', async () => {
135+
const dbRow = makeDBInvoice({ id: 'found-id', amount_requested: 2500n })
136+
const mockSelect = sandbox.stub().resolves([dbRow])
137+
const mockWhere = sandbox.stub().returns({ select: mockSelect })
138+
const mockClient = sandbox.stub().returns({ where: mockWhere }) as unknown as DatabaseClient
139+
140+
const repo = new InvoiceRepository(mockClient)
141+
const result = await repo.findById('found-id')
142+
143+
expect(result).to.not.be.undefined
144+
expect(result!.id).to.equal('found-id')
145+
expect(result!.pubkey).to.equal(PUBKEY)
146+
expect(result!.amountRequested).to.equal(2500n)
147+
})
148+
149+
it('maps amountPaid when present', async () => {
150+
const dbRow = makeDBInvoice({ amount_paid: 999n })
151+
const mockSelect = sandbox.stub().resolves([dbRow])
152+
const mockWhere = sandbox.stub().returns({ select: mockSelect })
153+
const mockClient = sandbox.stub().returns({ where: mockWhere }) as unknown as DatabaseClient
154+
155+
const repo = new InvoiceRepository(mockClient)
156+
const result = await repo.findById('test-invoice-id')
157+
158+
expect(result!.amountPaid).to.equal(999n)
159+
})
160+
})
161+
162+
describe('.findPendingInvoices', () => {
163+
it('returns mapped invoices with default offset=0 and limit=10', async () => {
164+
const dbRow = makeDBInvoice({ id: 'pending-id' })
165+
const mockSelect = sandbox.stub().resolves([dbRow])
166+
const mockLimit = sandbox.stub().returns({ select: mockSelect })
167+
const mockOffset = sandbox.stub().returns({ limit: mockLimit })
168+
const mockWhere = sandbox.stub().returns({ offset: mockOffset })
169+
const mockClient = sandbox.stub().returns({ where: mockWhere }) as unknown as DatabaseClient
170+
171+
const repo = new InvoiceRepository(mockClient)
172+
const results = await repo.findPendingInvoices()
173+
174+
expect(results).to.have.length(1)
175+
expect(results[0].id).to.equal('pending-id')
176+
expect(mockWhere).to.have.been.calledWith('status', InvoiceStatus.PENDING)
177+
expect(mockOffset).to.have.been.calledWith(0)
178+
expect(mockLimit).to.have.been.calledWith(10)
179+
})
180+
181+
it('forwards provided offset and limit', async () => {
182+
const mockSelect = sandbox.stub().resolves([])
183+
const mockLimit = sandbox.stub().returns({ select: mockSelect })
184+
const mockOffset = sandbox.stub().returns({ limit: mockLimit })
185+
const mockWhere = sandbox.stub().returns({ offset: mockOffset })
186+
const mockClient = sandbox.stub().returns({ where: mockWhere }) as unknown as DatabaseClient
187+
188+
const repo = new InvoiceRepository(mockClient)
189+
await repo.findPendingInvoices(5, 20)
190+
191+
expect(mockOffset).to.have.been.calledWith(5)
192+
expect(mockLimit).to.have.been.calledWith(20)
193+
})
194+
195+
it('returns empty array when no pending invoices exist', async () => {
196+
const mockSelect = sandbox.stub().resolves([])
197+
const mockLimit = sandbox.stub().returns({ select: mockSelect })
198+
const mockOffset = sandbox.stub().returns({ limit: mockLimit })
199+
const mockWhere = sandbox.stub().returns({ offset: mockOffset })
200+
const mockClient = sandbox.stub().returns({ where: mockWhere }) as unknown as DatabaseClient
201+
202+
const repo = new InvoiceRepository(mockClient)
203+
const results = await repo.findPendingInvoices()
204+
205+
expect(results).to.deep.equal([])
206+
})
207+
})
208+
209+
describe('.confirmInvoice', () => {
210+
it('calls client.raw with invoice id, stringified amount, and ISO date', async () => {
211+
const rawStub = sandbox.stub().resolves()
212+
const mockClient = { raw: rawStub } as unknown as DatabaseClient
213+
214+
const invoiceId = 'confirm-me'
215+
const amount = 5000n
216+
const confirmedAt = new Date('2024-01-15T10:00:00.000Z')
217+
218+
const repo = new InvoiceRepository(mockClient)
219+
await repo.confirmInvoice(invoiceId, amount, confirmedAt)
220+
221+
expect(rawStub).to.have.been.calledOnceWithExactly('select confirm_invoice(?, ?, ?)', [
222+
invoiceId,
223+
'5000',
224+
confirmedAt.toISOString(),
225+
])
226+
})
227+
228+
it('uses the injected client parameter over the default', async () => {
229+
const defaultRaw = sandbox.stub().resolves()
230+
const injectedRaw = sandbox.stub().resolves()
231+
const defaultClient = { raw: defaultRaw } as unknown as DatabaseClient
232+
const injectedClient = { raw: injectedRaw } as unknown as DatabaseClient
233+
234+
const repo = new InvoiceRepository(defaultClient)
235+
await repo.confirmInvoice('id', 100n, new Date(), injectedClient)
236+
237+
expect(defaultRaw).to.not.have.been.called
238+
expect(injectedRaw).to.have.been.calledOnce
239+
})
240+
241+
it('re-throws when client.raw rejects', async () => {
242+
const err = new Error('DB unavailable')
243+
const rawStub = sandbox.stub().rejects(err)
244+
const mockClient = { raw: rawStub } as unknown as DatabaseClient
245+
246+
const repo = new InvoiceRepository(mockClient)
247+
let thrown: Error | undefined
248+
249+
try {
250+
await repo.confirmInvoice('id', 100n, new Date())
251+
} catch (e) {
252+
thrown = e as Error
253+
}
254+
255+
expect(thrown).to.not.be.undefined
256+
expect(thrown!.message).to.equal('DB unavailable')
257+
})
258+
})
259+
})

0 commit comments

Comments
 (0)