Files
QuoteForge/internal/services/export_test.go

407 lines
9.5 KiB
Go

package services
import (
"bytes"
"encoding/csv"
"io"
"testing"
"time"
"git.mchus.pro/mchus/quoteforge/internal/config"
)
func newTestProjectData(items []ExportItem, article string, serverCount int) *ProjectExportData {
var unitTotal float64
for _, item := range items {
unitTotal += item.UnitPrice * float64(item.Quantity)
}
if serverCount < 1 {
serverCount = 1
}
return &ProjectExportData{
Configs: []ConfigExportBlock{
{
Article: article,
ServerCount: serverCount,
UnitPrice: unitTotal,
Items: items,
},
},
CreatedAt: time.Now(),
}
}
func TestToCSV_UTF8BOM(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]ExportItem{
{
LotName: "LOT-001",
Category: "CAT",
Quantity: 1,
UnitPrice: 100.0,
TotalPrice: 100.0,
},
}, "TEST-ARTICLE", 1)
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")
}
expectedBOM := []byte{0xEF, 0xBB, 0xBF}
actualBOM := csvBytes[:3]
if !bytes.Equal(actualBOM, expectedBOM) {
t.Errorf("UTF-8 BOM mismatch. Expected %v, got %v", expectedBOM, actualBOM)
}
}
func TestToCSV_SemicolonDelimiter(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]ExportItem{
{
LotName: "LOT-001",
Category: "CAT",
Quantity: 2,
UnitPrice: 100.50,
TotalPrice: 201.00,
},
}, "TEST-ARTICLE", 1)
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 = ';'
// Read header
header, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read header: %v", err)
}
if len(header) != 8 {
t.Errorf("Expected 8 columns, got %d", len(header))
}
expectedHeader := []string{"Line", "Type", "p/n", "Description", "Qty (1 pcs.)", "Qty (total)", "Price (1 pcs.)", "Price (total)"}
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 server row
serverRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read server row: %v", err)
}
if serverRow[0] != "10" {
t.Errorf("Expected line number 10, got %s", serverRow[0])
}
if serverRow[2] != "TEST-ARTICLE" {
t.Errorf("Expected article TEST-ARTICLE, got %s", serverRow[2])
}
// Read component row
itemRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read item row: %v", err)
}
if itemRow[2] != "LOT-001" {
t.Errorf("Lot name mismatch: expected LOT-001, got %s", itemRow[2])
}
if itemRow[4] != "2" {
t.Errorf("Quantity mismatch: expected 2, got %s", itemRow[4])
}
if itemRow[6] != "100,5" {
t.Errorf("Unit price mismatch: expected 100,5, got %s", itemRow[6])
}
}
func TestToCSV_ServerRow(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]ExportItem{
{LotName: "LOT-001", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
{LotName: "LOT-002", Category: "CAT", Quantity: 2, UnitPrice: 50.0, TotalPrice: 100.0},
}, "DL380-ART", 10)
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()
// Read server row
serverRow, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read server row: %v", err)
}
if serverRow[0] != "10" {
t.Errorf("Expected line 10, got %s", serverRow[0])
}
if serverRow[2] != "DL380-ART" {
t.Errorf("Expected article DL380-ART, got %s", serverRow[2])
}
if serverRow[5] != "10" {
t.Errorf("Expected server count 10, got %s", serverRow[5])
}
// UnitPrice = 100 + 100 = 200
if serverRow[6] != "200" {
t.Errorf("Expected unit price 200, got %s", serverRow[6])
}
// TotalPrice = 200 * 10 = 2000
if serverRow[7] != "2 000" {
t.Errorf("Expected total price '2 000', got %q", serverRow[7])
}
}
func TestToCSV_CategorySorting(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]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},
}, "ART", 1)
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 server row
reader.Read()
reader.Read()
// Without category repo, items maintain original order
row1, _ := reader.Read()
if row1[2] != "LOT-001" {
t.Errorf("Expected LOT-001 first, got %s", row1[2])
}
row2, _ := reader.Read()
if row2[2] != "LOT-002" {
t.Errorf("Expected LOT-002 second, got %s", row2[2])
}
row3, _ := reader.Read()
if row3[2] != "LOT-003" {
t.Errorf("Expected LOT-003 third, got %s", row3[2])
}
}
func TestToCSV_EmptyData(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ProjectExportData{
Configs: []ConfigExportBlock{},
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 = ';'
header, err := reader.Read()
if err != nil {
t.Fatalf("Failed to read header: %v", err)
}
if len(header) != 8 {
t.Errorf("Expected 8 columns, got %d", len(header))
}
// No more rows expected
_, err = reader.Read()
if err != io.EOF {
t.Errorf("Expected EOF after header, got: %v", err)
}
}
func TestToCSVBytes_BackwardCompat(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]ExportItem{
{LotName: "LOT-001", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
}, "ART", 1)
csvBytes, err := svc.ToCSVBytes(data)
if err != nil {
t.Fatalf("ToCSVBytes failed: %v", err)
}
if len(csvBytes) < 3 {
t.Fatalf("CSV bytes too short")
}
expectedBOM := []byte{0xEF, 0xBB, 0xBF}
actualBOM := csvBytes[:3]
if !bytes.Equal(actualBOM, expectedBOM) {
t.Errorf("UTF-8 BOM mismatch in ToCSVBytes")
}
}
func TestToCSV_WriterError(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := newTestProjectData([]ExportItem{
{LotName: "LOT-001", Category: "CAT", Quantity: 1, UnitPrice: 100.0, TotalPrice: 100.0},
}, "ART", 1)
failingWriter := &failingWriter{}
if err := svc.ToCSV(failingWriter, data); err == nil {
t.Errorf("Expected error from failing writer, got nil")
}
}
func TestToCSV_MultipleBlocks(t *testing.T) {
svc := NewExportService(config.ExportConfig{}, nil, nil)
data := &ProjectExportData{
Configs: []ConfigExportBlock{
{
Article: "ART-1",
ServerCount: 2,
UnitPrice: 500.0,
Items: []ExportItem{
{LotName: "LOT-A", Category: "CPU", Quantity: 1, UnitPrice: 500.0, TotalPrice: 500.0},
},
},
{
Article: "ART-2",
ServerCount: 3,
UnitPrice: 1000.0,
Items: []ExportItem{
{LotName: "LOT-B", Category: "MEM", Quantity: 2, UnitPrice: 500.0, TotalPrice: 1000.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 = ';'
reader.FieldsPerRecord = -1 // allow variable fields
// Header
reader.Read()
// Block 1: server row
srv1, _ := reader.Read()
if srv1[0] != "10" {
t.Errorf("Block 1 line: expected 10, got %s", srv1[0])
}
if srv1[7] != "1 000" {
t.Errorf("Block 1 total: expected '1 000', got %q", srv1[7])
}
// Block 1: component row
comp1, _ := reader.Read()
if comp1[2] != "LOT-A" {
t.Errorf("Block 1 component: expected LOT-A, got %s", comp1[2])
}
// Separator row
sep, _ := reader.Read()
allEmpty := true
for _, v := range sep {
if v != "" {
allEmpty = false
}
}
if !allEmpty {
t.Errorf("Expected empty separator row, got %v", sep)
}
// Block 2: server row
srv2, _ := reader.Read()
if srv2[0] != "20" {
t.Errorf("Block 2 line: expected 20, got %s", srv2[0])
}
if srv2[7] != "3 000" {
t.Errorf("Block 2 total: expected '3 000', got %q", srv2[7])
}
}
func TestFormatPriceWithSpace(t *testing.T) {
tests := []struct {
input float64
expected string
}{
{0, "0"},
{100, "100"},
{1000, "1 000"},
{10470, "10 470"},
{104700, "104 700"},
{1000000, "1 000 000"},
}
for _, tt := range tests {
result := formatPriceWithSpace(tt.input)
if result != tt.expected {
t.Errorf("formatPriceWithSpace(%v): expected %q, got %q", tt.input, tt.expected, result)
}
}
}
func TestFormatPriceComma(t *testing.T) {
tests := []struct {
input float64
expected string
}{
{100.0, "100"},
{2074.5, "2074,5"},
{100.50, "100,5"},
{99.99, "99,99"},
{0, "0"},
}
for _, tt := range tests {
result := formatPriceComma(tt.input)
if result != tt.expected {
t.Errorf("formatPriceComma(%v): expected %q, got %q", tt.input, tt.expected, result)
}
}
}
// failingWriter always returns an error
type failingWriter struct{}
func (fw *failingWriter) Write(p []byte) (int, error) {
return 0, io.EOF
}