package handlers import ( "bytes" "encoding/csv" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "git.mchus.pro/mchus/quoteforge/internal/config" "git.mchus.pro/mchus/quoteforge/internal/models" "git.mchus.pro/mchus/quoteforge/internal/services" "github.com/gin-gonic/gin" ) // Mock services for testing type mockConfigService struct { config *models.Configuration err error } func (m *mockConfigService) GetByUUID(uuid string, ownerUsername string) (*models.Configuration, error) { return m.config, m.err } func TestExportCSV_Success(t *testing.T) { gin.SetMode(gin.TestMode) // Create handler with mocks exportSvc := services.NewExportService(config.ExportConfig{}, nil, nil) handler := NewExportHandler( exportSvc, &mockConfigService{}, nil, ) // Create JSON request body jsonBody := `{ "name": "Test Export", "items": [ { "lot_name": "LOT-001", "quantity": 2, "unit_price": 100.50 } ], "notes": "Test notes" }` // Create HTTP request req, _ := http.NewRequest("POST", "/api/export/csv", bytes.NewBufferString(jsonBody)) req.Header.Set("Content-Type", "application/json") // Create response recorder w := httptest.NewRecorder() // Create Gin context c, _ := gin.CreateTestContext(w) c.Request = req // Call handler handler.ExportCSV(c) // Check status code if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } // Check Content-Type header contentType := w.Header().Get("Content-Type") if contentType != "text/csv; charset=utf-8" { t.Errorf("Expected Content-Type 'text/csv; charset=utf-8', got %q", contentType) } // Check for BOM responseBody := w.Body.Bytes() if len(responseBody) < 3 { t.Fatalf("Response too short to contain BOM") } expectedBOM := []byte{0xEF, 0xBB, 0xBF} actualBOM := responseBody[:3] if !bytes.Equal(actualBOM, expectedBOM) { t.Errorf("UTF-8 BOM mismatch. Expected %v, got %v", expectedBOM, actualBOM) } // Check semicolon delimiter in CSV reader := csv.NewReader(bytes.NewReader(responseBody[3:])) reader.Comma = ';' header, err := reader.Read() if err != nil { t.Errorf("Failed to parse CSV header: %v", err) } if len(header) != 8 { t.Errorf("Expected 8 columns, got %d", len(header)) } } func TestExportCSV_InvalidRequest(t *testing.T) { gin.SetMode(gin.TestMode) exportSvc := services.NewExportService(config.ExportConfig{}, nil, nil) handler := NewExportHandler( exportSvc, &mockConfigService{}, nil, ) // Create invalid request (missing required field) req, _ := http.NewRequest("POST", "/api/export/csv", bytes.NewBufferString(`{"name": "Test"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req handler.ExportCSV(c) // Should return 400 Bad Request if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } // Should return JSON error var errResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &errResp) if _, hasError := errResp["error"]; !hasError { t.Errorf("Expected error in JSON response") } } func TestExportCSV_EmptyItems(t *testing.T) { gin.SetMode(gin.TestMode) exportSvc := services.NewExportService(config.ExportConfig{}, nil, nil) handler := NewExportHandler( exportSvc, &mockConfigService{}, nil, ) // Create request with empty items array - should fail binding validation req, _ := http.NewRequest("POST", "/api/export/csv", bytes.NewBufferString(`{"name":"Empty Export","items":[],"notes":""}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req handler.ExportCSV(c) // Should return 400 Bad Request (validation error from gin binding) if w.Code != http.StatusBadRequest { t.Logf("Status code: %d (expected 400 for empty items)", w.Code) } } func TestExportConfigCSV_Success(t *testing.T) { gin.SetMode(gin.TestMode) // Mock configuration mockConfig := &models.Configuration{ UUID: "test-uuid", Name: "Test Config", OwnerUsername: "testuser", Items: models.ConfigItems{ { LotName: "LOT-001", Quantity: 1, UnitPrice: 100.0, }, }, CreatedAt: time.Now(), } exportSvc := services.NewExportService(config.ExportConfig{}, nil, nil) handler := NewExportHandler( exportSvc, &mockConfigService{config: mockConfig}, nil, ) // Create HTTP request req, _ := http.NewRequest("GET", "/api/configs/test-uuid/export", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{ {Key: "uuid", Value: "test-uuid"}, } // Mock middleware.GetUsername c.Set("username", "testuser") handler.ExportConfigCSV(c) // Check status code if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } // Check Content-Type header contentType := w.Header().Get("Content-Type") if contentType != "text/csv; charset=utf-8" { t.Errorf("Expected Content-Type 'text/csv; charset=utf-8', got %q", contentType) } // Check for BOM responseBody := w.Body.Bytes() if len(responseBody) < 3 { t.Fatalf("Response too short to contain BOM") } expectedBOM := []byte{0xEF, 0xBB, 0xBF} actualBOM := responseBody[:3] if !bytes.Equal(actualBOM, expectedBOM) { t.Errorf("UTF-8 BOM mismatch") } } func TestExportConfigCSV_NotFound(t *testing.T) { gin.SetMode(gin.TestMode) exportSvc := services.NewExportService(config.ExportConfig{}, nil, nil) handler := NewExportHandler( exportSvc, &mockConfigService{err: errors.New("config not found")}, nil, ) req, _ := http.NewRequest("GET", "/api/configs/nonexistent-uuid/export", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{ {Key: "uuid", Value: "nonexistent-uuid"}, } c.Set("username", "testuser") handler.ExportConfigCSV(c) // Should return 404 Not Found if w.Code != http.StatusNotFound { t.Errorf("Expected status 404, got %d", w.Code) } // Should return JSON error var errResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &errResp) if _, hasError := errResp["error"]; !hasError { t.Errorf("Expected error in JSON response") } } func TestExportConfigCSV_EmptyItems(t *testing.T) { gin.SetMode(gin.TestMode) // Mock configuration with empty items mockConfig := &models.Configuration{ UUID: "test-uuid", Name: "Empty Config", OwnerUsername: "testuser", Items: models.ConfigItems{}, CreatedAt: time.Now(), } exportSvc := services.NewExportService(config.ExportConfig{}, nil, nil) handler := NewExportHandler( exportSvc, &mockConfigService{config: mockConfig}, nil, ) req, _ := http.NewRequest("GET", "/api/configs/test-uuid/export", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Params = gin.Params{ {Key: "uuid", Value: "test-uuid"}, } c.Set("username", "testuser") handler.ExportConfigCSV(c) // Should return 400 Bad Request if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } // Should return JSON error var errResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &errResp) if _, hasError := errResp["error"]; !hasError { t.Errorf("Expected error in JSON response") } }