Skip to content

Commit 7fc0552

Browse files
CKodidelacameri
andauthored
test(unit): add InvoiceRepository and UserRepository unit tests (#551)
* test(unit): add InvoiceRepository and UserRepository unit tests * test(unit): tighten pubkey buffer and UUID assertions per review --------- Co-authored-by: Ricardo Cabral <me@ricardocabral.io>
1 parent 00240a9 commit 7fc0552

3 files changed

Lines changed: 603 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
Add unit tests for InvoiceRepository and UserRepository with sinon-stubbed DB client
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import * as chai from 'chai'
2+
import * as sinon from 'sinon'
3+
import knex from 'knex'
4+
import sinonChai from 'sinon-chai'
5+
import chaiAsPromised from 'chai-as-promised'
6+
7+
import { DatabaseClient } from '../../../src/@types/base'
8+
import { Invoice, InvoiceStatus, InvoiceUnit } from '../../../src/@types/invoice'
9+
import { IInvoiceRepository } from '../../../src/@types/repositories'
10+
import { InvoiceRepository } from '../../../src/repositories/invoice-repository'
11+
12+
chai.use(sinonChai)
13+
chai.use(chaiAsPromised)
14+
15+
const { expect } = chai
16+
17+
describe('InvoiceRepository', () => {
18+
let repository: IInvoiceRepository
19+
let sandbox: sinon.SinonSandbox
20+
let dbClient: DatabaseClient
21+
22+
const pubkeyHex = '22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793'
23+
const fixedDate = new Date('2026-01-01T00:00:00.000Z')
24+
25+
const testInvoice: Invoice = {
26+
id: 'test-invoice-id',
27+
pubkey: pubkeyHex,
28+
bolt11: 'lnbc10n1pjqtest',
29+
amountRequested: 1000n,
30+
unit: InvoiceUnit.MSATS,
31+
status: InvoiceStatus.PENDING,
32+
description: 'test invoice',
33+
expiresAt: fixedDate,
34+
updatedAt: fixedDate,
35+
createdAt: fixedDate,
36+
verifyURL: 'https://example.com/verify',
37+
}
38+
39+
const dbInvoiceRow = {
40+
id: 'test-invoice-id',
41+
pubkey: Buffer.from(pubkeyHex, 'hex'),
42+
bolt11: 'lnbc10n1pjqtest',
43+
amount_requested: '1000',
44+
amount_paid: null,
45+
unit: InvoiceUnit.MSATS,
46+
status: InvoiceStatus.PENDING,
47+
description: 'test invoice',
48+
confirmed_at: null,
49+
expires_at: fixedDate,
50+
updated_at: fixedDate,
51+
created_at: fixedDate,
52+
verify_url: 'https://example.com/verify',
53+
}
54+
55+
beforeEach(() => {
56+
sandbox = sinon.createSandbox()
57+
sandbox.useFakeTimers(fixedDate.getTime())
58+
dbClient = knex({ client: 'pg' })
59+
repository = new InvoiceRepository(dbClient)
60+
})
61+
62+
afterEach(() => {
63+
dbClient.destroy()
64+
sandbox.restore()
65+
})
66+
67+
describe('.confirmInvoice', () => {
68+
it('calls raw with confirm_invoice stored procedure and correct arguments', async () => {
69+
const rawStub = sandbox.stub().resolves()
70+
const client = { raw: rawStub } as unknown as DatabaseClient
71+
72+
await repository.confirmInvoice('invoice-123', 5000n, fixedDate, client)
73+
74+
expect(rawStub).to.have.been.calledOnceWithExactly(
75+
'select confirm_invoice(?, ?, ?)',
76+
['invoice-123', '5000', fixedDate.toISOString()],
77+
)
78+
})
79+
80+
it('re-throws when raw call rejects', async () => {
81+
const dbError = new Error('connection refused')
82+
const client = { raw: sandbox.stub().rejects(dbError) } as unknown as DatabaseClient
83+
84+
await expect(
85+
repository.confirmInvoice('invoice-123', 5000n, fixedDate, client),
86+
).to.be.rejectedWith(dbError)
87+
})
88+
})
89+
90+
describe('.findById', () => {
91+
it('returns undefined when no invoice is found', async () => {
92+
const selectStub = sandbox.stub().resolves([])
93+
const client = sandbox.stub().returns({
94+
where: sandbox.stub().returns({ select: selectStub }),
95+
}) as unknown as DatabaseClient
96+
97+
const result = await repository.findById('nonexistent-id', client)
98+
99+
expect(result).to.be.undefined
100+
})
101+
102+
it('returns a transformed Invoice when found', async () => {
103+
const selectStub = sandbox.stub().resolves([dbInvoiceRow])
104+
const client = sandbox.stub().returns({
105+
where: sandbox.stub().returns({ select: selectStub }),
106+
}) as unknown as DatabaseClient
107+
108+
const result = await repository.findById('test-invoice-id', client)
109+
110+
expect(result).to.not.be.undefined
111+
expect(result!.id).to.equal('test-invoice-id')
112+
expect(result!.pubkey).to.equal(pubkeyHex)
113+
expect(result!.status).to.equal(InvoiceStatus.PENDING)
114+
expect(result!.amountRequested).to.equal(1000n)
115+
})
116+
117+
it('queries invoices table by id', async () => {
118+
const whereStub = sandbox.stub().returns({ select: sandbox.stub().resolves([]) })
119+
const client = sandbox.stub().returns({ where: whereStub }) as unknown as DatabaseClient
120+
121+
await repository.findById('some-id', client)
122+
123+
expect(client).to.have.been.calledWith('invoices')
124+
expect(whereStub).to.have.been.calledWith('id', 'some-id')
125+
})
126+
})
127+
128+
describe('.findPendingInvoices', () => {
129+
function makePendingClient(results: any[]): DatabaseClient {
130+
const selectStub = sandbox.stub().resolves(results)
131+
const limitStub = sandbox.stub().returns({ select: selectStub })
132+
const offsetStub = sandbox.stub().returns({ limit: limitStub })
133+
const orderByStub = sandbox.stub().returns({ offset: offsetStub })
134+
const whereStub = sandbox.stub().returns({ orderBy: orderByStub })
135+
return sandbox.stub().returns({ where: whereStub }) as unknown as DatabaseClient
136+
}
137+
138+
it('returns empty array when no pending invoices exist', async () => {
139+
const result = await repository.findPendingInvoices(0, 10, makePendingClient([]))
140+
141+
expect(result).to.deep.equal([])
142+
})
143+
144+
it('returns transformed invoices when pending invoices are found', async () => {
145+
const result = await repository.findPendingInvoices(0, 10, makePendingClient([dbInvoiceRow]))
146+
147+
expect(result).to.have.length(1)
148+
expect(result[0].id).to.equal('test-invoice-id')
149+
expect(result[0].amountRequested).to.equal(1000n)
150+
})
151+
152+
it('passes offset and limit to the query', async () => {
153+
const selectStub = sandbox.stub().resolves([])
154+
const limitStub = sandbox.stub().returns({ select: selectStub })
155+
const offsetStub = sandbox.stub().returns({ limit: limitStub })
156+
const orderByStub = sandbox.stub().returns({ offset: offsetStub })
157+
const whereStub = sandbox.stub().returns({ orderBy: orderByStub })
158+
const client = sandbox.stub().returns({ where: whereStub }) as unknown as DatabaseClient
159+
160+
await repository.findPendingInvoices(5, 20, client)
161+
162+
expect(offsetStub).to.have.been.calledWith(5)
163+
expect(limitStub).to.have.been.calledWith(20)
164+
})
165+
166+
it('orders by created_at ascending', async () => {
167+
const selectStub = sandbox.stub().resolves([])
168+
const limitStub = sandbox.stub().returns({ select: selectStub })
169+
const offsetStub = sandbox.stub().returns({ limit: limitStub })
170+
const orderByStub = sandbox.stub().returns({ offset: offsetStub })
171+
const whereStub = sandbox.stub().returns({ orderBy: orderByStub })
172+
const client = sandbox.stub().returns({ where: whereStub }) as unknown as DatabaseClient
173+
174+
await repository.findPendingInvoices(0, 10, client)
175+
176+
expect(orderByStub).to.have.been.calledWith('created_at', 'asc')
177+
})
178+
179+
it('filters by pending status', async () => {
180+
const orderByStub = sandbox.stub().returns({
181+
offset: sandbox.stub().returns({ limit: sandbox.stub().returns({ select: sandbox.stub().resolves([]) }) }),
182+
})
183+
const whereStub = sandbox.stub().returns({ orderBy: orderByStub })
184+
const client = sandbox.stub().returns({ where: whereStub }) as unknown as DatabaseClient
185+
186+
await repository.findPendingInvoices(0, 10, client)
187+
188+
expect(whereStub).to.have.been.calledWith('status', InvoiceStatus.PENDING)
189+
})
190+
})
191+
192+
describe('.updateStatus', () => {
193+
it('returns an object with then, catch, and toString', () => {
194+
const result = repository.updateStatus(testInvoice)
195+
196+
expect(result).to.have.property('then').that.is.a('function')
197+
expect(result).to.have.property('catch').that.is.a('function')
198+
expect(result).to.have.property('toString').that.is.a('function')
199+
})
200+
201+
it('generates UPDATE SQL targeting the invoices table', () => {
202+
const sql = repository.updateStatus(testInvoice).toString()
203+
204+
expect(sql).to.include('update "invoices"')
205+
expect(sql).to.include('"status"')
206+
expect(sql).to.include('"updated_at"')
207+
})
208+
209+
it('includes the invoice id in the WHERE clause', () => {
210+
const sql = repository.updateStatus(testInvoice).toString()
211+
212+
expect(sql).to.include('"id"')
213+
expect(sql).to.include('test-invoice-id')
214+
})
215+
216+
it('includes RETURNING * clause', () => {
217+
const sql = repository.updateStatus(testInvoice).toString()
218+
219+
expect(sql).to.include('returning')
220+
})
221+
})
222+
223+
describe('.upsert', () => {
224+
it('returns an object with then, catch, and toString', () => {
225+
const result = repository.upsert(testInvoice)
226+
227+
expect(result).to.have.property('then').that.is.a('function')
228+
expect(result).to.have.property('catch').that.is.a('function')
229+
expect(result).to.have.property('toString').that.is.a('function')
230+
})
231+
232+
it('generates INSERT with on-conflict do update set SQL', () => {
233+
const sql = repository.upsert(testInvoice).toString()
234+
235+
expect(sql).to.include('insert into "invoices"')
236+
expect(sql).to.include('on conflict')
237+
expect(sql).to.include('do update set')
238+
})
239+
240+
it('includes the invoice id when one is provided', () => {
241+
const sql = repository.upsert({ ...testInvoice, id: 'specific-id' }).toString()
242+
243+
expect(sql).to.include('specific-id')
244+
})
245+
246+
it('uses a generated UUID when no id is provided', () => {
247+
const { id: _id, ...invoiceWithoutId } = testInvoice
248+
const sql = repository.upsert(invoiceWithoutId as Invoice).toString()
249+
250+
expect(sql).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
251+
})
252+
253+
it('encodes pubkey as hex buffer in SQL', () => {
254+
const sql = repository.upsert(testInvoice).toString()
255+
256+
expect(sql).to.include(`X'${pubkeyHex}'`)
257+
})
258+
259+
it('includes all required invoice fields', () => {
260+
const sql = repository.upsert(testInvoice).toString()
261+
262+
expect(sql).to.include('"bolt11"')
263+
expect(sql).to.include('"status"')
264+
expect(sql).to.include('"unit"')
265+
expect(sql).to.include('"description"')
266+
})
267+
})
268+
})

0 commit comments

Comments
 (0)