Files
QuoteForge/internal/handlers/export_test.go
Mikhail Chusavitin 8f596cec68 fix: standardize CSV export filename format to use project name
Unified export filename format across both ExportCSV and ExportConfigCSV:
- Format: YYYY-MM-DD (project_name) config_name BOM.csv
- Use PriceUpdatedAt if available, otherwise CreatedAt
- Extract project name from ProjectUUID for ExportCSV via projectService
- Pass project_uuid from frontend to backend in export request
- Add projectUUID and projectName state variables to track project context

This ensures consistent naming whether exporting from form or project view,
and uses most recent price update timestamp in filename.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-09 17:22:51 +03:00

315 lines
7.5 KiB
Go

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 a basic mock component service that doesn't panic
mockComponentService := &services.ComponentService{}
// Create handler with mocks
exportSvc := services.NewExportService(config.ExportConfig{}, nil)
handler := NewExportHandler(
exportSvc,
&mockConfigService{},
mockComponentService,
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.Compare(actualBOM, expectedBOM) != 0 {
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) != 6 {
t.Errorf("Expected 6 columns, got %d", len(header))
}
}
func TestExportCSV_InvalidRequest(t *testing.T) {
gin.SetMode(gin.TestMode)
exportSvc := services.NewExportService(config.ExportConfig{}, nil)
handler := NewExportHandler(
exportSvc,
&mockConfigService{},
&services.ComponentService{},
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)
handler := NewExportHandler(
exportSvc,
&mockConfigService{},
&services.ComponentService{},
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)
handler := NewExportHandler(
exportSvc,
&mockConfigService{config: mockConfig},
&services.ComponentService{},
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.Compare(actualBOM, expectedBOM) != 0 {
t.Errorf("UTF-8 BOM mismatch")
}
}
func TestExportConfigCSV_NotFound(t *testing.T) {
gin.SetMode(gin.TestMode)
exportSvc := services.NewExportService(config.ExportConfig{}, nil)
handler := NewExportHandler(
exportSvc,
&mockConfigService{err: errors.New("config not found")},
&services.ComponentService{},
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)
handler := NewExportHandler(
exportSvc,
&mockConfigService{config: mockConfig},
&services.ComponentService{},
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")
}
}