package services import ( "bytes" "encoding/csv" "io" "testing" "time" "git.mchus.pro/mchus/quoteforge/internal/config" ) func TestToCSV_UTF8BOM(t *testing.T) { svc := NewExportService(config.ExportConfig{}, nil) data := &ExportData{ Name: "Test", Items: []ExportItem{ { LotName: "LOT-001", Description: "Test Item", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0, }, }, Total: 100.0, CreatedAt: time.Now(), } var buf bytes.Buffer if err := svc.ToCSV(&buf, data); err != nil { t.Fatalf("ToCSV failed: %v", err) } csvBytes := buf.Bytes() if len(csvBytes) < 3 { t.Fatalf("CSV too short to contain BOM") } // Check UTF-8 BOM: 0xEF 0xBB 0xBF expectedBOM := []byte{0xEF, 0xBB, 0xBF} actualBOM := csvBytes[:3] if bytes.Compare(actualBOM, expectedBOM) != 0 { t.Errorf("UTF-8 BOM mismatch. Expected %v, got %v", expectedBOM, actualBOM) } } func TestToCSV_SemicolonDelimiter(t *testing.T) { svc := NewExportService(config.ExportConfig{}, nil) data := &ExportData{ Name: "Test", Items: []ExportItem{ { LotName: "LOT-001", Description: "Test Item", Category: "CAT", Quantity: 2, UnitPrice: 100.50, TotalPrice: 201.00, }, }, Total: 201.00, CreatedAt: time.Now(), } var buf bytes.Buffer if err := svc.ToCSV(&buf, data); err != nil { t.Fatalf("ToCSV failed: %v", err) } // Skip BOM and read CSV with semicolon delimiter csvBytes := buf.Bytes() reader := csv.NewReader(bytes.NewReader(csvBytes[3:])) reader.Comma = ';' // Read header header, err := reader.Read() if err != nil { t.Fatalf("Failed to read header: %v", err) } if len(header) != 6 { t.Errorf("Expected 6 columns, got %d", len(header)) } expectedHeader := []string{"Артикул", "Описание", "Категория", "Количество", "Цена за единицу", "Сумма"} for i, col := range expectedHeader { if i < len(header) && header[i] != col { t.Errorf("Column %d: expected %q, got %q", i, col, header[i]) } } // Read item row itemRow, err := reader.Read() if err != nil { t.Fatalf("Failed to read item row: %v", err) } if itemRow[0] != "LOT-001" { t.Errorf("Lot name mismatch: expected LOT-001, got %s", itemRow[0]) } if itemRow[3] != "2" { t.Errorf("Quantity mismatch: expected 2, got %s", itemRow[3]) } if itemRow[4] != "100,50" { t.Errorf("Unit price mismatch: expected 100,50, got %s", itemRow[4]) } } func TestToCSV_TotalRow(t *testing.T) { svc := NewExportService(config.ExportConfig{}, nil) data := &ExportData{ Name: "Test", Items: []ExportItem{ { LotName: "LOT-001", Description: "Item 1", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0, }, { LotName: "LOT-002", Description: "Item 2", Category: "CAT", Quantity: 2, UnitPrice: 50.0, TotalPrice: 100.0, }, }, Total: 200.0, CreatedAt: time.Now(), } var buf bytes.Buffer if err := svc.ToCSV(&buf, data); err != nil { t.Fatalf("ToCSV failed: %v", err) } csvBytes := buf.Bytes() reader := csv.NewReader(bytes.NewReader(csvBytes[3:])) reader.Comma = ';' // Skip header and item rows reader.Read() reader.Read() reader.Read() // Read total row totalRow, err := reader.Read() if err != nil { t.Fatalf("Failed to read total row: %v", err) } // Total row should have "ИТОГО:" in position 4 and total value in position 5 if totalRow[4] != "ИТОГО:" { t.Errorf("Expected 'ИТОГО:' in column 4, got %q", totalRow[4]) } if totalRow[5] != "200,00" { t.Errorf("Expected total 200,00, got %s", totalRow[5]) } } func TestToCSV_CategorySorting(t *testing.T) { // Test category sorting without category repo (items maintain original order) svc := NewExportService(config.ExportConfig{}, nil) data := &ExportData{ Name: "Test", Items: []ExportItem{ { LotName: "LOT-001", Category: "CAT-A", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0, }, { LotName: "LOT-002", Category: "CAT-C", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0, }, { LotName: "LOT-003", Category: "CAT-B", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0, }, }, Total: 300.0, CreatedAt: time.Now(), } var buf bytes.Buffer if err := svc.ToCSV(&buf, data); err != nil { t.Fatalf("ToCSV failed: %v", err) } csvBytes := buf.Bytes() reader := csv.NewReader(bytes.NewReader(csvBytes[3:])) reader.Comma = ';' // Skip header reader.Read() // Without category repo, items maintain original order row1, _ := reader.Read() if row1[0] != "LOT-001" { t.Errorf("Expected LOT-001 first, got %s", row1[0]) } row2, _ := reader.Read() if row2[0] != "LOT-002" { t.Errorf("Expected LOT-002 second, got %s", row2[0]) } row3, _ := reader.Read() if row3[0] != "LOT-003" { t.Errorf("Expected LOT-003 third, got %s", row3[0]) } } func TestToCSV_EmptyData(t *testing.T) { svc := NewExportService(config.ExportConfig{}, nil) data := &ExportData{ Name: "Test", Items: []ExportItem{}, Total: 0.0, CreatedAt: time.Now(), } var buf bytes.Buffer if err := svc.ToCSV(&buf, data); err != nil { t.Fatalf("ToCSV failed: %v", err) } csvBytes := buf.Bytes() reader := csv.NewReader(bytes.NewReader(csvBytes[3:])) reader.Comma = ';' // Should have header and total row header, err := reader.Read() if err != nil { t.Fatalf("Failed to read header: %v", err) } if len(header) != 6 { t.Errorf("Expected 6 columns, got %d", len(header)) } totalRow, err := reader.Read() if err != nil { t.Fatalf("Failed to read total row: %v", err) } if totalRow[4] != "ИТОГО:" { t.Errorf("Expected ИТОГО: in total row, got %s", totalRow[4]) } } func TestToCSVBytes_BackwardCompat(t *testing.T) { svc := NewExportService(config.ExportConfig{}, nil) data := &ExportData{ Name: "Test", Items: []ExportItem{ { LotName: "LOT-001", Description: "Test Item", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0, }, }, Total: 100.0, CreatedAt: time.Now(), } csvBytes, err := svc.ToCSVBytes(data) if err != nil { t.Fatalf("ToCSVBytes failed: %v", err) } if len(csvBytes) < 3 { t.Fatalf("CSV bytes too short") } // Verify BOM is present expectedBOM := []byte{0xEF, 0xBB, 0xBF} actualBOM := csvBytes[:3] if bytes.Compare(actualBOM, expectedBOM) != 0 { t.Errorf("UTF-8 BOM mismatch in ToCSVBytes") } } func TestToCSV_WriterError(t *testing.T) { svc := NewExportService(config.ExportConfig{}, nil) data := &ExportData{ Name: "Test", Items: []ExportItem{ { LotName: "LOT-001", Description: "Test", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0, }, }, Total: 100.0, CreatedAt: time.Now(), } // Use a failing writer failingWriter := &failingWriter{} if err := svc.ToCSV(failingWriter, data); err == nil { t.Errorf("Expected error from failing writer, got nil") } } // failingWriter always returns an error type failingWriter struct{} func (fw *failingWriter) Write(p []byte) (int, error) { return 0, io.EOF }