package service import ( "errors" "strings" "testing" "github.com/DATA-DOG/go-sqlmock" ) // expectWarehouseSummary 让 WarehouseRepository.List 返回一组仓库/库位,用于驱动 // ResolveInventoryScope。传 nil 表示"该公司没有仓库"。 func expectWarehouseSummary(mock sqlmock.Sqlmock, organizationID int64, warehouseStatus, locationStatus *string) { rows := sqlmock.NewRows([]string{"id", "organization_id", "code", "name", "status", "location_id", "location_code", "location_name", "location_status"}) if warehouseStatus != nil { var locationID interface{} = nil var locationCode, locationName, locStatus interface{} = nil, nil, nil if locationStatus != nil { locationID, locationCode, locationName, locStatus = 20, "L", "库位", *locationStatus } rows.AddRow(10, organizationID, "W", "仓库", *warehouseStatus, locationID, locationCode, locationName, locStatus) } mock.ExpectQuery("SELECT w.id, w.organization_id").WithArgs(organizationID).WillReturnRows(rows) } func strptr(s string) *string { return &s } func TestResolveInventoryScopeUnavailableCases(t *testing.T) { cases := []struct { name string warehouseStatus *string locationStatus *string requireActive bool wantSubstring string }{ {"no warehouse", nil, nil, true, "请先配置本公司仓库"}, {"no location", strptr("active"), nil, true, "请先配置本公司仓库的库位"}, {"disabled warehouse", strptr("inactive"), strptr("active"), true, "本公司仓库已停用"}, {"disabled location", strptr("active"), strptr("inactive"), true, "本公司库位已停用"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { mock := outboundMock(t) expectWarehouseSummary(mock, 1, tc.warehouseStatus, tc.locationStatus) _, err := ResolveInventoryScope(1, tc.requireActive) if !errors.Is(err, ErrInventoryScopeUnavailable) { t.Fatalf("got %v, want ErrInventoryScopeUnavailable", err) } if !strings.Contains(err.Error(), tc.wantSubstring) { t.Fatalf("message %q must contain %q", err.Error(), tc.wantSubstring) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } func TestResolveInventoryScopeReturnsActiveDimensions(t *testing.T) { mock := outboundMock(t) expectWarehouseSummary(mock, 3, strptr("active"), strptr("active")) scope, err := ResolveInventoryScope(3, true) if err != nil { t.Fatalf("resolve scope: %v", err) } if scope.OrganizationID != 3 || scope.WarehouseID != 10 || scope.LocationID != 20 { t.Fatalf("scope=%+v, want 3/10/20", scope) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } // 只读场景(requireActive=false):停用的仓库、库位仍返回其维度,方便核验历史余额。 func TestResolveInventoryScopeKeepsDisabledDimensionsForReads(t *testing.T) { mock := outboundMock(t) expectWarehouseSummary(mock, 1, strptr("inactive"), strptr("inactive")) scope, err := ResolveInventoryScope(1, false) if err != nil { t.Fatalf("resolve scope for reads: %v", err) } if scope.WarehouseID != 10 || scope.LocationID != 20 { t.Fatalf("scope=%+v, want disabled dimensions preserved", scope) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }