From 108c0328963f0243a5f7872cd435eccbd464ef40 Mon Sep 17 00:00:00 2001 From: JCW Date: Wed, 19 Aug 2026 11:16:45 +0100 Subject: [PATCH] Improve test coverage --- src/test/app/lending/LendingHelpers_test.cpp | 103 +++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/test/app/lending/LendingHelpers_test.cpp b/src/test/app/lending/LendingHelpers_test.cpp index 909b617980..ac6d17fc55 100644 --- a/src/test/app/lending/LendingHelpers_test.cpp +++ b/src/test/app/lending/LendingHelpers_test.cpp @@ -3,18 +3,32 @@ #include #include #include +#include +#include #include #include +#include +#include +#include #include +#include +#include #include +#include +#include #include #include #include #include #include +#include +#include #include +#include #include +#include +#include #include #include @@ -1871,6 +1885,93 @@ public: } } + // Covers the accountSendMulti failure branch of disburseLoan + // (the final `return ter;` in LendingHelpers.cpp). In production this + // line is unreachable: LoanSet::preclaim verifies + // Vault.AssetsAvailable >= principalRequested, and ValidVault keeps + // AssetsAvailable in sync with the vault pseudo-account's actual XRP + // holding. To reach it we drive the helper directly from a synthetic + // ApplyContext (same pattern as LoanBroker_test's + // testLoanBrokerCoverDepositNullVault), drain the pseudo-account's + // sfBalance on the scratch view, and observe disburseLoan surface the + // tec that accountSendMultiIOU returns for a native-asset transfer + // whose sender balance is insufficient. Bypassing LoanSet's own + // preclaim/doApply means the AssetsAvailable guard is skipped; the + // mutation lives on a cloned OpenView, so nothing commits back to the + // real ledger and no invariant fires. + void + testDisburseLoanTransferFailure() + { + testcase("disburseLoan: accountSendMulti failure surfaces the tec"); + + using namespace jtx; + + Env env{*this}; + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + // Standard XRP vault owned by the lender, with a deposit that + // funds the pseudo-account so the drain below is meaningful. + PrettyAsset const asset{xrpIssue(), 1}; + Vault const vault{env}; + auto const [createTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); + env(createTx); + env.close(); + env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(100'000)})); + env.close(); + + auto const vaultSle0 = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle0)) + return; + AccountID const vaultPseudo = vaultSle0->at(sfAccount); + Asset const vaultAsset = vaultSle0->at(sfAsset); + + // Dummy STTx: disburseLoan does not inspect tx fields, but + // ApplyContext requires an STTx. Use a Payment (arbitrary type) + // signed by the lender so the account field is well-formed. + STTx const tx{ttPAYMENT, [&](STObject& obj) { obj.setAccountID(sfAccount, lender.id()); }}; + + // Clone the current ledger into a writable ApplyContext. + OpenView ov{*env.current()}; + test::StreamSink sink{beast::Severity::Warning}; + beast::Journal const jlog{sink}; + ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog}; + + auto borrowerSle = ac.view().peek(keylet::account(borrower.id())); + auto brokerOwnerSle = ac.view().peek(keylet::account(lender.id())); + auto pseudoSle = ac.view().peek(keylet::account(vaultPseudo)); + if (!BEAST_EXPECT(borrowerSle && brokerOwnerSle && pseudoSle)) + return; + + // Drain the vault pseudo-account so accountSendMultiIOU's native + // branch (sfBalance < takeFromSender) returns tecFAILED_PROCESSING. + pseudoSle->setFieldAmount(sfBalance, STAmount(XRPAmount(0))); + ac.view().update(pseudoSle); + + // originationFee > 0 also exercises the second addEmptyHolding leg + // (broker owner side). Both are no-ops for native XRP. + Number const originationFee{5'000}; + Number const toBorrower{95'000}; + + auto viewContext = ac.getApplyViewContext(); + TER const result = disburseLoan( + viewContext, + borrowerSle, + brokerOwnerSle, + vaultPseudo, + vaultAsset, + toBorrower, + originationFee, + borrower.id(), + lender.id(), + jlog); + + BEAST_EXPECT(result == TER{tecFAILED_PROCESSING}); + } + void run() override { @@ -1906,6 +2007,8 @@ public: testLoanOriginationExceedsVaultMaximumDispatcher(); testLoanVaultExposureDispatcher(); testLoanPaymentDeltasDispatcher(); + + testDisburseLoanTransferFailure(); } };