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

398 lines
13 KiB
Go

package service_test
import (
"context"
"database/sql"
"encoding/json"
"os"
"path/filepath"
"testing"
"bugulma/backend/internal/domain"
"bugulma/backend/internal/repository"
"bugulma/backend/internal/service"
"bugulma/backend/internal/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/gorm"
)
type GeographicalDataMigrationServiceTestSuite struct {
suite.Suite
db *gorm.DB
siteRepo domain.SiteRepository
geoRepo domain.GeographicalFeatureRepository
sqliteDB *sql.DB
tempDir string
migrationSvc *service.GeographicalDataMigrationService
}
func (suite *GeographicalDataMigrationServiceTestSuite) SetupTest() {
suite.db = testutils.SetupTestDB(suite.T())
suite.siteRepo = repository.NewSiteRepository(suite.db)
suite.geoRepo = repository.NewGeographicalFeatureRepository(suite.db)
// Create temporary directory and SQLite database
suite.tempDir = suite.T().TempDir()
sqlitePath := filepath.Join(suite.tempDir, "test_data.db")
// Create test SQLite database with sample data
err := suite.createTestSQLiteDB(sqlitePath)
suite.Require().NoError(err)
// Create migration service
suite.migrationSvc, err = service.NewGeographicalDataMigrationService(
suite.db, suite.geoRepo, suite.siteRepo, sqlitePath,
)
suite.Require().NoError(err)
// Create test organization
org := &domain.Organization{ID: "org-migration-test", Name: "Migration Test Organization"}
err = repository.NewOrganizationRepository(suite.db).Create(context.Background(), org)
suite.Require().NoError(err)
}
func (suite *GeographicalDataMigrationServiceTestSuite) TearDownTest() {
if suite.migrationSvc != nil {
suite.migrationSvc.Close()
}
os.RemoveAll(suite.tempDir)
}
func TestGeographicalDataMigrationService(t *testing.T) {
suite.Run(t, new(GeographicalDataMigrationServiceTestSuite))
}
func (suite *GeographicalDataMigrationServiceTestSuite) createTestSQLiteDB(dbPath string) error {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return err
}
defer db.Close()
// Create test tables
createTablesSQL := `
CREATE TABLE osm_features (
id TEXT PRIMARY KEY,
osm_type TEXT,
osm_id TEXT,
feature_type TEXT,
geometry TEXT,
properties TEXT
);
CREATE TABLE osm_buildings (
id TEXT PRIMARY KEY,
osm_type TEXT,
osm_id TEXT,
building_type TEXT,
geometry TEXT,
properties TEXT
);
CREATE TABLE osm_roads (
id TEXT PRIMARY KEY,
osm_type TEXT,
osm_id TEXT,
road_type TEXT,
geometry TEXT,
properties TEXT
);
CREATE TABLE osm_green_spaces (
id TEXT PRIMARY KEY,
osm_type TEXT,
osm_id TEXT,
green_space_type TEXT,
geometry TEXT,
properties TEXT
);
`
_, err = db.Exec(createTablesSQL)
if err != nil {
return err
}
// Insert test data
testData := []struct {
table string
data []map[string]interface{}
}{
{
table: "osm_features",
data: []map[string]interface{}{
{
"id": "building-1", "osm_type": "way", "osm_id": "1001", "feature_type": "building",
"geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.41, 52.5], [13.41, 52.51], [13.4, 52.51], [13.4, 52.5]]]}`,
"properties": `{"building": "office", "name": "Test Office Building"}`,
},
{
"id": "road-1", "osm_type": "way", "osm_id": "2001", "feature_type": "road",
"geometry": `{"type": "LineString", "coordinates": [[13.4, 52.5], [13.42, 52.5]]}`,
"properties": `{"highway": "primary", "name": "Test Road"}`,
},
{
"id": "park-1", "osm_type": "way", "osm_id": "3001", "feature_type": "green_space",
"geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.405, 52.5], [13.405, 52.505], [13.4, 52.505], [13.4, 52.5]]]}`,
"properties": `{"leisure": "park", "name": "Test Park"}`,
},
},
},
{
table: "osm_buildings",
data: []map[string]interface{}{
{
"id": "building-1", "osm_type": "way", "osm_id": "1001", "building_type": "office",
"geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.41, 52.5], [13.41, 52.51], [13.4, 52.51], [13.4, 52.5]]]}`,
"properties": `{"building": "office", "name": "Test Office Building", "levels": 5}`,
},
},
},
{
table: "osm_roads",
data: []map[string]interface{}{
{
"id": "road-1", "osm_type": "way", "osm_id": "2001", "road_type": "primary",
"geometry": `{"type": "LineString", "coordinates": [[13.4, 52.5], [13.42, 52.5]]}`,
"properties": `{"highway": "primary", "name": "Test Road", "surface": "asphalt"}`,
},
},
},
{
table: "osm_green_spaces",
data: []map[string]interface{}{
{
"id": "park-1", "osm_type": "way", "osm_id": "3001", "green_space_type": "park",
"geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.405, 52.5], [13.405, 52.505], [13.4, 52.505], [13.4, 52.5]]]}`,
"properties": `{"leisure": "park", "name": "Test Park", "area": "small"}`,
},
},
},
}
for _, tableData := range testData {
for _, record := range tableData.data {
columns := []string{"id", "osm_type", "osm_id", "feature_type", "geometry", "properties"}
if tableData.table != "osm_features" {
switch tableData.table {
case "osm_buildings":
columns = []string{"id", "osm_type", "osm_id", "building_type", "geometry", "properties"}
case "osm_roads":
columns = []string{"id", "osm_type", "osm_id", "road_type", "geometry", "properties"}
case "osm_green_spaces":
columns = []string{"id", "osm_type", "osm_id", "green_space_type", "geometry", "properties"}
}
}
values := make([]interface{}, len(columns))
for i, col := range columns {
values[i] = record[col]
}
placeholders := "?, ?, ?, ?, ?, ?"
query := "INSERT INTO " + tableData.table + " (" + columns[0]
for i := 1; i < len(columns); i++ {
query += ", " + columns[i]
}
query += ") VALUES (" + placeholders[:len(placeholders)-len(", ?")] + placeholders[len(placeholders)-3:] + ")"
_, err := db.Exec(query, values...)
if err != nil {
return err
}
}
}
return nil
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestNewGeographicalDataMigrationService() {
assert.NotNil(suite.T(), suite.migrationSvc)
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrateBuildingPolygons() {
progress, err := suite.migrationSvc.MigrateBuildingPolygons(context.Background())
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), progress)
assert.Equal(suite.T(), "Migrating Building Polygons", progress.CurrentOperation)
assert.Greater(suite.T(), progress.TotalRecords, 0)
// Verify buildings were migrated
buildings, err := suite.geoRepo.GetByType(context.Background(), domain.GeographicalFeatureTypeLandUse)
assert.NoError(suite.T(), err)
assert.Len(suite.T(), buildings, progress.Successful)
// Verify building data
if len(buildings) > 0 {
building := buildings[0]
assert.Equal(suite.T(), domain.GeographicalFeatureTypeLandUse, building.FeatureType)
assert.Equal(suite.T(), "way", building.OSMType)
assert.NotEmpty(suite.T(), building.Properties)
var props map[string]interface{}
err := json.Unmarshal(building.Properties, &props)
assert.NoError(suite.T(), err)
assert.Contains(suite.T(), props, "building")
assert.Contains(suite.T(), props, "name")
}
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrateRoadNetwork() {
progress, err := suite.migrationSvc.MigrateRoadNetwork(context.Background())
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), progress)
assert.Equal(suite.T(), "Migrating Road Network", progress.CurrentOperation)
assert.Greater(suite.T(), progress.TotalRecords, 0)
// Verify roads were migrated
roads, err := suite.geoRepo.GetByType(context.Background(), domain.GeographicalFeatureTypeRoad)
assert.NoError(suite.T(), err)
assert.Len(suite.T(), roads, progress.Successful)
// Verify road data
if len(roads) > 0 {
road := roads[0]
assert.Equal(suite.T(), domain.GeographicalFeatureTypeRoad, road.FeatureType)
assert.Equal(suite.T(), "way", road.OSMType)
var props map[string]interface{}
err := json.Unmarshal(road.Properties, &props)
assert.NoError(suite.T(), err)
assert.Contains(suite.T(), props, "highway")
}
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrateGreenSpaces() {
progress, err := suite.migrationSvc.MigrateGreenSpaces(context.Background())
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), progress)
assert.Equal(suite.T(), "Migrating Green Spaces", progress.CurrentOperation)
assert.Greater(suite.T(), progress.TotalRecords, 0)
// Verify green spaces were migrated
greenSpaces, err := suite.geoRepo.GetByType(context.Background(), domain.GeographicalFeatureTypeGreenSpace)
assert.NoError(suite.T(), err)
assert.Len(suite.T(), greenSpaces, progress.Successful)
// Verify green space data
if len(greenSpaces) > 0 {
greenSpace := greenSpaces[0]
assert.Equal(suite.T(), domain.GeographicalFeatureTypeGreenSpace, greenSpace.FeatureType)
assert.Equal(suite.T(), "way", greenSpace.OSMType)
var props map[string]interface{}
err := json.Unmarshal(greenSpace.Properties, &props)
assert.NoError(suite.T(), err)
assert.Contains(suite.T(), props, "leisure")
assert.Equal(suite.T(), "park", props["leisure"])
}
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestGenerateMigrationStatistics() {
// Run migrations first
suite.migrationSvc.MigrateBuildingPolygons(context.Background())
suite.migrationSvc.MigrateRoadNetwork(context.Background())
suite.migrationSvc.MigrateGreenSpaces(context.Background())
stats, err := suite.migrationSvc.GetMigrationStatistics(context.Background())
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), stats)
// Verify expected statistics
assert.Contains(suite.T(), stats, "sites")
assert.Contains(suite.T(), stats, "green_space_total_area_km2")
assert.Contains(suite.T(), stats, "road_network")
// Check road network statistics structure
roadStats, ok := stats["road_network"].(map[string]interface{})
assert.True(suite.T(), ok)
assert.Contains(suite.T(), roadStats, "total_roads")
assert.Contains(suite.T(), roadStats, "total_length_km")
assert.Contains(suite.T(), roadStats, "avg_length_km")
assert.Contains(suite.T(), roadStats, "max_length_km")
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrationProgress_Tracking() {
progress, err := suite.migrationSvc.MigrateBuildingPolygons(context.Background())
assert.NoError(suite.T(), err)
// Verify progress tracking
assert.GreaterOrEqual(suite.T(), progress.ProcessedRecords, 0)
assert.GreaterOrEqual(suite.T(), progress.Successful, 0)
assert.GreaterOrEqual(suite.T(), progress.Failed, 0)
assert.Equal(suite.T(), progress.Successful+progress.Failed, progress.ProcessedRecords)
assert.GreaterOrEqual(suite.T(), progress.ProgressPercent, 0.0)
assert.LessOrEqual(suite.T(), progress.ProgressPercent, 100.0)
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrationWithSiteMatching() {
// Create a site that matches a building ID
site := &domain.Site{
ID: "building-1", // Same ID as test building
Name: "Matching Site",
Latitude: 52.505,
Longitude: 13.405,
SiteType: domain.SiteTypeIndustrial,
OwnerOrganizationID: "org-migration-test",
}
err := suite.siteRepo.Create(context.Background(), site)
suite.Require().NoError(err)
// Run building migration
progress, err := suite.migrationSvc.MigrateBuildingPolygons(context.Background())
assert.NoError(suite.T(), err)
assert.True(suite.T(), progress.Successful > 0)
// Verify the site still exists (geometry is stored at database level, not in struct)
updatedSite, err := suite.siteRepo.GetByID(context.Background(), "building-1")
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), updatedSite)
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestClose() {
err := suite.migrationSvc.Close()
assert.NoError(suite.T(), err)
// Should be able to close multiple times without error
err = suite.migrationSvc.Close()
assert.NoError(suite.T(), err)
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrationErrorHandling() {
// Test with invalid SQLite path (service should handle this gracefully)
invalidSvc, err := service.NewGeographicalDataMigrationService(
suite.db, suite.geoRepo, suite.siteRepo, "/nonexistent/path.db",
)
assert.Error(suite.T(), err)
assert.Nil(suite.T(), invalidSvc)
}
func (suite *GeographicalDataMigrationServiceTestSuite) TestEmptyMigration() {
// Create a service with empty SQLite database
emptyDBPath := filepath.Join(suite.tempDir, "empty.db")
emptyDB, err := sql.Open("sqlite3", emptyDBPath)
suite.Require().NoError(err)
// Create empty table
_, err = emptyDB.Exec(`CREATE TABLE osm_features (id TEXT, osm_type TEXT, osm_id TEXT, feature_type TEXT, geometry TEXT, properties TEXT)`)
suite.Require().NoError(err)
emptyDB.Close()
emptySvc, err := service.NewGeographicalDataMigrationService(
suite.db, suite.geoRepo, suite.siteRepo, emptyDBPath,
)
suite.Require().NoError(err)
defer emptySvc.Close()
// Run migration on empty data
progress, err := emptySvc.MigrateBuildingPolygons(context.Background())
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), 0, progress.TotalRecords)
assert.Equal(suite.T(), 0, progress.Successful)
assert.Equal(suite.T(), 0, progress.Failed)
assert.Equal(suite.T(), 100.0, progress.ProgressPercent)
}