package test import ( "bytes" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/rovina/zsp-backend/internal/dto" "github.com/rovina/zsp-backend/internal/handler" "github.com/rovina/zsp-backend/internal/model" "github.com/rovina/zsp-backend/internal/service" "time" ) // mockZSPFinancesService implements service.ZSPFinancesService for testing type mockZSPFinancesService struct { createFreightChargeFn func(req *dto.ZSPFreightChargesCreateRequest) error getFreightChargeFn func(id string) (*model.ZSPFreightCharges, error) listFreightChargesFn func() ([]model.ZSPFreightCharges, error) updateFreightChargeFn func(id string, req *dto.ZSPFreightChargesUpdateRequest) error deleteFreightChargeFn func(id string) error createMiscChargeFn func(req *dto.ZSPMiscChargesCreateRequest) error getMiscChargeFn func(id string) (*model.ZSPMiscCharges, error) listMiscChargesFn func() ([]model.ZSPMiscCharges, error) updateMiscChargeFn func(id string, req *dto.ZSPMiscChargesUpdateRequest) error deleteMiscChargeFn func(id string) error createServiceFeeFn func(req *dto.ZSPServiceFeesCreateRequest) error getServiceFeeFn func(id string) (*model.ZSPServiceFees, error) listServiceFeesFn func() ([]model.ZSPServiceFees, error) updateServiceFeeFn func(id string, req *dto.ZSPServiceFeesUpdateRequest) error deleteServiceFeeFn func(id string) error createCostBreakdownFn func(req *dto.ZSPCostBreakdownCreateRequest) error getCostBreakdownFn func(id string) (*model.ZSPCostBreakdown, error) listCostBreakdownsFn func(businessID string) ([]model.ZSPCostBreakdown, error) updateCostBreakdownFn func(id string, req *dto.ZSPCostBreakdownUpdateRequest) error deleteCostBreakdownFn func(id string) error createProfitRecordFn func(req *dto.ZSPProfitRecordCreateRequest) error getProfitRecordFn func(id string) (*model.ZSPProfitRecord, error) listProfitRecordsFn func(filters map[string]string) ([]model.ZSPProfitRecord, error) updateProfitRecordFn func(id string, req *dto.ZSPProfitRecordUpdateRequest) error deleteProfitRecordFn func(id string) error } func (m *mockZSPFinancesService) CreateFreightCharge(req *dto.ZSPFreightChargesCreateRequest) error { return m.createFreightChargeFn(req) } func (m *mockZSPFinancesService) GetFreightCharge(id string) (*model.ZSPFreightCharges, error) { return m.getFreightChargeFn(id) } func (m *mockZSPFinancesService) ListFreightCharges() ([]model.ZSPFreightCharges, error) { return m.listFreightChargesFn() } func (m *mockZSPFinancesService) UpdateFreightCharge(id string, req *dto.ZSPFreightChargesUpdateRequest) error { return m.updateFreightChargeFn(id, req) } func (m *mockZSPFinancesService) DeleteFreightCharge(id string) error { return m.deleteFreightChargeFn(id) } func (m *mockZSPFinancesService) CreateMiscCharge(req *dto.ZSPMiscChargesCreateRequest) error { return m.createMiscChargeFn(req) } func (m *mockZSPFinancesService) GetMiscCharge(id string) (*model.ZSPMiscCharges, error) { return m.getMiscChargeFn(id) } func (m *mockZSPFinancesService) ListMiscCharges() ([]model.ZSPMiscCharges, error) { return m.listMiscChargesFn() } func (m *mockZSPFinancesService) UpdateMiscCharge(id string, req *dto.ZSPMiscChargesUpdateRequest) error { return m.updateMiscChargeFn(id, req) } func (m *mockZSPFinancesService) DeleteMiscCharge(id string) error { return m.deleteMiscChargeFn(id) } func (m *mockZSPFinancesService) CreateServiceFee(req *dto.ZSPServiceFeesCreateRequest) error { return m.createServiceFeeFn(req) } func (m *mockZSPFinancesService) GetServiceFee(id string) (*model.ZSPServiceFees, error) { return m.getServiceFeeFn(id) } func (m *mockZSPFinancesService) ListServiceFees() ([]model.ZSPServiceFees, error) { return m.listServiceFeesFn() } func (m *mockZSPFinancesService) UpdateServiceFee(id string, req *dto.ZSPServiceFeesUpdateRequest) error { return m.updateServiceFeeFn(id, req) } func (m *mockZSPFinancesService) DeleteServiceFee(id string) error { return m.deleteServiceFeeFn(id) } func (m *mockZSPFinancesService) CreateCostBreakdown(req *dto.ZSPCostBreakdownCreateRequest) error { return m.createCostBreakdownFn(req) } func (m *mockZSPFinancesService) GetCostBreakdown(id string) (*model.ZSPCostBreakdown, error) { return m.getCostBreakdownFn(id) } func (m *mockZSPFinancesService) ListCostBreakdowns(businessID string) ([]model.ZSPCostBreakdown, error) { return m.listCostBreakdownsFn(businessID) } func (m *mockZSPFinancesService) UpdateCostBreakdown(id string, req *dto.ZSPCostBreakdownUpdateRequest) error { return m.updateCostBreakdownFn(id, req) } func (m *mockZSPFinancesService) DeleteCostBreakdown(id string) error { return m.deleteCostBreakdownFn(id) } func (m *mockZSPFinancesService) CreateProfitRecord(req *dto.ZSPProfitRecordCreateRequest) error { return m.createProfitRecordFn(req) } func (m *mockZSPFinancesService) GetProfitRecord(id string) (*model.ZSPProfitRecord, error) { return m.getProfitRecordFn(id) } func (m *mockZSPFinancesService) ListProfitRecords(filters map[string]string) ([]model.ZSPProfitRecord, error) { return m.listProfitRecordsFn(filters) } func (m *mockZSPFinancesService) UpdateProfitRecord(id string, req *dto.ZSPProfitRecordUpdateRequest) error { return m.updateProfitRecordFn(id, req) } func (m *mockZSPFinancesService) DeleteProfitRecord(id string) error { return m.deleteProfitRecordFn(id) } var _ service.ZSPFinancesService = (*mockZSPFinancesService)(nil) func setupZSPFinancesRouter(h *handler.ZSPFinancesHandler) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() api := r.Group("/api") { finances := api.Group("/zsp-finances") { // Freight charges finances.POST("/freight-charges", h.CreateFreightCharge) finances.GET("/freight-charges/:id", h.GetFreightCharge) finances.GET("/freight-charges", h.ListFreightCharges) finances.PUT("/freight-charges/:id", h.UpdateFreightCharge) finances.DELETE("/freight-charges/:id", h.DeleteFreightCharge) // Misc charges finances.POST("/misc-charges", h.CreateMiscCharge) finances.GET("/misc-charges/:id", h.GetMiscCharge) finances.GET("/misc-charges", h.ListMiscCharges) finances.PUT("/misc-charges/:id", h.UpdateMiscCharge) finances.DELETE("/misc-charges/:id", h.DeleteMiscCharge) // Service fees finances.POST("/service-fees", h.CreateServiceFee) finances.GET("/service-fees/:id", h.GetServiceFee) finances.GET("/service-fees", h.ListServiceFees) finances.PUT("/service-fees/:id", h.UpdateServiceFee) finances.DELETE("/service-fees/:id", h.DeleteServiceFee) // Cost breakdowns finances.POST("/cost-breakdowns", h.CreateCostBreakdown) finances.GET("/cost-breakdowns/:id", h.GetCostBreakdown) finances.GET("/cost-breakdowns", h.ListCostBreakdowns) finances.PUT("/cost-breakdowns/:id", h.UpdateCostBreakdown) finances.DELETE("/cost-breakdowns/:id", h.DeleteCostBreakdown) // Profit records finances.POST("/profit-records", h.CreateProfitRecord) finances.GET("/profit-records/:id", h.GetProfitRecord) finances.GET("/profit-records", h.ListProfitRecords) finances.PUT("/profit-records/:id", h.UpdateProfitRecord) finances.DELETE("/profit-records/:id", h.DeleteProfitRecord) } } return r } // ========== Freight Charges Tests ========== func TestZSPFinancesCreateFreightCharge_Success(t *testing.T) { mock := &mockZSPFinancesService{ createFreightChargeFn: func(req *dto.ZSPFreightChargesCreateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) body, _ := json.Marshal(dto.ZSPFreightChargesCreateRequest{ ExpenseItem: "运费", Amount: 1000.0, PaymentDate: "2024-01-15", FreightCompanyName: "货代公司A", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/zsp-finances/freight-charges", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["success"] != true { t.Errorf("expected success true, got %v", resp["success"]) } } func TestZSPFinancesCreateFreightCharge_BadRequest(t *testing.T) { mock := &mockZSPFinancesService{ createFreightChargeFn: func(req *dto.ZSPFreightChargesCreateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/zsp-finances/freight-charges", bytes.NewReader([]byte("{}"))) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400 for missing required fields, got %d", w.Code) } } func TestZSPFinancesGetFreightCharge_Success(t *testing.T) { mock := &mockZSPFinancesService{ getFreightChargeFn: func(id string) (*model.ZSPFreightCharges, error) { return &model.ZSPFreightCharges{ ID: 1, ExpenseItem: "运费", Amount: 1000.0, FreightCompanyName: "货代公司A", }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/freight-charges/1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesListFreightCharges_Success(t *testing.T) { mock := &mockZSPFinancesService{ listFreightChargesFn: func() ([]model.ZSPFreightCharges, error) { return []model.ZSPFreightCharges{ {ID: 1, ExpenseItem: "运费", Amount: 1000.0}, {ID: 2, ExpenseItem: "港杂费", Amount: 500.0}, }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/freight-charges", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["success"] != true { t.Errorf("expected success true, got %v", resp["success"]) } } func TestZSPFinancesUpdateFreightCharge_Success(t *testing.T) { mock := &mockZSPFinancesService{ updateFreightChargeFn: func(id string, req *dto.ZSPFreightChargesUpdateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) body, _ := json.Marshal(dto.ZSPFreightChargesUpdateRequest{ Amount: 1500.0, }) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/zsp-finances/freight-charges/1", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesDeleteFreightCharge_Success(t *testing.T) { mock := &mockZSPFinancesService{ deleteFreightChargeFn: func(id string) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/zsp-finances/freight-charges/1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesDeleteFreightCharge_Error(t *testing.T) { mock := &mockZSPFinancesService{ deleteFreightChargeFn: func(id string) error { return errors.New("record not found") }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/zsp-finances/freight-charges/999", nil) router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Errorf("expected status 500, got %d", w.Code) } } // ========== Misc Charges Tests ========== func TestZSPFinancesCreateMiscCharge_Success(t *testing.T) { mock := &mockZSPFinancesService{ createMiscChargeFn: func(req *dto.ZSPMiscChargesCreateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) body, _ := json.Marshal(dto.ZSPMiscChargesCreateRequest{ ExpenseItem: "报关费", Amount: 200.0, PaymentDate: "2024-01-15", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/zsp-finances/misc-charges", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesListMiscCharges_Success(t *testing.T) { mock := &mockZSPFinancesService{ listMiscChargesFn: func() ([]model.ZSPMiscCharges, error) { return []model.ZSPMiscCharges{ {ID: 1, ExpenseItem: "报关费", Amount: 200.0}, {ID: 2, ExpenseItem: "仓储费", Amount: 300.0}, }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/misc-charges", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesDeleteMiscCharge_Success(t *testing.T) { mock := &mockZSPFinancesService{ deleteMiscChargeFn: func(id string) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/zsp-finances/misc-charges/1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } // ========== Service Fees Tests ========== func TestZSPFinancesCreateServiceFee_Success(t *testing.T) { mock := &mockZSPFinancesService{ createServiceFeeFn: func(req *dto.ZSPServiceFeesCreateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) body, _ := json.Marshal(dto.ZSPServiceFeesCreateRequest{ Name: "代理服务费", Amount: 500.0, Date: "2024-01-15", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/zsp-finances/service-fees", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesListServiceFees_Success(t *testing.T) { mock := &mockZSPFinancesService{ listServiceFeesFn: func() ([]model.ZSPServiceFees, error) { return []model.ZSPServiceFees{ {ID: 1, Name: "代理服务费", Amount: 500.0}, {ID: 2, Name: "咨询费", Amount: 300.0}, }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/service-fees", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesDeleteServiceFee_Success(t *testing.T) { mock := &mockZSPFinancesService{ deleteServiceFeeFn: func(id string) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/zsp-finances/service-fees/1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } // ========== Cost Breakdown Tests ========== func TestZSPFinancesCreateCostBreakdown_Success(t *testing.T) { mock := &mockZSPFinancesService{ createCostBreakdownFn: func(req *dto.ZSPCostBreakdownCreateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) body, _ := json.Marshal(dto.ZSPCostBreakdownCreateRequest{ BusinessID: "B001", BusinessType: "contract", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/zsp-finances/cost-breakdowns", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesListCostBreakdowns_Success(t *testing.T) { mock := &mockZSPFinancesService{ listCostBreakdownsFn: func(businessID string) ([]model.ZSPCostBreakdown, error) { return []model.ZSPCostBreakdown{ {ID: 1, BusinessID: "B001", BusinessType: "contract", TotalCost: 1000.0}, }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/cost-breakdowns?businessId=B001", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesDeleteCostBreakdown_Success(t *testing.T) { mock := &mockZSPFinancesService{ deleteCostBreakdownFn: func(id string) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/zsp-finances/cost-breakdowns/1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } // ========== Profit Record Tests ========== func TestZSPFinancesCreateProfitRecord_Success(t *testing.T) { mock := &mockZSPFinancesService{ createProfitRecordFn: func(req *dto.ZSPProfitRecordCreateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) body, _ := json.Marshal(dto.ZSPProfitRecordCreateRequest{ BusinessID: "B001", BusinessType: "contract", Revenue: 2000.0, TotalCost: 1500.0, RecordDate: "2024-01-15", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/zsp-finances/profit-records", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesGetProfitRecord_Success(t *testing.T) { mock := &mockZSPFinancesService{ getProfitRecordFn: func(id string) (*model.ZSPProfitRecord, error) { return &model.ZSPProfitRecord{ ID: 1, BusinessID: "B001", BusinessType: "contract", Revenue: 2000.0, TotalCost: 1500.0, Profit: 500.0, RecordDate: time.Now(), }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records/1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesListProfitRecords_Success(t *testing.T) { mock := &mockZSPFinancesService{ listProfitRecordsFn: func(filters map[string]string) ([]model.ZSPProfitRecord, error) { return []model.ZSPProfitRecord{ {ID: 1, BusinessID: "B001", Revenue: 2000.0, Profit: 500.0}, {ID: 2, BusinessID: "B002", Revenue: 3000.0, Profit: 800.0}, }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) if resp["success"] != true { t.Errorf("expected success true, got %v", resp["success"]) } } func TestZSPFinancesListProfitRecords_WithFilters(t *testing.T) { mock := &mockZSPFinancesService{ listProfitRecordsFn: func(filters map[string]string) ([]model.ZSPProfitRecord, error) { if filters["businessId"] != "B001" { t.Errorf("expected businessId filter B001, got %v", filters["businessId"]) } return []model.ZSPProfitRecord{ {ID: 1, BusinessID: "B001", Revenue: 2000.0, Profit: 500.0}, }, nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/zsp-finances/profit-records?businessId=B001&businessType=contract", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesUpdateProfitRecord_Success(t *testing.T) { mock := &mockZSPFinancesService{ updateProfitRecordFn: func(id string, req *dto.ZSPProfitRecordUpdateRequest) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) body, _ := json.Marshal(dto.ZSPProfitRecordUpdateRequest{ Revenue: 2500.0, TotalCost: 1600.0, }) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/zsp-finances/profit-records/1", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestZSPFinancesDeleteProfitRecord_Success(t *testing.T) { mock := &mockZSPFinancesService{ deleteProfitRecordFn: func(id string) error { return nil }, } h := handler.NewZSPFinancesHandler(mock) router := setupZSPFinancesRouter(h) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/zsp-finances/profit-records/1", nil) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } }