diff --git a/internal/csvimport/csvimport.go b/internal/csvimport/csvimport.go new file mode 100644 index 0000000..b18dd97 --- /dev/null +++ b/internal/csvimport/csvimport.go @@ -0,0 +1,56 @@ +package csvimport + +import ( + "encoding/csv" + "fmt" + "io" + "strings" +) + +type Row struct { + SrcLogin string + SrcPass string + DstLogin string + DstPass string +} + +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 { + break + } + if err != nil { + return nil, err + } + line++ + if len(rec) == 1 && strings.TrimSpace(rec[0]) == "" { + continue // пустая строка + } + if len(rec) != 4 { + return nil, fmt.Errorf("line %d: expected 4 columns, got %d", line, len(rec)) + } + for i := range rec { + rec[i] = strings.TrimSpace(rec[i]) + if rec[i] == "" { + return nil, fmt.Errorf("line %d: column %d is empty", line, i+1) + } + } + if seen[rec[0]] { + return nil, fmt.Errorf("line %d: duplicate src_login %q", line, rec[0]) + } + seen[rec[0]] = true + rows = append(rows, Row{SrcLogin: rec[0], SrcPass: rec[1], DstLogin: rec[2], DstPass: rec[3]}) + } + if len(rows) == 0 { + return nil, fmt.Errorf("no rows parsed") + } + return rows, nil +} diff --git a/internal/csvimport/csvimport_test.go b/internal/csvimport/csvimport_test.go new file mode 100644 index 0000000..3089ed0 --- /dev/null +++ b/internal/csvimport/csvimport_test.go @@ -0,0 +1,34 @@ +package csvimport + +import ( + "strings" + "testing" +) + +func TestParseOK(t *testing.T) { + rows, err := Parse(strings.NewReader("a@x,p1,a@y,p2\nb@x,p3,b@y,p4\n")) + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(rows) != 2 || rows[0].SrcLogin != "a@x" || rows[1].DstPass != "p4" { + t.Fatalf("bad rows: %+v", rows) + } +} + +func TestParseRejectsBadColumns(t *testing.T) { + if _, err := Parse(strings.NewReader("a,b,c\n")); err == nil { + t.Fatal("3 columns must error") + } +} + +func TestParseRejectsDuplicateSrc(t *testing.T) { + if _, err := Parse(strings.NewReader("a@x,p,a@y,p\na@x,q,c@y,q\n")); err == nil { + t.Fatal("duplicate src_login must error") + } +} + +func TestParseRejectsEmptyField(t *testing.T) { + if _, err := Parse(strings.NewReader("a@x,,a@y,p\n")); err == nil { + t.Fatal("empty password must error") + } +}