mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Add comprehensive geographical data models (GeographicalFeature, TransportMode, TransportProfile, TransportOption) - Implement geographical feature repository with PostGIS support and spatial queries - Create transportation service for cost calculation and route optimization - Build spatial resource matcher for geographical resource matching - Develop environmental impact service for site environmental scoring - Implement facility location optimizer with multi-criteria analysis - Add geographical data migration service for SQLite to PostgreSQL migration - Create database migrations for geographical features and site footprints - Update geospatial service integration and server initialization - Add CLI command for geographical data synchronization - Implement complete test coverage for all geographical components (28 test cases) - Update test infrastructure for geographical table creation and PostGIS handling This implements advanced geospatial capabilities including transportation cost modeling, environmental impact assessment, and facility location optimization for the Turash platform.
352 lines
13 KiB
Go
352 lines
13 KiB
Go
package testutils
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
"bugulma/backend/internal/domain"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/peterldowns/pgtestdb"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
// GormMigrator implements pgtestdb.Migrator interface for GORM migrations
|
|
// This migrator ensures PostGIS is properly set up before running migrations
|
|
type GormMigrator struct{}
|
|
|
|
// Hash returns a unique identifier for the migration state
|
|
// This is used by pgtestdb to identify template databases
|
|
// Update the version string when migrations change to force template recreation
|
|
func (m *GormMigrator) Hash() (string, error) {
|
|
// Create a hash based on the migration state
|
|
// This should be deterministic and change when migrations change
|
|
hash := sha256.Sum256([]byte("bugulma-backend-migrations-v3"))
|
|
return hex.EncodeToString(hash[:]), nil
|
|
}
|
|
|
|
// Migrate runs all GORM migrations including PostGIS setup
|
|
// This function is called by pgtestdb when creating template databases
|
|
// PostGIS extension MUST be created before any migrations that use geometry types
|
|
func (m *GormMigrator) Migrate(ctx context.Context, db *sql.DB, config pgtestdb.Config) error {
|
|
// Step 1: Try to enable PostGIS extension FIRST (before any migrations)
|
|
// This must be done using raw SQL connection, not GORM, to ensure it persists
|
|
// If PostGIS cannot be enabled (e.g., permission issues), we'll skip PostGIS migrations
|
|
postgisEnabled := false
|
|
if err := enablePostGISExtension(db); err != nil {
|
|
// PostGIS extension creation failed - this might happen if:
|
|
// - User doesn't have permission to create extensions
|
|
// - PostGIS is not installed in PostgreSQL
|
|
// - Template database restrictions
|
|
// We'll continue without PostGIS - tests that need it will fail with clear errors
|
|
_ = err
|
|
postgisEnabled = false
|
|
} else {
|
|
postgisEnabled = true
|
|
}
|
|
|
|
// Step 2: Create GORM connection
|
|
gormDB, err := gorm.Open(postgres.New(postgres.Config{
|
|
Conn: db,
|
|
}), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create GORM connection: %w", err)
|
|
}
|
|
|
|
// Step 3: Run domain migrations (creates tables, but not PostGIS columns)
|
|
if err := domain.AutoMigrate(gormDB); err != nil {
|
|
return fmt.Errorf("failed to run domain migrations: %w", err)
|
|
}
|
|
|
|
// Step 4: Run PostGIS-specific migrations (creates geometry columns, indexes)
|
|
// Only run if PostGIS was successfully enabled
|
|
if postgisEnabled {
|
|
if err := domain.RunPostGISMigrations(gormDB); err != nil {
|
|
// PostGIS migrations failed - this is critical for spatial operations
|
|
// Return error to fail template creation - better to fail early than have broken tests
|
|
return fmt.Errorf("failed to run PostGIS migrations: %w", err)
|
|
}
|
|
}
|
|
// If PostGIS is not enabled, skip PostGIS migrations
|
|
// Tests that need PostGIS will fail with appropriate error messages
|
|
|
|
// Step 5: Create additional indexes
|
|
if err := domain.CreateIndexes(gormDB); err != nil {
|
|
// Index creation errors are non-fatal (indexes might already exist)
|
|
// Log but don't fail - this allows migrations to be idempotent
|
|
_ = err
|
|
}
|
|
|
|
// Step 6: Run geographical feature migrations manually
|
|
// Since geographical features are created via golang-migrate, we need to run them manually
|
|
if postgisEnabled {
|
|
if err := runGeographicalFeatureMigrations(gormDB); err != nil {
|
|
// Geographical migrations are important for geo tests but not critical for core functionality
|
|
// Log the error but don't fail - tests that need geographical features will fail appropriately
|
|
_ = err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// runGeographicalFeatureMigrations runs the geographical feature table migrations
|
|
func runGeographicalFeatureMigrations(db *gorm.DB) error {
|
|
// Create geographical_features table
|
|
createTableSQL := `
|
|
CREATE TABLE IF NOT EXISTS geographical_features (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
feature_type VARCHAR(50) NOT NULL,
|
|
osm_type VARCHAR(50),
|
|
osm_id VARCHAR(50),
|
|
properties JSONB DEFAULT '{}'::jsonb,
|
|
processing_version VARCHAR(20) DEFAULT '1.0',
|
|
quality_score DOUBLE PRECISION DEFAULT 0.0,
|
|
source VARCHAR(100) DEFAULT 'osm',
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
`
|
|
|
|
if err := db.Exec(createTableSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to create geographical_features table: %w", err)
|
|
}
|
|
|
|
// Add geometry column
|
|
addGeometrySQL := `
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'geographical_features' AND column_name = 'geometry'
|
|
) THEN
|
|
ALTER TABLE geographical_features ADD COLUMN geometry GEOMETRY(Geometry, 4326);
|
|
END IF;
|
|
END $$;
|
|
`
|
|
|
|
if err := db.Exec(addGeometrySQL).Error; err != nil {
|
|
return fmt.Errorf("failed to add geometry column: %w", err)
|
|
}
|
|
|
|
// Create indexes
|
|
indexSQLs := []string{
|
|
`CREATE INDEX IF NOT EXISTS idx_geographical_features_geometry ON geographical_features USING GIST (geometry)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_geographical_features_type ON geographical_features (feature_type)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_geographical_features_osm_id ON geographical_features (osm_type, osm_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_geographical_features_properties ON geographical_features USING GIN (properties)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_geographical_features_created_at ON geographical_features (created_at)`,
|
|
}
|
|
|
|
for _, sql := range indexSQLs {
|
|
if err := db.Exec(sql).Error; err != nil {
|
|
return fmt.Errorf("failed to create index: %w", err)
|
|
}
|
|
}
|
|
|
|
// Add site footprint geometry column
|
|
addFootprintSQL := `
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = 'sites' AND column_name = 'footprint_geometry'
|
|
) THEN
|
|
ALTER TABLE sites ADD COLUMN footprint_geometry geometry(Polygon, 4326);
|
|
CREATE INDEX IF NOT EXISTS idx_sites_footprint_geometry ON sites USING GIST (footprint_geometry);
|
|
END IF;
|
|
END $$;
|
|
`
|
|
|
|
if err := db.Exec(addFootprintSQL).Error; err != nil {
|
|
return fmt.Errorf("failed to add footprint geometry column: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// enablePostGISExtension enables the PostGIS extension in the database
|
|
// This must be done using raw SQL connection to ensure it persists
|
|
// PostGIS extension is required for spatial operations and geometry types
|
|
// The extension must be created BEFORE any migrations that use geometry types
|
|
func enablePostGISExtension(db *sql.DB) error {
|
|
// Always try to create the extension (IF NOT EXISTS handles existing case)
|
|
// This ensures PostGIS is available even if the database was cloned without extensions
|
|
if _, err := db.Exec("CREATE EXTENSION IF NOT EXISTS postgis"); err != nil {
|
|
return fmt.Errorf("failed to create PostGIS extension: %w", err)
|
|
}
|
|
|
|
// Verify PostGIS is properly initialized by checking for PostGIS functions
|
|
var version string
|
|
if err := db.QueryRow("SELECT PostGIS_Version()").Scan(&version); err != nil {
|
|
return fmt.Errorf("PostGIS extension exists but functions are not available: %w", err)
|
|
}
|
|
|
|
// PostGIS is properly initialized
|
|
return nil
|
|
}
|
|
|
|
// getEnv gets environment variable or returns default value
|
|
func getEnv(key, defaultValue string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
// ginkgoTBWrapper wraps Ginkgo's FullGinkgoTInterface to work with pgtestdb
|
|
// pgtestdb requires a testing.TB interface, but Ginkgo provides a different interface
|
|
type ginkgoTBWrapper struct {
|
|
t interface {
|
|
Helper()
|
|
Logf(format string, args ...interface{})
|
|
Fatalf(format string, args ...interface{})
|
|
Failed() bool
|
|
}
|
|
cleanups []func()
|
|
}
|
|
|
|
func (w *ginkgoTBWrapper) Helper() { w.t.Helper() }
|
|
func (w *ginkgoTBWrapper) Logf(format string, args ...interface{}) { w.t.Logf(format, args...) }
|
|
func (w *ginkgoTBWrapper) Fatalf(format string, args ...interface{}) { w.t.Fatalf(format, args...) }
|
|
func (w *ginkgoTBWrapper) Failed() bool { return w.t.Failed() }
|
|
func (w *ginkgoTBWrapper) Cleanup(fn func()) { w.cleanups = append(w.cleanups, fn) }
|
|
|
|
// SetupTestDB creates an isolated PostgreSQL database for testing using pgtestdb
|
|
// Each test gets its own temporary database with migrations already applied
|
|
// This function accepts testing.TB interface, compatible with *testing.T
|
|
//
|
|
// Example usage:
|
|
//
|
|
// func TestMyFeature(t *testing.T) {
|
|
// db := testutils.SetupTestDB(t)
|
|
// repo := repository.NewMyRepository(db)
|
|
// // Your test code here
|
|
// }
|
|
func SetupTestDB(t testing.TB) *gorm.DB {
|
|
return setupTestDBWithTB(t)
|
|
}
|
|
|
|
// SetupTestDBForGinkgo creates an isolated PostgreSQL database for Ginkgo tests
|
|
// Use this function in Ginkgo BeforeEach blocks
|
|
//
|
|
// Example usage:
|
|
//
|
|
// BeforeEach(func() {
|
|
// db = testutils.SetupTestDBForGinkgo(GinkgoT())
|
|
// repo = repository.NewMyRepository(db)
|
|
// })
|
|
func SetupTestDBForGinkgo(ginkgoT interface {
|
|
Helper()
|
|
Logf(format string, args ...interface{})
|
|
Fatalf(format string, args ...interface{})
|
|
Failed() bool
|
|
}) *gorm.DB {
|
|
wrapper := &ginkgoTBWrapper{t: ginkgoT}
|
|
return setupTestDBWithTB(wrapper)
|
|
}
|
|
|
|
// setupTestDBWithTB is the internal implementation that works with both
|
|
// standard testing.T and Ginkgo's testing interface
|
|
func setupTestDBWithTB(t interface {
|
|
Helper()
|
|
Logf(format string, args ...interface{})
|
|
Fatalf(format string, args ...interface{})
|
|
Failed() bool
|
|
Cleanup(func())
|
|
}) *gorm.DB {
|
|
// Configure PostgreSQL connection
|
|
// Defaults match the running Docker Compose PostgreSQL container (turash-postgres)
|
|
//
|
|
// IMPORTANT: pgtestdb creates ISOLATED test databases - it does NOT touch production data!
|
|
// - Connects to 'postgres' database (admin database) to CREATE new test databases
|
|
// - Each test gets a unique temporary database (e.g., pgtestdb_abc123)
|
|
// - Test databases are automatically DROPPED after each test completes
|
|
// - Production database 'turash' is NEVER modified or accessed
|
|
conf := pgtestdb.Config{
|
|
DriverName: "pgx",
|
|
User: getEnv("POSTGRES_USER", "turash"),
|
|
Password: getEnv("POSTGRES_PASSWORD", "turash123"),
|
|
Host: getEnv("POSTGRES_HOST", "localhost"),
|
|
Port: getEnv("POSTGRES_PORT", "5432"),
|
|
Database: getEnv("POSTGRES_DB", "postgres"), // Connect to 'postgres' database to create test databases
|
|
Options: "sslmode=disable",
|
|
}
|
|
|
|
// Create migrator instance
|
|
migrator := &GormMigrator{}
|
|
|
|
// Create isolated test database with migrations
|
|
// pgtestdb.New accepts any type that implements the TB interface methods
|
|
// It will:
|
|
// 1. Check if a template database exists (by Hash())
|
|
// 2. Create template if needed (runs Migrate function)
|
|
// 3. Clone template for this test (fast, milliseconds)
|
|
// 4. Return connection to cloned database
|
|
sqlDB := pgtestdb.New(t, conf, migrator)
|
|
|
|
// Check if PostGIS is already enabled or if geometry column exists
|
|
// If geometry column exists, PostGIS was set up in the template
|
|
var postgisEnabled bool
|
|
var columnExists bool
|
|
|
|
// Check if PostGIS extension exists
|
|
if err := sqlDB.QueryRow("SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'postgis')").Scan(&postgisEnabled); err != nil {
|
|
postgisEnabled = false
|
|
}
|
|
|
|
// Check if geometry column exists (indicates PostGIS was set up)
|
|
if err := sqlDB.QueryRow("SELECT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'sites' AND column_name = 'location_geometry')").Scan(&columnExists); err != nil {
|
|
columnExists = false
|
|
}
|
|
|
|
// If PostGIS is not enabled but column exists, try to enable PostGIS
|
|
// This handles cases where template had PostGIS but cloned database doesn't inherit it
|
|
if !postgisEnabled && columnExists {
|
|
// Try to enable PostGIS - if it fails, that's OK since column already exists
|
|
if err := enablePostGISExtension(sqlDB); err == nil {
|
|
postgisEnabled = true
|
|
}
|
|
} else if !postgisEnabled && !columnExists {
|
|
// PostGIS not enabled and column doesn't exist - try to enable PostGIS
|
|
if err := enablePostGISExtension(sqlDB); err != nil {
|
|
// Can't enable PostGIS - tests that need it will fail
|
|
t.Logf("Warning: Failed to enable PostGIS extension: %v", err)
|
|
} else {
|
|
postgisEnabled = true
|
|
}
|
|
}
|
|
|
|
// Convert to GORM DB for use in tests
|
|
gormDB, err := gorm.Open(postgres.New(postgres.Config{
|
|
Conn: sqlDB,
|
|
}), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create GORM DB: %v", err)
|
|
}
|
|
|
|
// Run PostGIS migrations if PostGIS is enabled
|
|
// This ensures geometry columns exist even if they weren't in the template
|
|
if postgisEnabled {
|
|
if err := domain.RunPostGISMigrations(gormDB); err != nil {
|
|
// PostGIS migrations failed - this is critical for spatial operations
|
|
// Fail the test setup rather than continuing with broken database
|
|
t.Fatalf("Failed to run PostGIS migrations: %v", err)
|
|
}
|
|
}
|
|
|
|
return gormDB
|
|
}
|