lightning-terminal/accounts/store_test.go
cyberguru1 009a5c28bc
accounts: add payment methods to Store interface
Introduce the ListAccountPayments and CountAccountPayments methods to
the accounts Store interface.

ListAccountPayments enables retrieval of a paginated list of payment
entries associated with a given account ID, supporting offset and
limit. CountAccountPayments returns the total number of payments
associated with the account.
2026-07-22 14:06:25 -05:00

982 lines
28 KiB
Go

package accounts
import (
"context"
"sync"
"testing"
"time"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// TestAccountStore tests that accounts can be stored and retrieved correctly.
func TestAccountStore(t *testing.T) {
t.Parallel()
ctx := context.Background()
clock := clock.NewTestClock(time.Now())
store := NewTestDB(t, clock)
// Create an account that does not expire.
acct1, err := store.NewAccount(ctx, 0, time.Time{}, "foo")
require.NoError(t, err)
require.False(t, acct1.HasExpired())
dbAccount, err := store.Account(ctx, acct1.ID)
require.NoError(t, err)
assertEqualAccounts(t, acct1, dbAccount)
// Make sure we cannot create a second account with the same label.
_, err = store.NewAccount(ctx, 123, time.Time{}, "foo")
require.ErrorIs(t, err, ErrLabelAlreadyExists)
// Make sure we cannot set a label that looks like an account ID.
_, err = store.NewAccount(ctx, 123, time.Time{}, "0011223344556677")
require.ErrorContains(t, err, "is not allowed as it can be mistaken")
// Make sure we can create an account with an empty label.
acctEmpty, err := store.NewAccount(ctx, 0, time.Time{}, "")
require.NoError(t, err)
require.Empty(t, acctEmpty.Label)
now := clock.Now()
// Update all values of the account that we can modify.
//
// Update the balance and expiry.
err = store.UpdateAccount(
ctx, acct1.ID, fn.Some(int64(-500)), fn.Some(now),
fn.None[string](),
)
require.NoError(t, err)
// Add 2 payments.
_, err = store.UpsertAccountPayment(
ctx, acct1.ID, lntypes.Hash{12, 34, 56, 78}, 123456,
lnrpc.Payment_FAILED,
)
require.NoError(t, err)
_, err = store.UpsertAccountPayment(
ctx, acct1.ID, lntypes.Hash{34, 56, 78, 90}, 789456123789,
lnrpc.Payment_SUCCEEDED,
)
require.NoError(t, err)
// Add 2 invoices.
err = store.AddAccountInvoice(
ctx, acct1.ID, lntypes.Hash{12, 34, 56, 78},
)
require.NoError(t, err)
err = store.AddAccountInvoice(
ctx, acct1.ID, lntypes.Hash{34, 56, 78, 90},
)
require.NoError(t, err)
// Adjust the account balance by first crediting 10000, and then
// debiting 5000.
err = store.CreditAccount(ctx, acct1.ID, lnwire.MilliSatoshi(10000))
require.NoError(t, err)
err = store.DebitAccount(ctx, acct1.ID, lnwire.MilliSatoshi(5000))
require.NoError(t, err)
// Update the in-memory account so that we can compare it with the
// account we get from the store.
acct1.CurrentBalance = -500
acct1.ExpirationDate = clock.Now()
acct1.Payments[lntypes.Hash{12, 34, 56, 78}] = &PaymentEntry{
Status: lnrpc.Payment_FAILED,
FullAmount: 123456,
}
acct1.Payments[lntypes.Hash{34, 56, 78, 90}] = &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 789456123789,
}
acct1.Invoices[lntypes.Hash{12, 34, 56, 78}] = struct{}{}
acct1.Invoices[lntypes.Hash{34, 56, 78, 90}] = struct{}{}
acct1.CurrentBalance += 10000
acct1.CurrentBalance -= 5000
dbAccount, err = store.Account(ctx, acct1.ID)
require.NoError(t, err)
assertEqualAccounts(t, acct1, dbAccount)
// Test that adjusting the balance to exactly 0 should work, while
// adjusting the balance to below 0 should fail.
err = store.DebitAccount(
ctx, acct1.ID, lnwire.MilliSatoshi(acct1.CurrentBalance),
)
require.NoError(t, err)
acct1.CurrentBalance = 0
dbAccount, err = store.Account(ctx, acct1.ID)
require.NoError(t, err)
assertEqualAccounts(t, acct1, dbAccount)
// Adjusting the value to below 0 should fail.
err = store.DebitAccount(ctx, acct1.ID, lnwire.MilliSatoshi(1))
require.ErrorContains(t, err, "balance would be below 0")
// Sleep just a tiny bit to make sure we are never too quick to measure
// the expiry, even though the time is nanosecond scale and writing to
// the store and reading again should take at least a couple of
// microseconds.
time.Sleep(5 * time.Millisecond)
require.True(t, acct1.HasExpired())
// Test listing and deleting accounts.
accounts, err := store.Accounts(ctx)
require.NoError(t, err)
require.Len(t, accounts, 2)
err = store.RemoveAccount(ctx, acct1.ID)
require.NoError(t, err)
accounts, err = store.Accounts(ctx)
require.NoError(t, err)
require.Len(t, accounts, 1)
_, err = store.Account(ctx, acct1.ID)
require.ErrorIs(t, err, ErrAccNotFound)
}
// TestAccountStoreAccountsGrouping verifies that Accounts groups invoices and
// payments by account ID and does not leak linked data across accounts.
func TestAccountStoreAccountsGrouping(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := NewTestDB(t, clock.NewTestClock(time.Now()))
accountA, err := store.NewAccount(ctx, 1_000, time.Time{}, "group-a")
require.NoError(t, err)
accountB, err := store.NewAccount(ctx, 2_000, time.Time{}, "group-b")
require.NoError(t, err)
invoiceA := lntypes.Hash{0x01, 0x02, 0x03, 0x04}
invoiceB := lntypes.Hash{0x0a, 0x0b, 0x0c, 0x0d}
err = store.AddAccountInvoice(ctx, accountA.ID, invoiceA)
require.NoError(t, err)
err = store.AddAccountInvoice(ctx, accountB.ID, invoiceB)
require.NoError(t, err)
paymentA := lntypes.Hash{0x11, 0x12, 0x13, 0x14}
paymentB := lntypes.Hash{0x1a, 0x1b, 0x1c, 0x1d}
_, err = store.UpsertAccountPayment(
ctx, accountA.ID, paymentA, lnwire.MilliSatoshi(1234),
lnrpc.Payment_IN_FLIGHT,
)
require.NoError(t, err)
_, err = store.UpsertAccountPayment(
ctx, accountB.ID, paymentB, lnwire.MilliSatoshi(5678),
lnrpc.Payment_SUCCEEDED,
)
require.NoError(t, err)
accounts, err := store.Accounts(ctx)
require.NoError(t, err)
require.Len(t, accounts, 2)
accountsByID := make(
map[AccountID]*OffChainBalanceAccount, len(accounts),
)
for _, account := range accounts {
accountsByID[account.ID] = account
}
accountAFromList, ok := accountsByID[accountA.ID]
require.True(t, ok)
accountBFromList, ok := accountsByID[accountB.ID]
require.True(t, ok)
require.Contains(t, accountAFromList.Invoices, invoiceA)
require.NotContains(t, accountAFromList.Invoices, invoiceB)
require.Contains(t, accountAFromList.Payments, paymentA)
require.NotContains(t, accountAFromList.Payments, paymentB)
require.Equal(
t, lnrpc.Payment_IN_FLIGHT,
accountAFromList.Payments[paymentA].Status,
)
require.Contains(t, accountBFromList.Invoices, invoiceB)
require.NotContains(t, accountBFromList.Invoices, invoiceA)
require.Contains(t, accountBFromList.Payments, paymentB)
require.NotContains(t, accountBFromList.Payments, paymentA)
require.Equal(
t, lnrpc.Payment_SUCCEEDED,
accountBFromList.Payments[paymentB].Status,
)
}
// assertEqualAccounts asserts that two accounts are equal. This helper function
// is needed because an account contains two time.Time values that cannot be
// compared using reflect.DeepEqual().
func assertEqualAccounts(t *testing.T, expected,
actual *OffChainBalanceAccount) {
expectedExpiry := expected.ExpirationDate
actualExpiry := actual.ExpirationDate
expectedUpdate := expected.LastUpdate
actualUpdate := actual.LastUpdate
expected.ExpirationDate = time.Time{}
expected.LastUpdate = time.Time{}
actual.ExpirationDate = time.Time{}
actual.LastUpdate = time.Time{}
require.Equal(t, expected, actual)
require.Equal(t, expectedExpiry.Unix(), actualExpiry.Unix())
require.Equal(t, expectedUpdate.Unix(), actualUpdate.Unix())
// Restore the old values to not influence the tests.
expected.ExpirationDate = expectedExpiry
expected.LastUpdate = expectedUpdate
actual.ExpirationDate = actualExpiry
actual.LastUpdate = actualUpdate
}
// TestAccountReadConcurrentWrite verifies that Account() returns correct data
// when the database is under concurrent write pressure. This is a regression
// test for a use-after-free on bbolt's mmap: bucket.Get() returns a slice
// pointing into the mmap'd file, and if that slice escapes the read
// transaction, a concurrent write can trigger a remap that invalidates the
// pointer — causing a segfault or silent data corruption.
func TestAccountReadConcurrentWrite(t *testing.T) {
t.Parallel()
ctx := context.Background()
clk := clock.NewTestClock(time.Now())
store := NewTestDB(t, clk)
// Create the account we'll keep reading throughout the test.
targetAcct, err := store.NewAccount(
ctx, 42_000, time.Time{}, "target",
)
require.NoError(t, err)
// Also store initial indexes so LastIndexes reads have data.
err = store.StoreLastIndexes(ctx, 100, 200)
require.NoError(t, err)
// Number of concurrent writers. Each creates accounts in a tight
// loop, growing the database and forcing bbolt to remap.
const (
numWriters = 4
numWrites = 50
numReaders = 4
numReads = 200
)
var wg sync.WaitGroup
// Spawn writers that grow the database.
for w := 0; w < numWriters; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < numWrites; i++ {
_, wErr := store.NewAccount(
ctx,
lnwire.MilliSatoshi(1_000_000+i),
time.Time{},
"",
)
require.NoError(t, wErr)
// Also update indexes to exercise
// LastIndexes' code path.
wErr = store.StoreLastIndexes(
ctx,
uint64(100+i),
uint64(200+i),
)
require.NoError(t, wErr)
}
}()
}
// Spawn readers that continuously read the target account and
// the last indexes while writers are growing the DB.
for r := 0; r < numReaders; r++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < numReads; i++ {
// Read single account (the crash site).
acct, rErr := store.Account(
ctx, targetAcct.ID,
)
require.NoError(t, rErr)
// Verify the data is not corrupted.
require.Equal(t,
lnwire.MilliSatoshi(42_000),
acct.InitialBalance,
"corrupted balance",
)
require.Equal(t,
"target", acct.Label,
"corrupted label",
)
// Read last indexes.
add, settle, rErr := store.LastIndexes(ctx)
require.NoError(t, rErr)
require.GreaterOrEqual(t, add,
uint64(100),
"corrupted add index",
)
require.GreaterOrEqual(t, settle,
uint64(200),
"corrupted settle index",
)
}
}()
}
wg.Wait()
}
// TestAccountUpdateMethods tests that all the Store methods that update an
// account work correctly.
func TestAccountUpdateMethods(t *testing.T) {
t.Parallel()
ctx := context.Background()
t.Run("UpdateAccount", func(t *testing.T) {
clock := clock.NewTestClock(time.Now())
store := NewTestDB(t, clock)
// Ensure that the function errors out if we try update an
// account that does not exist.
err := store.UpdateAccount(
ctx, AccountID{}, fn.None[int64](),
fn.None[time.Time](), fn.None[string](),
)
require.ErrorIs(t, err, ErrAccNotFound)
acct, err := store.NewAccount(ctx, 0, time.Time{}, "foo")
require.NoError(t, err)
assertBalanceAndExpiry := func(balance int64,
expiry time.Time) {
dbAcct, err := store.Account(ctx, acct.ID)
require.NoError(t, err)
require.EqualValues(t, balance, dbAcct.CurrentBalance)
require.WithinDuration(
t, expiry, dbAcct.ExpirationDate, time.Second,
)
}
// Get the account from the store and check to see what its
// initial balance and expiry fields are set to.
assertBalanceAndExpiry(0, time.Time{})
// Now, update just the balance of the account.
newBalance := int64(123)
err = store.UpdateAccount(
ctx, acct.ID, fn.Some(newBalance), fn.None[time.Time](),
fn.None[string](),
)
require.NoError(t, err)
assertBalanceAndExpiry(newBalance, time.Time{})
// Now update just the expiry of the account.
newExpiry := clock.Now().Add(time.Hour)
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.Some(newExpiry),
fn.None[string](),
)
require.NoError(t, err)
assertBalanceAndExpiry(newBalance, newExpiry)
// Update both the balance and expiry of the account.
newBalance = 456
newExpiry = clock.Now().Add(2 * time.Hour)
err = store.UpdateAccount(
ctx, acct.ID, fn.Some(newBalance), fn.Some(newExpiry),
fn.None[string](),
)
require.NoError(t, err)
assertBalanceAndExpiry(newBalance, newExpiry)
// Finally, test an update that has no net changes to the
// balance or expiry.
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.None[string](),
)
require.NoError(t, err)
assertBalanceAndExpiry(newBalance, newExpiry)
// Test renaming the account.
newLabel := "bar"
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some(newLabel),
)
require.NoError(t, err)
dbAcct, err := store.Account(ctx, acct.ID)
require.NoError(t, err)
require.Equal(t, newLabel, dbAcct.Label)
// Test updating an account with its existing label doesn't fail
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some(newLabel),
)
require.NoError(t, err)
require.Equal(t, newLabel, dbAcct.Label)
// Try to rename to an existing label.
_, err = store.NewAccount(ctx, 0, time.Time{}, "existing")
require.NoError(t, err)
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some("existing"),
)
require.ErrorIs(t, err, ErrLabelAlreadyExists)
// Test that passing an empty Some("") label works and clears
// the label.
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some(""),
)
require.NoError(t, err)
dbAcct, err = store.Account(ctx, acct.ID)
require.NoError(t, err)
require.Empty(t, dbAcct.Label)
// Test that passing a None label doesn't change anything.
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some("new-label"),
)
require.NoError(t, err)
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.None[string](),
)
require.NoError(t, err)
dbAcct, err = store.Account(ctx, acct.ID)
require.NoError(t, err)
require.Equal(t, "new-label", dbAcct.Label)
// Test that we cannot update to a label that looks like an
// account ID.
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some("0011223344556677"),
)
require.ErrorContains(t, err, "is not allowed"+
" as it can be mistaken")
// Test that a hex string with a different length is allowed.
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some("00112233445566"),
)
require.NoError(t, err)
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some("001122334455667788"),
)
require.NoError(t, err)
// Test that a non-hex string with the same length as an account
// ID is allowed.
err = store.UpdateAccount(
ctx, acct.ID, fn.None[int64](), fn.None[time.Time](),
fn.Some("G011223344556677"),
)
require.NoError(t, err)
})
t.Run("AddAccountInvoice", func(t *testing.T) {
store := NewTestDB(t, clock.NewTestClock(time.Now()))
acct, err := store.NewAccount(ctx, 0, time.Time{}, "foo")
require.NoError(t, err)
assertInvoices := func(invoices ...lntypes.Hash) {
dbAcct, err := store.Account(ctx, acct.ID)
require.NoError(t, err)
// First make sure the number of invoices match before
// de-duping the hashes.
require.Len(t, dbAcct.Invoices, len(invoices))
dbInvs := make([]lntypes.Hash, 0, len(dbAcct.Invoices))
for hash := range dbAcct.Invoices {
dbInvs = append(dbInvs, hash)
}
require.ElementsMatch(t, invoices, dbInvs)
}
// The account initially has no invoices.
assertInvoices()
// Adding an invoice to an account that doesnt exist yet should
// error out.
err = store.AddAccountInvoice(ctx, AccountID{}, lntypes.Hash{})
require.ErrorIs(t, err, ErrAccNotFound)
// Add an invoice to the account.
hash1 := lntypes.Hash{1, 2, 3, 4}
err = store.AddAccountInvoice(ctx, acct.ID, hash1)
require.NoError(t, err)
assertInvoices(hash1)
// Assert that adding the same invoice again does not change the
// state.
err = store.AddAccountInvoice(ctx, acct.ID, hash1)
require.NoError(t, err)
assertInvoices(hash1)
// Now add a second invoice.
hash2 := lntypes.Hash{5, 6, 7, 8}
err = store.AddAccountInvoice(ctx, acct.ID, hash2)
require.NoError(t, err)
assertInvoices(hash1, hash2)
})
t.Run("CreditAccount", func(t *testing.T) {
store := NewTestDB(t, clock.NewTestClock(time.Now()))
// Increasing the balance of an account that doesn't exist
// should error out.
err := store.CreditAccount(ctx, AccountID{}, 100)
require.ErrorIs(t, err, ErrAccNotFound)
acct, err := store.NewAccount(ctx, 123, time.Time{}, "foo")
require.NoError(t, err)
assertBalance := func(balance int64) {
dbAcct, err := store.Account(ctx, acct.ID)
require.NoError(t, err)
require.EqualValues(t, balance, dbAcct.CurrentBalance)
}
// The account initially has a balance of 123.
assertBalance(123)
// Increase the balance by 100 and assert that the new balance
// is 223.
err = store.CreditAccount(ctx, acct.ID, 100)
require.NoError(t, err)
assertBalance(223)
})
t.Run("Upsert and Delete AccountPayment", func(t *testing.T) {
store := NewTestDB(t, clock.NewTestClock(time.Now()))
acct, err := store.NewAccount(ctx, 1000, time.Time{}, "foo")
require.NoError(t, err)
assertBalanceAndPayments := func(balance int64,
payments AccountPayments) {
dbAcct, err := store.Account(ctx, acct.ID)
require.NoError(t, err)
require.EqualValues(t, balance, dbAcct.CurrentBalance)
require.Len(t, dbAcct.Payments, len(payments))
for hash, payment := range payments {
dbPayment, ok := dbAcct.Payments[hash]
require.True(t, ok)
require.Equal(t, payment, dbPayment)
}
}
// The account initially has a balance of 1000 and no payments.
assertBalanceAndPayments(1000, nil)
// Assert that calling the method for a non-existent account
// errors out.
_, err = store.UpsertAccountPayment(
ctx, AccountID{}, lntypes.Hash{}, 0,
lnrpc.Payment_UNKNOWN,
)
require.ErrorIs(t, err, ErrAccNotFound)
// Add a payment to the account but don't update the balance.
// We do add a WithErrIfAlreadyPending and
// WithErrIfAlreadySucceeded option. here just to show that no
// error is returned since the payment does not exist yet.
hash1 := lntypes.Hash{1, 2, 3, 4}
known, err := store.UpsertAccountPayment(
ctx, acct.ID, hash1, 600, lnrpc.Payment_UNKNOWN,
WithErrIfAlreadyPending(),
WithErrIfAlreadySucceeded(),
)
require.NoError(t, err)
require.False(t, known)
assertBalanceAndPayments(1000, AccountPayments{
hash1: &PaymentEntry{
Status: lnrpc.Payment_UNKNOWN,
FullAmount: 600,
},
})
// Add a second payment to the account and again don't update
// the balance.
hash2 := lntypes.Hash{5, 6, 7, 8}
known, err = store.UpsertAccountPayment(
ctx, acct.ID, hash2, 100, lnrpc.Payment_UNKNOWN,
)
require.NoError(t, err)
require.False(t, known)
assertBalanceAndPayments(1000, AccountPayments{
hash1: &PaymentEntry{
Status: lnrpc.Payment_UNKNOWN,
FullAmount: 600,
},
hash2: &PaymentEntry{
Status: lnrpc.Payment_UNKNOWN,
FullAmount: 100,
},
})
// Now, update the first payment to have a new status and this
// time, debit the account.
known, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 600, lnrpc.Payment_SUCCEEDED,
WithDebitAccount(),
)
require.NoError(t, err)
require.True(t, known)
// The account should now have a balance of 400 and the first
// payment should have a status of succeeded.
assertBalanceAndPayments(400, AccountPayments{
hash1: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 600,
},
hash2: &PaymentEntry{
Status: lnrpc.Payment_UNKNOWN,
FullAmount: 100,
},
})
// Calling the same method again with the same payment hash
// should have no effect by default.
known, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 600, lnrpc.Payment_SUCCEEDED,
)
require.NoError(t, err)
require.True(t, known)
assertBalanceAndPayments(400, AccountPayments{
hash1: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 600,
},
hash2: &PaymentEntry{
Status: lnrpc.Payment_UNKNOWN,
FullAmount: 100,
},
})
// But, if we use the WithErrIfAlreadyPending option, we should
// get an error since the payment already exists.
known, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 600, lnrpc.Payment_SUCCEEDED,
WithErrIfAlreadyPending(),
)
require.ErrorContains(t, err, "is already in flight")
require.True(t, known)
// Do the above call again but this time, use the
// WithErrIfAlreadySucceeded option. This should return the
// ErrAlreadySucceeded error since the payment has already
// succeeded.
known, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 600, lnrpc.Payment_SUCCEEDED,
WithErrIfAlreadySucceeded(),
)
require.ErrorIs(t, err, ErrAlreadySucceeded)
require.True(t, known)
// We now call the method again for hash 2 and update its status
// to SUCCEEDED. This time, we will use the WithPendingAmount
// option which means that whatever `fullAmount` is passed in
// should be ignored and the pending amount should be used
// instead.
known, err = store.UpsertAccountPayment(
ctx, acct.ID, hash2, 0, lnrpc.Payment_SUCCEEDED,
WithPendingAmount(),
)
require.NoError(t, err)
require.True(t, known)
assertBalanceAndPayments(400, AccountPayments{
hash1: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 600,
},
hash2: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 100,
},
})
// Delete the first payment and make sure it is removed from the
// account.
err = store.DeleteAccountPayment(ctx, acct.ID, hash1)
require.NoError(t, err)
assertBalanceAndPayments(400, AccountPayments{
hash2: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 100,
},
})
// Test that deleting a payment that does not exist returns an
// error.
err = store.DeleteAccountPayment(ctx, acct.ID, hash1)
require.ErrorIs(t, err, ErrPaymentNotAssociated)
// Try once more to insert a payment that is currently unknown
// but this time add the WithErrIfUnknown option. This should
// return the ErrPaymentNotAssociated error.
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 600, lnrpc.Payment_SUCCEEDED,
WithErrIfUnknown(),
)
require.ErrorIs(t, err, ErrPaymentNotAssociated)
// Show that using the two options WithErrIfUnknown and
// WithPendingAmount together will return the
// ErrPaymentNotAssociated and will not successfully update
// the status. We call this for hash1 since it is no longer
// known. We do this to simulate the behaviour of
// removePayment.
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 0, lnrpc.Payment_SUCCEEDED,
WithErrIfUnknown(),
WithPendingAmount(),
)
require.ErrorIs(t, err, ErrPaymentNotAssociated)
assertBalanceAndPayments(400, AccountPayments{
hash2: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 100,
},
})
// Now insert hash 1 again.
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 600, lnrpc.Payment_IN_FLIGHT,
)
require.NoError(t, err)
assertBalanceAndPayments(400, AccountPayments{
hash1: &PaymentEntry{
Status: lnrpc.Payment_IN_FLIGHT,
FullAmount: 600,
},
hash2: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 100,
},
})
// Once again call UpsertAccountPayment with both the
// WithErrIfUnknown and WithPendingAmount options. This time
// it should succeed since the payment is now known and so the
// status should be updated.
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 0, lnrpc.Payment_SUCCEEDED,
WithErrIfUnknown(),
WithPendingAmount(),
)
require.NoError(t, err)
assertBalanceAndPayments(400, AccountPayments{
hash1: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 600,
},
hash2: &PaymentEntry{
Status: lnrpc.Payment_SUCCEEDED,
FullAmount: 100,
},
})
})
}
// TestLastInvoiceIndexes makes sure the last known invoice indexes can be
// stored and retrieved correctly.
func TestLastInvoiceIndexes(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := NewTestDB(t, clock.NewTestClock(time.Now()))
_, _, err := store.LastIndexes(ctx)
require.ErrorIs(t, err, ErrNoInvoiceIndexKnown)
require.NoError(t, store.StoreLastIndexes(ctx, 7, 99))
add, settle, err := store.LastIndexes(ctx)
require.NoError(t, err)
require.EqualValues(t, 7, add)
require.EqualValues(t, 99, settle)
}
// TestCheckLabel ensures that only labels that could be mistaken for a hex
// encoded account ID are rejected, while all other labels (including the empty
// label) are accepted.
func TestCheckLabel(t *testing.T) {
t.Parallel()
tests := []struct {
name string
label string
expectErr bool
}{{
name: "empty label is allowed",
label: "",
}, {
name: "plain text label is allowed",
label: "my account",
}, {
name: "short hex label is allowed",
label: "00112233",
}, {
name: "non-hex label with account ID length is allowed",
// 16 characters long, matching an encoded account ID, but not
// valid hex.
label: "zzzzzzzzzzzzzzzz",
}, {
name: "lowercase hex label with account ID length is rejected",
// 16 characters of valid hex, exactly the length of an encoded
// account ID.
label: "0011223344556677",
expectErr: true,
}, {
name: "uppercase hex label with account ID length is rejected",
// hex.DecodeString also accepts uppercase digits, so this must
// be rejected as well.
label: "00112233445566AA",
expectErr: true,
}}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := checkLabel(tc.label)
if tc.expectErr {
require.ErrorContains(
t, err, "is not allowed as it can be "+
"mistaken",
)
return
}
require.NoError(t, err)
})
}
}
// TestListAccountPayments tests listing and counting payment entries associated
// with a given account.
func TestListAccountPayments(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := NewTestDB(t, clock.NewTestClock(time.Now()))
// Listing payments for non-existent account should fail.
_, err := store.ListAccountPayments(
ctx, AccountID{}, 0, 0,
)
require.ErrorIs(t, err, ErrAccNotFound)
acct, err := store.NewAccount(
ctx, 10000, time.Time{}, "payment-list",
)
require.NoError(t, err)
// Initially, there should be no payments.
payments, err := store.ListAccountPayments(
ctx, acct.ID, 0, 0,
)
require.NoError(t, err)
require.Empty(t, payments)
count, err := store.CountAccountPayments(ctx, acct.ID)
require.NoError(t, err)
require.Zero(t, count)
// Add 3 payments.
hash1 := lntypes.Hash{1}
hash2 := lntypes.Hash{2}
hash3 := lntypes.Hash{3}
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 100, lnrpc.Payment_IN_FLIGHT,
)
require.NoError(t, err)
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash2, 200, lnrpc.Payment_SUCCEEDED,
)
require.NoError(t, err)
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash3, 300, lnrpc.Payment_FAILED,
)
require.NoError(t, err)
// Test counting all payments.
count, err = store.CountAccountPayments(ctx, acct.ID)
require.NoError(t, err)
require.EqualValues(t, 3, count)
// List all payments in default order (ascending by hash).
payments, err = store.ListAccountPayments(
ctx, acct.ID, 0, 3,
)
require.NoError(t, err)
require.Len(t, payments, 3)
require.Equal(t, hash1, payments[0].Hash)
require.Equal(t, hash2, payments[1].Hash)
require.Equal(t, hash3, payments[2].Hash)
// Test offset and limit.
payments, err = store.ListAccountPayments(
ctx, acct.ID, 1, 1,
)
require.NoError(t, err)
require.Len(t, payments, 1)
require.Equal(t, hash2, payments[0].Hash)
}