turash/bugulma/backend/internal/service/facility_location_optimizer_test.go
Damir Mukimov 44f34ec181
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 1m1s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Successful in 1m37s
CI/CD Pipeline / frontend-build (push) Failing after 35s
CI/CD Pipeline / e2e-test (push) Has been skipped
Refactor testing setup to utilize Testcontainers for PostgreSQL isolation
- Replace pgtestdb with Testcontainers for improved test isolation and reliability
- Update test setup functions to spin up dedicated PostgreSQL containers for each test
- Ensure automatic cleanup of containers after tests to prevent resource leaks
- Modify documentation to reflect changes in testing methodology and benefits of using Testcontainers
2025-12-26 15:40:43 +01:00

326 lines
11 KiB
Go

package service_test
import (
"context"
"testing"
"bugulma/backend/internal/domain"
"bugulma/backend/internal/geospatial"
"bugulma/backend/internal/repository"
"bugulma/backend/internal/service"
"bugulma/backend/internal/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type FacilityLocationOptimizerTestSuite struct {
suite.Suite
db *gorm.DB
siteRepo domain.SiteRepository
geoRepo domain.GeographicalFeatureRepository
geoSvc *service.GeospatialService
spatialMatcher *service.SpatialResourceMatcher
envSvc *service.EnvironmentalImpactService
transportSvc *service.TransportationService
geoCalc geospatial.Calculator
optimizer *service.FacilityLocationOptimizer
}
func (suite *FacilityLocationOptimizerTestSuite) SetupTest() {
suite.db = testutils.SetupTestDBWithTestcontainers(suite.T())
suite.siteRepo = repository.NewSiteRepository(suite.db)
suite.geoRepo = repository.NewGeographicalFeatureRepository(suite.db)
suite.geoSvc = service.NewGeospatialService(suite.db, suite.geoRepo)
suite.geoCalc = geospatial.NewCalculatorWithDefaults()
suite.transportSvc = service.NewTransportationService(suite.geoCalc)
suite.spatialMatcher = service.NewSpatialResourceMatcher(
suite.geoRepo,
suite.siteRepo,
nil, // resourceFlowRepo - not needed for basic tests
suite.geoSvc,
suite.transportSvc,
suite.geoCalc,
)
suite.envSvc = service.NewEnvironmentalImpactService(suite.geoRepo, suite.siteRepo, suite.geoSvc, suite.geoCalc)
suite.optimizer = service.NewFacilityLocationOptimizer(
suite.geoRepo,
suite.siteRepo,
suite.geoSvc,
suite.spatialMatcher,
suite.envSvc,
suite.transportSvc,
)
// Create test organization
org := &domain.Organization{ID: "org-facility-test", Name: "Facility Test Organization"}
err := repository.NewOrganizationRepository(suite.db).Create(context.Background(), org)
suite.Require().NoError(err)
}
func TestFacilityLocationOptimizer(t *testing.T) {
suite.Run(t, new(FacilityLocationOptimizerTestSuite))
}
func (suite *FacilityLocationOptimizerTestSuite) TestNewFacilityLocationOptimizer() {
assert.NotNil(suite.T(), suite.optimizer)
}
func (suite *FacilityLocationOptimizerTestSuite) setupTestSites() []*domain.Site {
sites := []*domain.Site{
{
ID: "site-industrial-good",
Name: "Good Industrial Site",
Latitude: 52.5200,
Longitude: 13.4050,
SiteType: domain.SiteTypeIndustrial,
OwnerOrganizationID: "org-facility-test",
EnvironmentalImpact: "low_impact",
AvailableUtilities: datatypes.JSON(`["electricity", "gas", "water", "rail_access"]`),
ParkingSpaces: 50,
LoadingDocks: 5,
FloorAreaM2: 5000.0,
},
{
ID: "site-office-poor",
Name: "Poor Office Site",
Latitude: 52.5300,
Longitude: 13.4150,
SiteType: domain.SiteTypeOffice,
OwnerOrganizationID: "org-facility-test",
EnvironmentalImpact: "high_impact",
AvailableUtilities: datatypes.JSON(`["electricity"]`),
ParkingSpaces: 10,
LoadingDocks: 1,
FloorAreaM2: 1000.0,
},
{
ID: "site-commercial-excellent",
Name: "Excellent Commercial Site",
Latitude: 52.5400,
Longitude: 13.4250,
SiteType: domain.SiteTypeRetail,
OwnerOrganizationID: "org-facility-test",
EnvironmentalImpact: "eco_friendly",
AvailableUtilities: datatypes.JSON(`["electricity", "gas", "water", "heating", "cooling", "pipeline_access"]`),
ParkingSpaces: 100,
LoadingDocks: 10,
FloorAreaM2: 10000.0,
},
}
for _, site := range sites {
err := suite.siteRepo.Create(context.Background(), site)
suite.Require().NoError(err)
}
return sites
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_BasicOptimization() {
suite.setupTestSites()
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{domain.TypeHeat},
ResourceRadiusKm: 50.0,
MaxTransportCost: 200.0,
MinEnvironmentalScore: 0.0,
RequiredUtilities: []string{"electricity"},
MinFloorAreaM2: 1000.0,
MaxResults: 5,
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
assert.NotEmpty(suite.T(), results)
assert.LessOrEqual(suite.T(), len(results), criteria.MaxResults)
// Verify results are sorted by score (highest first)
for i := 0; i < len(results)-1; i++ {
assert.GreaterOrEqual(suite.T(), results[i].Score.OverallScore, results[i+1].Score.OverallScore)
}
// Verify all results meet minimum criteria
for _, result := range results {
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0) // Max possible score
assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, criteria.MinEnvironmentalScore)
}
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_WithUtilityRequirements() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{domain.TypeHeat},
ResourceRadiusKm: 100.0,
MaxTransportCost: 500.0,
MinEnvironmentalScore: 0.0,
RequiredUtilities: []string{"gas", "water", "rail_access"}, // Very specific requirements
MinFloorAreaM2: 1500.0,
MaxResults: 10,
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
assert.NotEmpty(suite.T(), results)
// Check that results have reasonable scores
for _, result := range results {
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0)
}
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_EnvironmentalFiltering() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{domain.TypeHeat},
ResourceRadiusKm: 100.0,
MaxTransportCost: 500.0,
MinEnvironmentalScore: 7.0, // High environmental requirement
RequiredUtilities: []string{"electricity"},
MinFloorAreaM2: 1000.0,
MaxResults: 10,
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
// Only sites meeting environmental criteria should be returned
for _, result := range results {
assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, 7.0, "All results should meet environmental criteria")
}
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_SiteTypeFiltering() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{domain.TypeHeat},
ResourceRadiusKm: 100.0,
MaxTransportCost: 500.0,
MinEnvironmentalScore: 0.0,
RequiredUtilities: []string{"electricity"},
MinFloorAreaM2: 1000.0,
MaxResults: 10,
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
// Check that results have reasonable scores
for _, result := range results {
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0)
}
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_NoMatchingSites() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{domain.TypeHeat},
ResourceRadiusKm: 100.0,
MaxTransportCost: 500.0,
MinEnvironmentalScore: 9.5, // Very high requirement
RequiredUtilities: []string{"nonexistent_utility"}, // Impossible requirement
MinFloorAreaM2: 1000.0,
MaxResults: 10,
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
assert.Empty(suite.T(), results, "No sites should match impossible criteria")
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_EmptyCriteria() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{},
ResourceRadiusKm: 0.0,
MaxTransportCost: 0.0,
MinEnvironmentalScore: 0.0,
RequiredUtilities: []string{},
MinFloorAreaM2: 1000.0,
MaxResults: 10,
Weights: service.DefaultWeights, // No limit
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
// Check that we get some results
assert.Greater(suite.T(), len(results), 0, "Should return some results")
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_MaxResults() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{},
ResourceRadiusKm: 100.0,
MaxTransportCost: 1000.0,
MinEnvironmentalScore: 0.0,
RequiredUtilities: []string{},
MinFloorAreaM2: 1000.0,
MaxResults: 2, // Limit to 2 results
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
assert.Len(suite.T(), results, 2, "Should return exactly MaxResults items")
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_ScoreCalculation() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{domain.TypeHeat},
ResourceRadiusKm: 50.0,
MaxTransportCost: 200.0,
MinEnvironmentalScore: 3.0,
RequiredUtilities: []string{"electricity"},
MinFloorAreaM2: 1000.0,
MaxResults: 10,
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
for _, result := range results {
// Verify score components are present
assert.GreaterOrEqual(suite.T(), result.Score.TransportationScore, 0.0)
assert.LessOrEqual(suite.T(), result.Score.TransportationScore, 10.0)
assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, 0.0)
assert.LessOrEqual(suite.T(), result.Score.EnvironmentalScore, 10.0)
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0)
}
}
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_Explanations() {
criteria := service.LocationCriteria{
RequiredResources: []domain.ResourceType{domain.TypeHeat},
ResourceRadiusKm: 50.0,
MaxTransportCost: 200.0,
MinEnvironmentalScore: 5.0,
RequiredUtilities: []string{"electricity", "gas"},
MinFloorAreaM2: 1000.0,
MaxResults: 10,
Weights: service.DefaultWeights,
}
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
assert.NoError(suite.T(), err)
// Check that results have valid scores
for _, result := range results {
assert.NotNil(suite.T(), result.Score, "Result should have a score")
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
}
}