paymentsdb: move db interface dependant tests to different file

This commit starts reusing test cases which are not dependant on
the kv db backend. So they can be later used with the native db
implementation as well.
This commit is contained in:
ziggie
2025-08-12 18:14:57 +02:00
parent 39b7417797
commit e22b898c1e
3 changed files with 998 additions and 972 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,12 @@
package paymentsdb
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"math"
"io"
"reflect"
"testing"
"time"
@@ -98,6 +100,97 @@ var (
}
)
// payment is a helper structure that holds basic information on a test payment,
// such as the payment id, the status and the total number of HTLCs attempted.
type payment struct {
id lntypes.Hash
status PaymentStatus
htlcs int
}
// createTestPayments registers payments depending on the provided statuses in
// the payments slice. Each payment will receive one failed HTLC and another
// HTLC depending on the final status of the payment provided.
func createTestPayments(t *testing.T, p DB, payments []*payment) {
attemptID := uint64(0)
for i := 0; i < len(payments); i++ {
info, attempt, preimg, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")
// Set the payment id accordingly in the payments slice.
payments[i].id = info.PaymentIdentifier
attempt.AttemptID = attemptID
attemptID++
// Init the payment.
err = p.InitPayment(info.PaymentIdentifier, info)
require.NoError(t, err, "unable to send htlc message")
// Register and fail the first attempt for all payments.
_, err = p.RegisterAttempt(info.PaymentIdentifier, attempt)
require.NoError(t, err, "unable to send htlc message")
htlcFailure := HTLCFailUnreadable
_, err = p.FailAttempt(
info.PaymentIdentifier, attempt.AttemptID,
&HTLCFailInfo{
Reason: htlcFailure,
},
)
require.NoError(t, err, "unable to fail htlc")
// Increase the HTLC counter in the payments slice for the
// failed attempt.
payments[i].htlcs++
// Depending on the test case, fail or succeed the next
// attempt.
attempt.AttemptID = attemptID
attemptID++
_, err = p.RegisterAttempt(info.PaymentIdentifier, attempt)
require.NoError(t, err, "unable to send htlc message")
switch payments[i].status {
// Fail the attempt and the payment overall.
case StatusFailed:
htlcFailure := HTLCFailUnreadable
_, err = p.FailAttempt(
info.PaymentIdentifier, attempt.AttemptID,
&HTLCFailInfo{
Reason: htlcFailure,
},
)
require.NoError(t, err, "unable to fail htlc")
failReason := FailureReasonNoRoute
_, err = p.Fail(info.PaymentIdentifier,
failReason)
require.NoError(t, err, "unable to fail payment hash")
// Settle the attempt
case StatusSucceeded:
_, err := p.SettleAttempt(
info.PaymentIdentifier, attempt.AttemptID,
&HTLCSettleInfo{
Preimage: preimg,
},
)
require.NoError(t, err, "no error should have been "+
"received from settling a htlc attempt")
// We leave the attempt in-flight by doing nothing.
case StatusInFlight:
}
// Increase the HTLC counter in the payments slice for any
// attempt above.
payments[i].htlcs++
}
}
// assertRouteEquals compares to routes for equality and returns an error if
// they are not equal.
func assertRouteEqual(a, b *route.Route) error {
@@ -109,398 +202,414 @@ func assertRouteEqual(a, b *route.Route) error {
return nil
}
// TestQueryPayments tests retrieval of payments with forwards and reversed
// queries.
func TestQueryPayments(t *testing.T) {
// Define table driven test for QueryPayments.
// Test payments have sequence indices [1, 3, 4, 5, 6, 7].
// Note that the payment with index 7 has the same payment hash as 6,
// and is stored in a nested bucket within payment 6 rather than being
// its own entry in the payments bucket. We do this to test retrieval
// of legacy payments.
tests := []struct {
name string
query Query
firstIndex uint64
lastIndex uint64
// assertPaymentInfo retrieves the payment referred to by hash and verifies the
// expected values.
func assertPaymentInfo(t *testing.T, p DB, hash lntypes.Hash,
c *PaymentCreationInfo, f *FailureReason,
a *htlcStatus) {
// expectedSeqNrs contains the set of sequence numbers we expect
// our query to return.
expectedSeqNrs []uint64
}{
{
name: "IndexOffset at the end of the payments range",
query: Query{
IndexOffset: 7,
MaxPayments: 7,
Reversed: false,
IncludeIncomplete: true,
},
firstIndex: 0,
lastIndex: 0,
expectedSeqNrs: nil,
},
{
name: "query in forwards order, start at beginning",
query: Query{
IndexOffset: 0,
MaxPayments: 2,
Reversed: false,
IncludeIncomplete: true,
},
firstIndex: 1,
lastIndex: 3,
expectedSeqNrs: []uint64{1, 3},
},
{
name: "query in forwards order, start at end, overflow",
query: Query{
IndexOffset: 6,
MaxPayments: 2,
Reversed: false,
IncludeIncomplete: true,
},
firstIndex: 7,
lastIndex: 7,
expectedSeqNrs: []uint64{7},
},
{
name: "start at offset index outside of payments",
query: Query{
IndexOffset: 20,
MaxPayments: 2,
Reversed: false,
IncludeIncomplete: true,
},
firstIndex: 0,
lastIndex: 0,
expectedSeqNrs: nil,
},
{
name: "overflow in forwards order",
query: Query{
IndexOffset: 4,
MaxPayments: math.MaxUint64,
Reversed: false,
IncludeIncomplete: true,
},
firstIndex: 5,
lastIndex: 7,
expectedSeqNrs: []uint64{5, 6, 7},
},
{
name: "start at offset index outside of payments, " +
"reversed order",
query: Query{
IndexOffset: 9,
MaxPayments: 2,
Reversed: true,
IncludeIncomplete: true,
},
firstIndex: 6,
lastIndex: 7,
expectedSeqNrs: []uint64{6, 7},
},
{
name: "query in reverse order, start at end",
query: Query{
IndexOffset: 0,
MaxPayments: 2,
Reversed: true,
IncludeIncomplete: true,
},
firstIndex: 6,
lastIndex: 7,
expectedSeqNrs: []uint64{6, 7},
},
{
name: "query in reverse order, starting in middle",
query: Query{
IndexOffset: 4,
MaxPayments: 2,
Reversed: true,
IncludeIncomplete: true,
},
firstIndex: 1,
lastIndex: 3,
expectedSeqNrs: []uint64{1, 3},
},
{
name: "query in reverse order, starting in middle, " +
"with underflow",
query: Query{
IndexOffset: 4,
MaxPayments: 5,
Reversed: true,
IncludeIncomplete: true,
},
firstIndex: 1,
lastIndex: 3,
expectedSeqNrs: []uint64{1, 3},
},
{
name: "all payments in reverse, order maintained",
query: Query{
IndexOffset: 0,
MaxPayments: 7,
Reversed: true,
IncludeIncomplete: true,
},
firstIndex: 1,
lastIndex: 7,
expectedSeqNrs: []uint64{1, 3, 4, 5, 6, 7},
},
{
name: "exclude incomplete payments",
query: Query{
IndexOffset: 0,
MaxPayments: 7,
Reversed: false,
IncludeIncomplete: false,
},
firstIndex: 7,
lastIndex: 7,
expectedSeqNrs: []uint64{7},
},
{
name: "query payments at index gap",
query: Query{
IndexOffset: 1,
MaxPayments: 7,
Reversed: false,
IncludeIncomplete: true,
},
firstIndex: 3,
lastIndex: 7,
expectedSeqNrs: []uint64{3, 4, 5, 6, 7},
},
{
name: "query payments reverse before index gap",
query: Query{
IndexOffset: 3,
MaxPayments: 7,
Reversed: true,
IncludeIncomplete: true,
},
firstIndex: 1,
lastIndex: 1,
expectedSeqNrs: []uint64{1},
},
{
name: "query payments reverse on index gap",
query: Query{
IndexOffset: 2,
MaxPayments: 7,
Reversed: true,
IncludeIncomplete: true,
},
firstIndex: 1,
lastIndex: 1,
expectedSeqNrs: []uint64{1},
},
{
name: "query payments forward on index gap",
query: Query{
IndexOffset: 2,
MaxPayments: 2,
Reversed: false,
IncludeIncomplete: true,
},
firstIndex: 3,
lastIndex: 4,
expectedSeqNrs: []uint64{3, 4},
},
{
name: "query in forwards order, with start creation " +
"time",
query: Query{
IndexOffset: 0,
MaxPayments: 2,
Reversed: false,
IncludeIncomplete: true,
CreationDateStart: 5,
},
firstIndex: 5,
lastIndex: 6,
expectedSeqNrs: []uint64{5, 6},
},
{
name: "query in forwards order, with start creation " +
"time at end, overflow",
query: Query{
IndexOffset: 0,
MaxPayments: 2,
Reversed: false,
IncludeIncomplete: true,
CreationDateStart: 7,
},
firstIndex: 7,
lastIndex: 7,
expectedSeqNrs: []uint64{7},
},
{
name: "query with start and end creation time",
query: Query{
IndexOffset: 9,
MaxPayments: math.MaxUint64,
Reversed: true,
IncludeIncomplete: true,
CreationDateStart: 3,
CreationDateEnd: 5,
},
firstIndex: 3,
lastIndex: 5,
expectedSeqNrs: []uint64{3, 4, 5},
},
t.Helper()
payment, err := p.FetchPayment(hash)
if err != nil {
t.Fatal(err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if !reflect.DeepEqual(payment.Info, c) {
t.Fatalf("PaymentCreationInfos don't match: %v vs %v",
spew.Sdump(payment.Info), spew.Sdump(c))
}
ctx := context.Background()
if f != nil {
if *payment.FailureReason != *f {
t.Fatal("unexpected failure reason")
}
} else {
if payment.FailureReason != nil {
t.Fatal("unexpected failure reason")
}
}
paymentDB := NewTestDB(t)
if a == nil {
if len(payment.HTLCs) > 0 {
t.Fatal("expected no htlcs")
}
// Initialize the payment database.
paymentDB, err := NewKVPaymentsDB(paymentDB.db)
require.NoError(t, err)
return
}
// Make a preliminary query to make sure it's ok to
// query when we have no payments.
resp, err := paymentDB.QueryPayments(ctx, tt.query)
require.NoError(t, err)
require.Len(t, resp.Payments, 0)
htlc := payment.HTLCs[a.AttemptID]
if err := assertRouteEqual(&htlc.Route, &a.Route); err != nil {
t.Fatal("routes do not match")
}
// Populate the database with a set of test payments.
// We create 6 original payments, deleting the payment
// at index 2 so that we cover the case where sequence
// numbers are missing. We also add a duplicate payment
// to the last payment added to test the legacy case
// where we have duplicates in the nested duplicates
// bucket.
nonDuplicatePayments := 6
if htlc.AttemptID != a.AttemptID {
t.Fatalf("unnexpected attempt ID %v, expected %v",
htlc.AttemptID, a.AttemptID)
}
for i := 0; i < nonDuplicatePayments; i++ {
// Generate a test payment.
info, _, preimg, err := genInfo(t)
if err != nil {
t.Fatalf("unable to create test "+
"payment: %v", err)
}
// Override creation time to allow for testing
// of CreationDateStart and CreationDateEnd.
info.CreationTime = time.Unix(int64(i+1), 0)
if a.failure != nil {
if htlc.Failure == nil {
t.Fatalf("expected HTLC to be failed")
}
// Create a new payment entry in the database.
err = paymentDB.InitPayment(
info.PaymentIdentifier, info,
)
require.NoError(t, err)
if htlc.Failure.Reason != *a.failure {
t.Fatalf("expected HTLC failure %v, had %v",
*a.failure, htlc.Failure.Reason)
}
} else if htlc.Failure != nil {
t.Fatalf("expected no HTLC failure")
}
// Immediately delete the payment with index 2.
if i == 1 {
pmt, err := paymentDB.FetchPayment(
info.PaymentIdentifier,
)
require.NoError(t, err)
deletePayment(
t, paymentDB.db,
info.PaymentIdentifier,
pmt.SequenceNum,
)
}
// If we are on the last payment entry, add a
// duplicate payment with sequence number equal
// to the parent payment + 1. Note that
// duplicate payments will always be succeeded.
if i == (nonDuplicatePayments - 1) {
pmt, err := paymentDB.FetchPayment(
info.PaymentIdentifier,
)
require.NoError(t, err)
appendDuplicatePayment(
t, paymentDB.db,
info.PaymentIdentifier,
pmt.SequenceNum+1,
preimg,
)
}
}
// Fetch all payments in the database.
allPayments, err := paymentDB.FetchPayments()
if err != nil {
t.Fatalf("payments could not be fetched from "+
"database: %v", err)
}
if len(allPayments) != 6 {
t.Fatalf("Number of payments received does "+
"not match expected one. Got %v, "+
"want %v.", len(allPayments), 6)
}
querySlice, err := paymentDB.QueryPayments(
ctx, tt.query,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tt.firstIndex != querySlice.FirstIndexOffset ||
tt.lastIndex != querySlice.LastIndexOffset {
t.Errorf("First or last index does not match "+
"expected index. Want (%d, %d), "+
"got (%d, %d).",
tt.firstIndex, tt.lastIndex,
querySlice.FirstIndexOffset,
querySlice.LastIndexOffset)
}
if len(querySlice.Payments) != len(tt.expectedSeqNrs) {
t.Errorf("expected: %v payments, got: %v",
len(tt.expectedSeqNrs),
len(querySlice.Payments))
}
for i, seqNr := range tt.expectedSeqNrs {
q := querySlice.Payments[i]
if seqNr != q.SequenceNum {
t.Errorf("sequence numbers do not "+
"match, got %v, want %v",
q.SequenceNum, seqNr)
}
}
})
if a.settle != nil {
if htlc.Settle.Preimage != *a.settle {
t.Fatalf("Preimages don't match: %x vs %x",
htlc.Settle.Preimage, a.settle)
}
} else if htlc.Settle != nil {
t.Fatal("expected no settle info")
}
}
// TestLazySessionKeyDeserialize tests that we can read htlc attempt session
// keys that were previously serialized as a private key as raw bytes.
func TestLazySessionKeyDeserialize(t *testing.T) {
var b bytes.Buffer
// assertPaymentStatus retrieves the status of the payment referred to by hash
// and compares it with the expected state.
func assertPaymentStatus(t *testing.T, p DB, hash lntypes.Hash,
expStatus PaymentStatus) {
// Serialize as a private key.
err := WriteElements(&b, priv)
require.NoError(t, err)
t.Helper()
// Deserialize into [btcec.PrivKeyBytesLen]byte.
attempt := HTLCAttemptInfo{}
err = ReadElements(&b, &attempt.sessionKey)
require.NoError(t, err)
require.Zero(t, b.Len())
payment, err := p.FetchPayment(hash)
if errors.Is(err, ErrPaymentNotInitiated) {
return
}
if err != nil {
t.Fatal(err)
}
sessionKey := attempt.SessionKey()
require.Equal(t, priv, sessionKey)
if payment.Status != expStatus {
t.Fatalf("payment status mismatch: expected %v, got %v",
expStatus, payment.Status)
}
}
// TestRegistrable checks the method `Registrable` behaves as expected for ALL
// possible payment statuses.
func TestRegistrable(t *testing.T) {
// assertPayments is a helper function that given a slice of payment and
// indices for the slice asserts that exactly the same payments in the
// slice for the provided indices exist when fetching payments from the
// database.
func assertPayments(t *testing.T, paymentDB DB, payments []*payment) {
t.Helper()
response, err := paymentDB.QueryPayments(
context.Background(), Query{
IndexOffset: 0,
MaxPayments: uint64(len(payments)),
IncludeIncomplete: true,
},
)
require.NoError(t, err, "could not fetch payments from db")
dbPayments := response.Payments
// Make sure that the number of fetched payments is the same
// as expected.
require.Len(
t, dbPayments, len(payments), "unexpected number of payments",
)
// Convert fetched payments of type MPPayment to our helper structure.
p := make([]*payment, len(dbPayments))
for i, dbPayment := range dbPayments {
p[i] = &payment{
id: dbPayment.Info.PaymentIdentifier,
status: dbPayment.Status,
htlcs: len(dbPayment.HTLCs),
}
}
// Check that each payment we want to assert exists in the database.
require.Equal(t, payments, p)
}
func genPreimage() ([32]byte, error) {
var preimage [32]byte
if _, err := io.ReadFull(rand.Reader, preimage[:]); err != nil {
return preimage, err
}
return preimage, nil
}
func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo,
lntypes.Preimage, error) {
preimage, err := genPreimage()
if err != nil {
return nil, nil, preimage, fmt.Errorf("unable to "+
"generate preimage: %v", err)
}
rhash := sha256.Sum256(preimage[:])
var hash lntypes.Hash
copy(hash[:], rhash[:])
attempt, err := NewHtlcAttempt(
0, priv, *testRoute.Copy(), time.Time{}, &hash,
)
require.NoError(t, err)
return &PaymentCreationInfo{
PaymentIdentifier: rhash,
Value: testRoute.ReceiverAmt(),
CreationTime: time.Unix(time.Now().Unix(), 0),
PaymentRequest: []byte("hola"),
}, &attempt.HTLCAttemptInfo, preimage, nil
}
func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) {
paymentDB := NewTestDB(
t, WithKeepFailedPaymentAttempts(keepFailedPaymentAttempts),
)
// Register three payments:
// All payments will have one failed HTLC attempt and one HTLC attempt
// according to its final status.
// 1. A payment with two failed attempts.
// 2. A payment with one failed and one in-flight attempt.
// 3. A payment with one failed and one settled attempt.
// Initiate payments, which is a slice of payment that is used as
// template to create the corresponding test payments in the database.
//
// Note: The payment id and number of htlc attempts of each payment will
// be added to this slice when creating the payments below.
// This allows the slice to be used directly for testing purposes.
payments := []*payment{
{status: StatusFailed},
{status: StatusInFlight},
{status: StatusSucceeded},
}
// Use helper function to register the test payments in the data and
// populate the data to the payments slice.
createTestPayments(t, paymentDB, payments)
// Check that all payments are there as we added them.
assertPayments(t, paymentDB, payments)
// Calling DeleteFailedAttempts on a failed payment should delete all
// HTLCs.
require.NoError(t, paymentDB.DeleteFailedAttempts(payments[0].id))
// Expect all HTLCs to be deleted if the config is set to delete them.
if !keepFailedPaymentAttempts {
payments[0].htlcs = 0
}
assertPayments(t, paymentDB, payments)
// Calling DeleteFailedAttempts on an in-flight payment should return
// an error.
//
// NOTE: In case the option keepFailedPaymentAttempts is set no delete
// operation are performed in general therefore we do NOT expect an
// error in this case.
if keepFailedPaymentAttempts {
require.NoError(
t, paymentDB.DeleteFailedAttempts(payments[1].id),
)
} else {
require.Error(t, paymentDB.DeleteFailedAttempts(payments[1].id))
}
// Since DeleteFailedAttempts returned an error, we should expect the
// payment to be unchanged.
assertPayments(t, paymentDB, payments)
// Cleaning up a successful payment should remove failed htlcs.
require.NoError(t, paymentDB.DeleteFailedAttempts(payments[2].id))
// Expect all HTLCs except for the settled one to be deleted if the
// config is set to delete them.
if !keepFailedPaymentAttempts {
payments[2].htlcs = 1
}
assertPayments(t, paymentDB, payments)
// NOTE: In case the option keepFailedPaymentAttempts is set no delete
// operation are performed in general therefore we do NOT expect an
// error in this case.
if keepFailedPaymentAttempts {
// DeleteFailedAttempts is ignored, even for non-existent
// payments, if the control tower is configured to keep failed
// HTLCs.
require.NoError(
t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash),
)
} else {
// Attempting to cleanup a non-existent payment returns an
// error.
require.Error(
t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash),
)
}
}
// TestKVPaymentsDBMPPRecordValidation tests MPP record validation.
func TestKVPaymentsDBMPPRecordValidation(t *testing.T) {
t.Parallel()
paymentDB := NewTestDB(t)
info, attempt, _, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")
// Init the payment.
err = paymentDB.InitPayment(info.PaymentIdentifier, info)
require.NoError(t, err, "unable to send htlc message")
// Create three unique attempts we'll use for the test, and
// register them with the payment control. We set each
// attempts's value to one third of the payment amount, and
// populate the MPP options.
shardAmt := info.Value / 3
attempt.Route.FinalHop().AmtToForward = shardAmt
attempt.Route.FinalHop().MPP = record.NewMPP(
info.Value, [32]byte{1},
)
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
require.NoError(t, err, "unable to send htlc message")
// Now try to register a non-MPP attempt, which should fail.
b := *attempt
b.AttemptID = 1
b.Route.FinalHop().MPP = nil
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
require.ErrorIs(t, err, ErrMPPayment)
// Try to register attempt one with a different payment address.
b.Route.FinalHop().MPP = record.NewMPP(
info.Value, [32]byte{2},
)
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
require.ErrorIs(t, err, ErrMPPPaymentAddrMismatch)
// Try registering one with a different total amount.
b.Route.FinalHop().MPP = record.NewMPP(
info.Value/2, [32]byte{1},
)
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
require.ErrorIs(t, err, ErrMPPTotalAmountMismatch)
// Create and init a new payment. This time we'll check that we cannot
// register an MPP attempt if we already registered a non-MPP one.
info, attempt, _, err = genInfo(t)
require.NoError(t, err, "unable to generate htlc message")
err = paymentDB.InitPayment(info.PaymentIdentifier, info)
require.NoError(t, err, "unable to send htlc message")
attempt.Route.FinalHop().MPP = nil
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
require.NoError(t, err, "unable to send htlc message")
// Attempt to register an MPP attempt, which should fail.
b = *attempt
b.AttemptID = 1
b.Route.FinalHop().MPP = record.NewMPP(
info.Value, [32]byte{1},
)
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
require.ErrorIs(t, err, ErrNonMPPayment)
}
// TestDeleteSinglePayment tests that DeletePayment correctly
// deletes information about a completed payment from the database.
func TestDeleteSinglePayment(t *testing.T) {
t.Parallel()
paymentDB := NewTestDB(t)
// Register four payments:
// All payments will have one failed HTLC attempt and one HTLC attempt
// according to its final status.
// 1. A payment with two failed attempts.
// 2. Another payment with two failed attempts.
// 3. A payment with one failed and one settled attempt.
// 4. A payment with one failed and one in-flight attempt.
// Initiate payments, which is a slice of payment that is used as
// template to create the corresponding test payments in the database.
//
// Note: The payment id and number of htlc attempts of each payment will
// be added to this slice when creating the payments below.
// This allows the slice to be used directly for testing purposes.
payments := []*payment{
{status: StatusFailed},
{status: StatusFailed},
{status: StatusSucceeded},
{status: StatusInFlight},
}
// Use helper function to register the test payments in the data and
// populate the data to the payments slice.
createTestPayments(t, paymentDB, payments)
// Check that all payments are there as we added them.
assertPayments(t, paymentDB, payments)
// Delete HTLC attempts for first payment only.
require.NoError(t, paymentDB.DeletePayment(payments[0].id, true))
// The first payment is the only altered one as its failed HTLC should
// have been removed but is still present as payment.
payments[0].htlcs = 0
assertPayments(t, paymentDB, payments)
// Delete the first payment completely.
require.NoError(t, paymentDB.DeletePayment(payments[0].id, false))
// The first payment should have been deleted.
assertPayments(t, paymentDB, payments[1:])
// Now delete the second payment completely.
require.NoError(t, paymentDB.DeletePayment(payments[1].id, false))
// The Second payment should have been deleted.
assertPayments(t, paymentDB, payments[2:])
// Delete failed HTLC attempts for the third payment.
require.NoError(t, paymentDB.DeletePayment(payments[2].id, true))
// Only the successful HTLC attempt should be left for the third
// payment.
payments[2].htlcs = 1
assertPayments(t, paymentDB, payments[2:])
// Now delete the third payment completely.
require.NoError(t, paymentDB.DeletePayment(payments[2].id, false))
// Only the last payment should be left.
assertPayments(t, paymentDB, payments[3:])
// Deleting HTLC attempts from InFlight payments should not work and an
// error returned.
require.Error(t, paymentDB.DeletePayment(payments[3].id, true))
// The payment is InFlight and therefore should not have been altered.
assertPayments(t, paymentDB, payments[3:])
// Finally deleting the InFlight payment should also not work and an
// error returned.
require.Error(t, paymentDB.DeletePayment(payments[3].id, false))
// The payment is InFlight and therefore should not have been altered.
assertPayments(t, paymentDB, payments[3:])
}
// TestPaymentRegistrable checks the method `Registrable` behaves as expected
// for ALL possible payment statuses.
func TestPaymentRegistrable(t *testing.T) {
t.Parallel()
testCases := []struct {
@@ -1058,3 +1167,98 @@ func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) {
_, _, err := generateSphinxPacket(emptyRoute, testHash[:], sessionKey)
require.ErrorIs(t, err, route.ErrNoRouteHopsProvided)
}
// TestKVPaymentsDBSuccessesWithoutInFlight tests that the payment control will
// disallow calls to Success when no payment is in flight.
func TestKVPaymentsDBSuccessesWithoutInFlight(t *testing.T) {
t.Parallel()
paymentDB := NewTestDB(t)
info, _, preimg, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")
// Attempt to complete the payment should fail.
_, err = paymentDB.SettleAttempt(
info.PaymentIdentifier, 0,
&HTLCSettleInfo{
Preimage: preimg,
},
)
require.ErrorIs(t, err, ErrPaymentNotInitiated)
}
// TestKVPaymentsDBFailsWithoutInFlight checks that a strict payment control
// will disallow calls to Fail when no payment is in flight.
func TestKVPaymentsDBFailsWithoutInFlight(t *testing.T) {
t.Parallel()
paymentDB := NewTestDB(t)
info, _, _, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")
// Calling Fail should return an error.
_, err = paymentDB.Fail(
info.PaymentIdentifier, FailureReasonNoRoute,
)
require.ErrorIs(t, err, ErrPaymentNotInitiated)
}
// TestKVPaymentsDBDeletePayments tests that DeletePayments correctly deletes
// information about completed payments from the database.
func TestKVPaymentsDBDeletePayments(t *testing.T) {
t.Parallel()
paymentDB := NewTestDB(t)
// Register three payments:
// 1. A payment with two failed attempts.
// 2. A payment with one failed and one settled attempt.
// 3. A payment with one failed and one in-flight attempt.
payments := []*payment{
{status: StatusFailed},
{status: StatusSucceeded},
{status: StatusInFlight},
}
// Use helper function to register the test payments in the data and
// populate the data to the payments slice.
createTestPayments(t, paymentDB, payments)
// Check that all payments are there as we added them.
assertPayments(t, paymentDB, payments)
// Delete HTLC attempts for failed payments only.
numPayments, err := paymentDB.DeletePayments(true, true)
require.NoError(t, err)
require.EqualValues(t, 0, numPayments)
// The failed payment is the only altered one.
payments[0].htlcs = 0
assertPayments(t, paymentDB, payments)
// Delete failed attempts for all payments.
numPayments, err = paymentDB.DeletePayments(false, true)
require.NoError(t, err)
require.EqualValues(t, 0, numPayments)
// The failed attempts should be deleted, except for the in-flight
// payment, that shouldn't be altered until it has completed.
payments[1].htlcs = 1
assertPayments(t, paymentDB, payments)
// Now delete all failed payments.
numPayments, err = paymentDB.DeletePayments(true, false)
require.NoError(t, err)
require.EqualValues(t, 1, numPayments)
assertPayments(t, paymentDB, payments[1:])
// Finally delete all completed payments.
numPayments, err = paymentDB.DeletePayments(false, false)
require.NoError(t, err)
require.EqualValues(t, 1, numPayments)
assertPayments(t, paymentDB, payments[2:])
}

View File

@@ -8,7 +8,24 @@ import (
)
// NewTestDB is a helper function that creates an BBolt database for testing.
func NewTestDB(t *testing.T, opts ...OptionModifier) *KVPaymentsDB {
func NewTestDB(t *testing.T, opts ...OptionModifier) DB {
backend, backendCleanup, err := kvdb.GetTestBackend(
t.TempDir(), "paymentsDB",
)
require.NoError(t, err)
t.Cleanup(backendCleanup)
paymentDB, err := NewKVPaymentsDB(backend, opts...)
require.NoError(t, err)
return paymentDB
}
// NewKVTestDB is a helper function that creates an BBolt database for testing
// and there is no need to convert the interface to the KVPaymentsDB because for
// some unit tests we still need access to the kvdb interface.
func NewKVTestDB(t *testing.T, opts ...OptionModifier) *KVPaymentsDB {
backend, backendCleanup, err := kvdb.GetTestBackend(
t.TempDir(), "kvPaymentDB",
)