diff --git a/internal/csvimport/csvimport.go b/internal/csvimport/csvimport.go index b18dd97..9d0c342 100644 --- a/internal/csvimport/csvimport.go +++ b/internal/csvimport/csvimport.go @@ -17,11 +17,9 @@ type Row struct { func Parse(r io.Reader) ([]Row, error) { cr := csv.NewReader(r) cr.FieldsPerRecord = -1 // проверяем сами - cr.TrimLeadingSpace = true var rows []Row seen := map[string]bool{} - line := 0 for { rec, err := cr.Read() if err == io.EOF { @@ -30,9 +28,9 @@ func Parse(r io.Reader) ([]Row, error) { if err != nil { return nil, err } - line++ + line, _ := cr.FieldPos(0) if len(rec) == 1 && strings.TrimSpace(rec[0]) == "" { - continue // пустая строка + continue // encoding/csv уже пропускает голые пустые строки; это ветка ловит строки из одних пробелов } if len(rec) != 4 { return nil, fmt.Errorf("line %d: expected 4 columns, got %d", line, len(rec)) diff --git a/internal/csvimport/csvimport_test.go b/internal/csvimport/csvimport_test.go index 3089ed0..82279a3 100644 --- a/internal/csvimport/csvimport_test.go +++ b/internal/csvimport/csvimport_test.go @@ -32,3 +32,20 @@ func TestParseRejectsEmptyField(t *testing.T) { t.Fatal("empty password must error") } } + +func TestParseBlankLineKeepsCorrectLineNumber(t *testing.T) { + // blank physical line 2, malformed row on physical line 3 + _, err := Parse(strings.NewReader("a@x,p1,a@y,p2\n\nbad,row,here\n")) + if err == nil { + t.Fatal("expected error for 3-column row") + } + if !strings.Contains(err.Error(), "line 3") { + t.Fatalf("error must reference physical line 3, got: %v", err) + } +} + +func TestParseZeroRowsErrors(t *testing.T) { + if _, err := Parse(strings.NewReader("\n\n \n")); err == nil { + t.Fatal("expected error when no rows parsed") + } +}