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>
315 lines
7.5 KiB
Go
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")
|
|
}
|
|
}
|