fix(export): write the transaction CSV atomically and report failures
The transaction export streamed rows straight into an ofstream. A write that failed partway, on a full disk or removable media, left a truncated CSV on disk behind a success toast. The CSV formatting also lived inline in the dialog with no test coverage. - The CSV is now built in memory and written atomically (temp file plus rename), so a write failure is reported with an error toast and leaves no partial file. - Formatting moves into a reusable helper, and the timestamp conversions use the thread-safe localtime variants and tolerate a conversion failure rather than dereferencing a null result. Tests: field escaping neutralises formula-injection prefixes and quotes special characters, and the full CSV has the expected header and rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
#endif
|
||||
#include "data/exchange_info.h"
|
||||
#include "util/text_format.h"
|
||||
#include "data/transaction_csv.h"
|
||||
#include "chat/chat_crypto.h"
|
||||
#include "chat/chat_identity.h"
|
||||
#include "chat/chat_service.h"
|
||||
@@ -18129,6 +18130,20 @@ void testContactsTextHelpers() {
|
||||
EXPECT_EQ(dragonx::util::trimmedClipboardText(std::string(1024, 'a').c_str()).size(), (size_t)1024);
|
||||
}
|
||||
|
||||
void testTransactionCsv() {
|
||||
std::vector<dragonx::TransactionInfo> txs(2);
|
||||
txs[0].type = "receive"; txs[0].amount = 1.5; txs[0].address = "zs1x"; txs[0].txid = "ab"; txs[0].confirmations = 7;
|
||||
txs[0].timestamp = 1700000000; txs[0].memo = "=HYPERLINK(\"x\")";
|
||||
txs[1].type = "send"; txs[1].amount = -0.00000001; txs[1].memo = "a,b\"c";
|
||||
const std::string csv = dragonx::data::formatTransactionsCsv(txs);
|
||||
EXPECT_TRUE(csv.rfind("Date,Type,Amount,Address,TXID,Confirmations,Memo\n", 0) == 0);
|
||||
EXPECT_TRUE(csv.find(",1.50000000,zs1x,ab,7,\"'=HYPERLINK(\"\"x\"\")\"\n") != std::string::npos);
|
||||
EXPECT_TRUE(csv.find(",-0.00000001,") != std::string::npos);
|
||||
EXPECT_TRUE(csv.find("\"a,b\"\"c\"\n") != std::string::npos);
|
||||
EXPECT_EQ(dragonx::data::escapeCsvField("+1"), std::string("'+1"));
|
||||
EXPECT_EQ(dragonx::data::escapeCsvField("plain"), std::string("plain"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -18438,6 +18453,7 @@ int main()
|
||||
testWriteFileIfDifferent();
|
||||
testCaptureExtent();
|
||||
testResolveExchangeSelection();
|
||||
testTransactionCsv();
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " test assertion(s) failed\n";
|
||||
|
||||
Reference in New Issue
Block a user