turash/bugulma/backend/internal/service/environmental_impact_service_test.go

271 lines
10 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 EnvironmentalImpactServiceTestSuite struct {
suite.Suite
db *gorm.DB
siteRepo domain.SiteRepository
geoRepo domain.GeographicalFeatureRepository
geoCalc geospatial.Calculator
geospatialSvc *service.GeospatialService
envSvc *service.EnvironmentalImpactService
}
func (suite *EnvironmentalImpactServiceTestSuite) SetupTest() {
suite.db = testutils.SetupTestDB(suite.T())
suite.siteRepo = repository.NewSiteRepository(suite.db)
suite.geoRepo = repository.NewGeographicalFeatureRepository(suite.db)
suite.geospatialSvc = service.NewGeospatialService(suite.db, suite.geoRepo)
suite.geoCalc = geospatial.NewCalculatorWithDefaults()
suite.envSvc = service.NewEnvironmentalImpactService(suite.geoRepo, suite.siteRepo, suite.geospatialSvc, suite.geoCalc)
// Create test organization
org := &domain.Organization{ID: "org-env-test", Name: "Environmental Test Organization"}
err := repository.NewOrganizationRepository(suite.db).Create(context.Background(), org)
suite.Require().NoError(err)
}
func TestEnvironmentalImpactService(t *testing.T) {
suite.Run(t, new(EnvironmentalImpactServiceTestSuite))
}
func (suite *EnvironmentalImpactServiceTestSuite) TestNewEnvironmentalImpactService() {
assert.NotNil(suite.T(), suite.envSvc)
}
func (suite *EnvironmentalImpactServiceTestSuite) setupTestSites() []*domain.Site {
sites := []*domain.Site{
{
ID: "site-high-impact",
Name: "High Impact Industrial Site",
Latitude: 52.5200,
Longitude: 13.4050,
SiteType: domain.SiteTypeIndustrial,
OwnerOrganizationID: "org-env-test",
EnvironmentalImpact: "high_impact",
},
{
ID: "site-low-impact",
Name: "Low Impact Office Site",
Latitude: 52.5300,
Longitude: 13.4150,
SiteType: domain.SiteTypeOffice,
OwnerOrganizationID: "org-env-test",
EnvironmentalImpact: "low_impact",
},
{
ID: "site-eco-friendly",
Name: "Eco-Friendly Site",
Latitude: 52.5400,
Longitude: 13.4250,
SiteType: domain.SiteTypeRetail,
OwnerOrganizationID: "org-env-test",
EnvironmentalImpact: "eco_friendly",
},
}
for _, site := range sites {
err := suite.siteRepo.Create(context.Background(), site)
suite.Require().NoError(err)
}
return sites
}
func (suite *EnvironmentalImpactServiceTestSuite) setupTestGreenSpaces() {
// Create some mock green space features
// Note: In a real scenario, these would have actual geometry data
greenSpaces := []*domain.GeographicalFeature{
{
ID: "park-central",
Name: "Central Park",
FeatureType: domain.GeographicalFeatureTypeGreenSpace,
OSMType: "way",
OSMID: "1001",
Properties: datatypes.JSON(`{"leisure": "park", "area": "large"}`),
Source: "osm",
},
{
ID: "park-small",
Name: "Small Park",
FeatureType: domain.GeographicalFeatureTypeGreenSpace,
OSMType: "way",
OSMID: "1002",
Properties: datatypes.JSON(`{"leisure": "park", "area": "small"}`),
Source: "osm",
},
}
for i, gs := range greenSpaces {
err := suite.geoRepo.Create(context.Background(), gs)
suite.Require().NoError(err)
// Ensure the DB has a geometry for each green space so radius queries
// return them during tests. Place them near 52.5200,13.4050 with small offsets
lat := 52.5200 + float64(i)*0.001
lng := 13.4050 + float64(i)*0.001
// Update raw geometry via SQL (PostGIS required in test DB template)
if err := suite.db.Exec(`UPDATE geographical_features SET geometry = ST_SetSRID(ST_MakePoint(?::double precision, ?::double precision), 4326) WHERE id = ?`, lng, lat, gs.ID).Error; err != nil {
// If updating geometry fails (e.g., PostGIS not available), continue — tests will adapt
suite.T().Logf("warning: could not set geometry for test green space %s: %v", gs.ID, err)
}
}
}
func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_HighImpact() {
sites := suite.setupTestSites()
score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), sites[0].Latitude, sites[0].Longitude)
assert.NoError(suite.T(), err)
assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), score.OverallScore, 10.0)
// High impact sites should have lower scores
assert.Less(suite.T(), score.OverallScore, 5.0)
}
func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_LowImpact() {
sites := suite.setupTestSites()
score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), sites[1].Latitude, sites[1].Longitude)
assert.NoError(suite.T(), err)
assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), score.OverallScore, 10.0)
// Low impact sites should have higher scores
assert.Greater(suite.T(), score.OverallScore, 5.0)
}
func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_EcoFriendly() {
sites := suite.setupTestSites()
score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), sites[2].Latitude, sites[2].Longitude)
assert.NoError(suite.T(), err)
assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), score.OverallScore, 10.0)
// Eco-friendly sites should have highest scores
assert.Greater(suite.T(), score.OverallScore, 7.0)
}
func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_NonExistentSite() {
// For non-existent sites, we test with coordinates directly since the method takes coordinates
_, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), 0.0, 0.0)
// This should work since it doesn't depend on site existence, just coordinates
assert.NoError(suite.T(), err)
}
func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateSiteEnvironmentalScore_DefaultScore() {
// Create a site without environmental impact data
site := &domain.Site{
ID: "site-no-data",
Name: "Site Without Data",
Latitude: 52.5500,
Longitude: 13.4350,
SiteType: domain.SiteTypeMixed,
OwnerOrganizationID: "org-env-test",
EnvironmentalImpact: "", // No environmental data
}
err := suite.siteRepo.Create(context.Background(), site)
suite.Require().NoError(err)
score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), site.Latitude, site.Longitude)
assert.NoError(suite.T(), err)
assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0)
assert.LessOrEqual(suite.T(), score.OverallScore, 10.0)
// Sites without data should get a default score around 5.0
assert.InDelta(suite.T(), 5.0, score.OverallScore, 2.0)
}
func (suite *EnvironmentalImpactServiceTestSuite) TestEnvironmentalScoreCalculation_Components() {
// Test the scoring algorithm with different site configurations
testCases := []struct {
name string
environmentalImpact string
expectedMin float64
expectedMax float64
}{
{"High Impact", "high_impact", 0.0, 3.0},
{"Low Impact", "low_impact", 4.0, 7.0},
{"Eco Friendly", "eco_friendly", 7.0, 10.0},
{"Unknown", "unknown_type", 3.0, 6.0},
}
for i, tc := range testCases {
suite.T().Run(tc.name, func(t *testing.T) {
site := &domain.Site{
ID: "test-site-" + tc.name,
Name: tc.name + " Site",
Latitude: 52.5200 + float64(i)*0.01,
Longitude: 13.4050 + float64(i)*0.01,
SiteType: domain.SiteTypeIndustrial,
OwnerOrganizationID: "org-env-test",
EnvironmentalImpact: tc.environmentalImpact,
}
err := suite.siteRepo.Create(context.Background(), site)
assert.NoError(t, err)
// Quick check: our repository should find the site by ID
got, gerr := suite.siteRepo.GetByID(context.Background(), site.ID)
assert.NoError(t, gerr)
assert.Equal(t, tc.environmentalImpact, got.EnvironmentalImpact, "site EnvironmentalImpact persisted correctly")
score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), site.Latitude, site.Longitude)
assert.NoError(t, err)
assert.GreaterOrEqual(t, score.OverallScore, tc.expectedMin, "Score should be >= %f for %s", tc.expectedMin, tc.name)
assert.LessOrEqual(t, score.OverallScore, tc.expectedMax, "Score should be <= %f for %s", tc.expectedMax, tc.name)
})
}
}
func (suite *EnvironmentalImpactServiceTestSuite) TestEnvironmentalScoreCalculation_ScoreNormalization() {
// Test that scores are properly normalized to 0-10 range
sites := suite.setupTestSites()
for _, site := range sites {
score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), site.Latitude, site.Longitude)
suite.T().Run("ScoreNormalization_"+site.ID, func(t *testing.T) {
assert.NoError(t, err)
assert.GreaterOrEqual(t, score.OverallScore, 0.0, "Score should be >= 0")
assert.LessOrEqual(t, score.OverallScore, 10.0, "Score should be <= 10")
})
}
}
func (suite *EnvironmentalImpactServiceTestSuite) TestGreenSpaceStatistics_IncludesExpectedFields() {
suite.setupTestGreenSpaces()
// Test that green spaces are created and can be retrieved
greenSpaces, err := suite.geoRepo.GetGreenSpacesWithinRadius(context.Background(), 52.5200, 13.4050, 10.0)
assert.NoError(suite.T(), err)
// Should have at least 2 green spaces from our setup
assert.GreaterOrEqual(suite.T(), len(greenSpaces), 2, "Should have at least 2 green spaces")
}
func (suite *EnvironmentalImpactServiceTestSuite) TestEnvironmentalImpactService_HandlesEmptyDatabase() {
// Test that CalculateFacilityEnvironmentalScore works even with coordinates not near green spaces
score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), 0.0, 0.0)
assert.NoError(suite.T(), err) // Should work with any coordinates
assert.NotNil(suite.T(), score)
// Test that green space queries work with empty database
greenSpaces, err := suite.geoRepo.GetGreenSpacesWithinRadius(context.Background(), 52.5200, 13.4050, 1.0)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, len(greenSpaces))
}