turash/models/params/loader_test.go
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

602 lines
21 KiB
Go

package params
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadFromFile(t *testing.T) {
p, err := LoadFromFile("../params.yaml")
require.NoError(t, err, "Failed to load params.yaml")
// Test time configuration
assert.Equal(t, []int{1, 2, 3}, p.Time.Years, "Years should be [1,2,3]")
// Test adoption parameters
assert.Equal(t, 500, p.Adoption.TotalOrgs.GetYear(1), "Year 1 total orgs should be 500")
assert.Equal(t, 2000, p.Adoption.TotalOrgs.GetYear(2), "Year 2 total orgs should be 2000")
assert.Equal(t, 5000, p.Adoption.TotalOrgs.GetYear(3), "Year 3 total orgs should be 5000")
assert.Equal(t, 0.30, p.Adoption.PayingShare.GetYear(1), "Year 1 paying share should be 0.30")
// Test pricing
assert.Equal(t, 35.0, p.Pricing.Basic, "Basic tier price should be 35")
assert.Equal(t, 120.0, p.Pricing.Business, "Business tier price should be 120")
assert.Equal(t, 400.0, p.Pricing.Enterprise, "Enterprise tier price should be 400")
}
// Test LoadFromFile with non-existent file
func TestLoadFromFile_FileNotFound(t *testing.T) {
_, err := LoadFromFile("non_existent_file.yaml")
assert.Error(t, err, "Should return error for non-existent file")
assert.Contains(t, err.Error(), "non_existent_file.yaml", "Error should mention the filename")
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
params *Params
wantError bool
errorMsg string
}{
{
name: "valid params",
params: createValidParams(),
wantError: false,
},
{
name: "no years",
params: &Params{
Time: TimeParams{Years: []int{}},
},
wantError: true,
errorMsg: "must have at least one year",
},
{
name: "missing total orgs",
params: &Params{
Time: TimeParams{Years: []int{1}},
Adoption: AdoptionParams{
TotalOrgs: YearlyInt{}, // empty
},
},
wantError: true,
errorMsg: "missing value for year 1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.params.Validate()
if tt.wantError {
assert.Error(t, err, "Expected validation error")
assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain expected text")
} else {
assert.NoError(t, err, "Expected no validation error")
}
})
}
}
func TestYearlyInt_GetYear(t *testing.T) {
yi := YearlyInt{
"1": 100,
"2": 200,
"3": 300,
}
assert.Equal(t, 100, yi.GetYear(1), "Year 1 should return 100")
assert.Equal(t, 200, yi.GetYear(2), "Year 2 should return 200")
assert.Equal(t, 300, yi.GetYear(3), "Year 3 should return 300")
assert.Equal(t, 0, yi.GetYear(4), "Non-existent year should return 0")
}
func TestYearlyFloat_GetYear(t *testing.T) {
yf := YearlyFloat{
"1": 1.5,
"2": 2.5,
"3": 3.5,
}
assert.Equal(t, 1.5, yf.GetYear(1), "Year 1 should return 1.5")
assert.Equal(t, 2.5, yf.GetYear(2), "Year 2 should return 2.5")
assert.Equal(t, 3.5, yf.GetYear(3), "Year 3 should return 3.5")
assert.Equal(t, 0.0, yf.GetYear(4), "Non-existent year should return 0.0")
}
func TestYearlyTierMix_GetYear(t *testing.T) {
ytm := YearlyTierMix{
"1": TierMix{Basic: 0.6, Business: 0.3, Enterprise: 0.1},
"2": TierMix{Basic: 0.5, Business: 0.35, Enterprise: 0.15},
}
tier1 := ytm.GetYear(1)
assert.Equal(t, 0.6, tier1.Basic, "Year 1 Basic should be 0.6")
assert.Equal(t, 0.3, tier1.Business, "Year 1 Business should be 0.3")
assert.Equal(t, 0.1, tier1.Enterprise, "Year 1 Enterprise should be 0.1")
tier2 := ytm.GetYear(2)
assert.Equal(t, 0.5, tier2.Basic, "Year 2 Basic should be 0.5")
assert.Equal(t, 0.35, tier2.Business, "Year 2 Business should be 0.35")
// Test empty case
emptyTier := ytm.GetYear(3)
assert.Equal(t, TierMix{}, emptyTier, "Non-existent year should return empty TierMix")
}
func createValidParams() *Params {
return &Params{
Time: TimeParams{Years: []int{1, 2, 3}},
Adoption: AdoptionParams{
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
},
Pricing: PricingParams{
Basic: 35.0,
Business: 120.0,
Enterprise: 400.0,
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
},
Transactions: TransactionParams{
AvgIntroFee: 550.0,
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
ServiceCommission: 0.15,
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
GroupCommission: 0.04,
},
Municipal: MunicipalParams{
Cities: YearlyInt{"1": 1, "2": 2, "3": 4},
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
},
ImplServices: ImplementationParams{
MatchesPerOrg: 0.5,
PaidShare: 0.25,
AvgFee: 5000.0,
},
Impact: ImpactParams{
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
GridFactor: 0.3,
HXEff: 0.9,
Utilization: 0.7,
WaterPerOrg: 25000.0,
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
WastePerOrg: 100.0,
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
},
Costs: CostParams{
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
EngineerSalary: 100000.0,
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
},
Market: MarketParams{
TAM: 500000000000.0,
AddressableDigital: 3000000000.0,
PilotCityEconomicBenefit: 4000000.0,
ScalabilityPotential: 400000000.0,
EUIndustrialFacilities: 2100000,
EnergyWastePotential: 0.45,
ResourceCostReduction: 0.25,
ViableExchangeRate: 0.15,
PlatformCaptureRate: 0.50,
SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
},
}
}
// Test LoadFromJSON function
func TestLoadFromJSON(t *testing.T) {
jsonData := `{
"time": {"years": [1, 2]},
"adoption": {
"total_orgs": {"1": 100, "2": 200},
"paying_share": {"1": 0.5, "2": 0.6}
},
"pricing": {
"basic": 30.0,
"business": 100.0,
"enterprise": 300.0
}
}`
p, err := LoadFromJSON([]byte(jsonData))
require.NoError(t, err, "Should load JSON successfully")
assert.Equal(t, []int{1, 2}, p.Time.Years, "Years should be loaded from JSON")
assert.Equal(t, 100, p.Adoption.TotalOrgs.GetYear(1), "Total orgs should be loaded from JSON")
assert.Equal(t, 0.5, p.Adoption.PayingShare.GetYear(1), "Paying share should be loaded from JSON")
assert.Equal(t, 30.0, p.Pricing.Basic, "Basic price should be loaded from JSON")
}
// Test LoadFromYAML function
func TestLoadFromYAML(t *testing.T) {
yamlData := `
time:
years: [1, 2]
adoption:
total_orgs:
"1": 150
"2": 250
paying_share:
"1": 0.4
"2": 0.5
pricing:
basic: 40.0
business: 130.0
enterprise: 350.0
`
p, err := LoadFromYAML([]byte(yamlData))
require.NoError(t, err, "Should load YAML successfully")
assert.Equal(t, []int{1, 2}, p.Time.Years, "Years should be loaded from YAML")
assert.Equal(t, 150, p.Adoption.TotalOrgs.GetYear(1), "Total orgs should be loaded from YAML")
assert.Equal(t, 0.4, p.Adoption.PayingShare.GetYear(1), "Paying share should be loaded from YAML")
assert.Equal(t, 40.0, p.Pricing.Basic, "Basic price should be loaded from YAML")
}
// Test ErrInvalidParams error formatting
func TestErrInvalidParams_Error(t *testing.T) {
err := ErrInvalidParams{
Field: "test.field",
Message: "test message",
}
errorMsg := err.Error()
assert.Contains(t, errorMsg, "invalid params", "Should contain error prefix")
assert.Contains(t, errorMsg, "test.field", "Should contain field name")
assert.Contains(t, errorMsg, "test message", "Should contain message")
}
// Test yearKey function
func TestYearKey(t *testing.T) {
assert.Equal(t, "1", yearKey(1), "Year 1 should convert to string '1'")
assert.Equal(t, "42", yearKey(42), "Year 42 should convert to string '42'")
assert.Equal(t, "0", yearKey(0), "Year 0 should convert to string '0'")
}
// Test GetYear methods with missing keys to ensure 100% coverage
func TestGetYear_Methods_MissingKeys(t *testing.T) {
// Test YearlyInt.GetYear with missing key
yi := YearlyInt{"1": 100, "2": 200}
assert.Equal(t, 100, yi.GetYear(1), "Should return value for existing key")
assert.Equal(t, 200, yi.GetYear(2), "Should return value for existing key")
assert.Equal(t, 0, yi.GetYear(3), "Should return 0 for missing key")
// Test YearlyFloat.GetYear with missing key
yf := YearlyFloat{"1": 100.5, "2": 200.5}
assert.Equal(t, 100.5, yf.GetYear(1), "Should return value for existing key")
assert.Equal(t, 200.5, yf.GetYear(2), "Should return value for existing key")
assert.Equal(t, 0.0, yf.GetYear(3), "Should return 0.0 for missing key")
// Test YearlyTierMix.GetYear with missing key
ytm := YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.5, 0.3, 0.2}}
tier1 := ytm.GetYear(1)
assert.Equal(t, 0.6, tier1.Basic, "Should return value for existing key")
assert.Equal(t, 0.3, tier1.Business, "Should return value for existing key")
assert.Equal(t, 0.1, tier1.Enterprise, "Should return value for existing key")
tier2 := ytm.GetYear(2)
assert.Equal(t, 0.5, tier2.Basic, "Should return value for existing key")
assert.Equal(t, 0.3, tier2.Business, "Should return value for existing key")
assert.Equal(t, 0.2, tier2.Enterprise, "Should return value for existing key")
tier3 := ytm.GetYear(3)
assert.Equal(t, TierMix{}, tier3, "Should return empty TierMix for missing key")
}
// Test Params.Validate method thoroughly
func TestParams_Validate(t *testing.T) {
tests := []struct {
name string
params *Params
expectError bool
errorField string
}{
{
name: "valid params",
params: createValidParams(),
expectError: false,
},
{
name: "no years",
params: &Params{
Time: TimeParams{Years: []int{}},
},
expectError: true,
errorField: "time.years",
},
{
name: "missing total orgs for year 1",
params: &Params{
Time: TimeParams{Years: []int{1, 2, 3}},
Adoption: AdoptionParams{
TotalOrgs: YearlyInt{"2": 2000, "3": 5000}, // Missing year 1
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
},
},
expectError: true,
errorField: "adoption.total_orgs",
},
{
name: "missing paying share for year 2",
params: &Params{
Time: TimeParams{Years: []int{1, 2, 3}},
Adoption: AdoptionParams{
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
PayingShare: YearlyFloat{"1": 0.3, "3": 0.3}, // Missing year 2
},
Pricing: PricingParams{
Basic: 35.0,
Business: 120.0,
Enterprise: 400.0,
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
},
Transactions: TransactionParams{
AvgIntroFee: 550.0,
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
ServiceCommission: 0.15,
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
GroupCommission: 0.04,
},
Municipal: MunicipalParams{
Cities: YearlyInt{"1": 1, "2": 2, "3": 4},
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
},
ImplServices: ImplementationParams{
MatchesPerOrg: 0.5,
PaidShare: 0.25,
AvgFee: 5000.0,
},
Impact: ImpactParams{
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
GridFactor: 0.3,
HXEff: 0.9,
Utilization: 0.7,
WaterPerOrg: 25000.0,
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
WastePerOrg: 100.0,
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
},
Costs: CostParams{
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
EngineerSalary: 100000.0,
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
},
Market: MarketParams{
TAM: 500000000000.0,
AddressableDigital: 3000000000.0,
PilotCityEconomicBenefit: 4000000.0,
ScalabilityPotential: 400000000.0,
EUIndustrialFacilities: 2100000,
EnergyWastePotential: 0.45,
ResourceCostReduction: 0.25,
ViableExchangeRate: 0.15,
PlatformCaptureRate: 0.50,
SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
},
},
expectError: true,
errorField: "adoption.paying_share",
},
{
name: "missing pricing tier mix for year 1",
params: &Params{
Time: TimeParams{Years: []int{1, 2, 3}},
Adoption: AdoptionParams{
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
},
Pricing: PricingParams{
Basic: 35.0,
Business: 120.0,
Enterprise: 400.0,
TierMix: YearlyTierMix{"2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}}, // Missing year 1
},
Transactions: TransactionParams{
AvgIntroFee: 550.0,
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
ServiceCommission: 0.15,
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
GroupCommission: 0.04,
},
Municipal: MunicipalParams{
Cities: YearlyInt{"1": 1, "2": 2, "3": 4},
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
},
ImplServices: ImplementationParams{
MatchesPerOrg: 0.5,
PaidShare: 0.25,
AvgFee: 5000.0,
},
Impact: ImpactParams{
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
GridFactor: 0.3,
HXEff: 0.9,
Utilization: 0.7,
WaterPerOrg: 25000.0,
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
WastePerOrg: 100.0,
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
},
Costs: CostParams{
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
EngineerSalary: 100000.0,
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
},
Market: MarketParams{
TAM: 500000000000.0,
AddressableDigital: 3000000000.0,
PilotCityEconomicBenefit: 4000000.0,
ScalabilityPotential: 400000000.0,
EUIndustrialFacilities: 2100000,
EnergyWastePotential: 0.45,
ResourceCostReduction: 0.25,
ViableExchangeRate: 0.15,
PlatformCaptureRate: 0.50,
SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
},
},
expectError: true,
errorField: "pricing.tier_mix",
},
{
name: "missing transaction intros for year 1",
params: &Params{
Time: TimeParams{Years: []int{1, 2, 3}},
Adoption: AdoptionParams{
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
},
Pricing: PricingParams{
Basic: 35.0,
Business: 120.0,
Enterprise: 400.0,
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
},
Transactions: TransactionParams{
AvgIntroFee: 550.0,
IntrosPerYear: YearlyInt{"2": 400, "3": 600}, // Missing year 1
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
ServiceCommission: 0.15,
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
GroupCommission: 0.04,
},
Impact: ImpactParams{
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
GridFactor: 0.3,
HXEff: 0.9,
Utilization: 0.7,
WaterPerOrg: 25000.0,
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
WastePerOrg: 100.0,
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
},
Costs: CostParams{
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
EngineerSalary: 100000.0,
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
},
Market: MarketParams{
TAM: 500000000000.0,
AddressableDigital: 3000000000.0,
PilotCityEconomicBenefit: 4000000.0,
ScalabilityPotential: 400000000.0,
EUIndustrialFacilities: 2100000,
EnergyWastePotential: 0.45,
ResourceCostReduction: 0.25,
ViableExchangeRate: 0.15,
PlatformCaptureRate: 0.50,
SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
},
},
expectError: true,
errorField: "transactions.intros_per_year",
},
{
name: "missing municipal cities for year 2",
params: &Params{
Time: TimeParams{Years: []int{1, 2, 3}},
Adoption: AdoptionParams{
TotalOrgs: YearlyInt{"1": 500, "2": 2000, "3": 5000},
PayingShare: YearlyFloat{"1": 0.3, "2": 0.3, "3": 0.3},
},
Pricing: PricingParams{
Basic: 35.0,
Business: 120.0,
Enterprise: 400.0,
TierMix: YearlyTierMix{"1": {0.6, 0.3, 0.1}, "2": {0.6, 0.3, 0.1}, "3": {0.6, 0.3, 0.1}},
},
Transactions: TransactionParams{
AvgIntroFee: 550.0,
IntrosPerYear: YearlyInt{"1": 200, "2": 400, "3": 600},
IntroConversion: YearlyFloat{"1": 0.35, "2": 0.38, "3": 0.40},
ServiceGMV: YearlyFloat{"1": 300000, "2": 800000, "3": 1500000},
ServiceCommission: 0.15,
GroupGMV: YearlyFloat{"1": 200000, "2": 400000, "3": 800000},
GroupCommission: 0.04,
},
Municipal: MunicipalParams{
Cities: YearlyInt{"1": 1, "3": 4}, // Missing year 2
AvgLicense: YearlyFloat{"1": 60000, "2": 90000, "3": 110000},
DataLicensing: YearlyFloat{"1": 0, "2": 50000, "3": 150000},
},
ImplServices: ImplementationParams{
MatchesPerOrg: 0.5,
PaidShare: 0.25,
AvgFee: 5000.0,
},
Impact: ImpactParams{
HeatMWh: YearlyFloat{"1": 500000, "2": 1500000, "3": 3000000},
GridFactor: 0.3,
HXEff: 0.9,
Utilization: 0.7,
WaterPerOrg: 25000.0,
WaterReuseRate: YearlyFloat{"1": 0.20, "2": 0.25, "3": 0.30},
WastePerOrg: 100.0,
WasteDiversionRate: YearlyFloat{"1": 0.15, "2": 0.25, "3": 0.35},
},
Costs: CostParams{
Engineers: YearlyInt{"1": 8, "2": 12, "3": 15},
EngineerSalary: 100000.0,
Infrastructure: YearlyFloat{"1": 200000, "2": 250000, "3": 400000},
MarketingSales: YearlyFloat{"1": 300000, "2": 600000, "3": 900000},
Operations: YearlyFloat{"1": 100000, "2": 150000, "3": 200000},
},
Market: MarketParams{
TAM: 500000000000.0,
AddressableDigital: 3000000000.0,
PilotCityEconomicBenefit: 4000000.0,
ScalabilityPotential: 400000000.0,
EUIndustrialFacilities: 2100000,
EnergyWastePotential: 0.45,
ResourceCostReduction: 0.25,
ViableExchangeRate: 0.15,
PlatformCaptureRate: 0.50,
SOM: YearlyFloat{"1": 50000000, "2": 300000000, "3": 1500000000},
},
},
expectError: true,
errorField: "municipal.cities",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.params.Validate()
if tt.expectError {
assert.Error(t, err, "Should return validation error")
assert.IsType(t, ErrInvalidParams{}, err, "Should be ErrInvalidParams")
errInvalid, ok := err.(ErrInvalidParams)
if !ok {
t.Fatalf("expected ErrInvalidParams, got %T", err)
}
assert.Contains(t, errInvalid.Field, tt.errorField, "Error field should match")
} else {
assert.NoError(t, err, "Should not return validation error")
}
})
}
}