From 077bacff0ec3fd096ade285a8f7a6b45019aa99e Mon Sep 17 00:00:00 2001 From: flourishbar Date: Thu, 30 Jul 2026 05:10:16 +0100 Subject: [PATCH] fix: verify batch invoice submission row limit matches contract enforcement --- __tests__/BatchInvoiceForm.test.tsx | 28 +++++++++++++++++++ src/components/BatchInvoiceForm.tsx | 1 + .../__tests__/BatchInvoiceForm.test.tsx | 22 +++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/__tests__/BatchInvoiceForm.test.tsx b/__tests__/BatchInvoiceForm.test.tsx index d0b0224..2b7ed66 100644 --- a/__tests__/BatchInvoiceForm.test.tsx +++ b/__tests__/BatchInvoiceForm.test.tsx @@ -182,4 +182,32 @@ GTEST123,1000.00,USDC,3.50,2024-12-31`; // Should show total amount in summary expect(screen.getByText('Total Amount')).toBeInTheDocument(); }); + + it('rejects/truncates 51-row CSV client-side with clear message matching 50-row contract limit', async () => { + render(); + + // Build a CSV with 51 rows + const header = 'payer,amount,token,discount_rate,due_date'; + const rows = Array.from({ length: 51 }, (_, i) => + `GTEST${i}EXAMPLE456DEF789GHI012JKL345MNO678PQR901STU234VWX567YZ,1000.00,USDC,3.50,2024-12-31` + ); + const csvContent = [header, ...rows].join('\n'); + + const file = new File([csvContent], 'test_51.csv', { type: 'text/csv' }); + const input = screen.getByLabelText('Click to upload CSV file').querySelector('input'); + + if (input) { + fireEvent.change(input, { target: { files: [file] } }); + } + + await waitFor(() => { + expect(mockAddToast).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'warning', + title: 'Rows Truncated', + message: 'Only the first 50 rows were loaded.', + }) + ); + }); + }); }); diff --git a/src/components/BatchInvoiceForm.tsx b/src/components/BatchInvoiceForm.tsx index 768701e..39d93ef 100644 --- a/src/components/BatchInvoiceForm.tsx +++ b/src/components/BatchInvoiceForm.tsx @@ -16,6 +16,7 @@ import { import { submitInvoicesBatch } from '@/utils/soroban'; import TokenSelector from './TokenSelector'; +// Enforces maximum 50 rows per batch, matching the contract's submit_invoices_batch() instruction (source of truth) const MAX_BATCH_SIZE = 50; export interface BatchInvoiceRow extends InvoiceFormValues { diff --git a/src/components/__tests__/BatchInvoiceForm.test.tsx b/src/components/__tests__/BatchInvoiceForm.test.tsx index 48c6006..75a900b 100644 --- a/src/components/__tests__/BatchInvoiceForm.test.tsx +++ b/src/components/__tests__/BatchInvoiceForm.test.tsx @@ -113,4 +113,26 @@ describe('BatchInvoiceForm', () => { render(); expect(screen.queryByText(/batch/i)).toBeInTheDocument(); }); + + it('rejects 51-row CSV file exceeding contract batch limit with a clear notification', async () => { + render(); + const header = 'payer,amount,token,discount_rate,due_date'; + const rows = Array.from({ length: 51 }, (_, i) => + `GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC${i % 10},100.00,USDC,3.00,2025-12-31` + ); + const file = new File([[header, ...rows].join('\n')], 'over_limit.csv', { type: 'text/csv' }); + const input = document.getElementById('csv-upload') as HTMLInputElement | null; + if (input) { + fireEvent.change(input, { target: { files: [file] } }); + await waitFor(() => { + expect(addToast).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'warning', + title: 'Rows Truncated', + message: 'Only the first 50 rows were loaded.', + }) + ); + }); + } + }); });