mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools
Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
* GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
* GET /api/v1/users/me/organizations - User organizations
* POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue
API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules
Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
266 lines
9.7 KiB
Go
266 lines
9.7 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
|
|
}
|
|
|
|
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
|
|
}
|