From 573320925145c37b02e4e9fd87921a302d0c19cd Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sun, 31 Aug 2025 19:43:41 +0200 Subject: [PATCH] test: Add comprehensive test suite for database functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add unit tests for database operations and optimization - Test external data source loading and caching - Add callsign manager functionality tests - Create test helpers for database testing utilities - Ensure database reliability and performance validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/database/database_test.go | 167 +++++++++++ internal/database/loader_test.go | 177 ++++++++++++ internal/database/manager_callsign_test.go | 268 ++++++++++++++++++ internal/database/optimization_test.go | 307 +++++++++++++++++++++ internal/database/test_helpers.go | 36 +++ 5 files changed, 955 insertions(+) create mode 100644 internal/database/database_test.go create mode 100644 internal/database/loader_test.go create mode 100644 internal/database/manager_callsign_test.go create mode 100644 internal/database/optimization_test.go create mode 100644 internal/database/test_helpers.go diff --git a/internal/database/database_test.go b/internal/database/database_test.go new file mode 100644 index 0000000..4e6d21b --- /dev/null +++ b/internal/database/database_test.go @@ -0,0 +1,167 @@ +package database + +import ( + "os" + "testing" + "time" +) + +func TestNewDatabase(t *testing.T) { + // Create temporary database file + tempFile, err := os.CreateTemp("", "test_skyview_*.db") + if err != nil { + t.Fatal("Failed to create temp database file:", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + config := &Config{ + Path: tempFile.Name(), + VacuumInterval: time.Hour, + } + + db, err := NewDatabase(config) + if err != nil { + t.Fatal("Failed to create database:", err) + } + defer db.Close() + + if db == nil { + t.Fatal("NewDatabase() returned nil") + } + + // Test connection + conn := db.GetConnection() + if conn == nil { + t.Fatal("GetConnection() returned nil") + } + + // Test basic query + var result int + err = conn.QueryRow("SELECT 1").Scan(&result) + if err != nil { + t.Error("Basic query failed:", err) + } + if result != 1 { + t.Error("Basic query returned wrong result:", result) + } +} + +func TestDatabaseClose(t *testing.T) { + tempFile, err := os.CreateTemp("", "test_skyview_*.db") + if err != nil { + t.Fatal("Failed to create temp database file:", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + config := &Config{Path: tempFile.Name()} + db, err := NewDatabase(config) + if err != nil { + t.Fatal("Failed to create database:", err) + } + + // Close should not error + if err := db.Close(); err != nil { + t.Error("Database Close() returned error:", err) + } + + // Second close should be safe + if err := db.Close(); err != nil { + t.Error("Second Close() returned error:", err) + } + + // Connection should be nil after close + conn := db.GetConnection() + if conn != nil { + t.Error("GetConnection() should return nil after Close()") + } +} + +func TestDatabaseConfig(t *testing.T) { + tempFile, err := os.CreateTemp("", "test_skyview_*.db") + if err != nil { + t.Fatal("Failed to create temp database file:", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + config := &Config{ + Path: tempFile.Name(), + VacuumInterval: 2 * time.Hour, + } + + db, err := NewDatabase(config) + if err != nil { + t.Fatal("Failed to create database:", err) + } + defer db.Close() + + // Test that config is stored correctly + if db.config != config { + t.Error("Database config not stored correctly") + } + if db.config.VacuumInterval != 2*time.Hour { + t.Error("VacuumInterval not preserved:", db.config.VacuumInterval) + } +} + +func TestDatabaseMigrations(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + conn := db.GetConnection() + + // Check that essential tables exist after migrations + tables := []string{"airlines", "airports", "callsign_cache", "data_sources", "aircraft_history"} + for _, table := range tables { + var count int + query := "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?" + err := conn.QueryRow(query, table).Scan(&count) + if err != nil { + t.Errorf("Failed to check for table %s: %v", table, err) + } + if count != 1 { + t.Errorf("Table %s does not exist", table) + } + } +} + +func TestDatabasePragmas(t *testing.T) { + tempFile, err := os.CreateTemp("", "test_skyview_*.db") + if err != nil { + t.Fatal("Failed to create temp database file:", err) + } + defer os.Remove(tempFile.Name()) + tempFile.Close() + + config := &Config{Path: tempFile.Name()} + db, err := NewDatabase(config) + if err != nil { + t.Fatal("Failed to create database:", err) + } + defer db.Close() + + conn := db.GetConnection() + + // Check that foreign keys are enabled + var foreignKeys int + err = conn.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeys) + if err != nil { + t.Error("Failed to check foreign_keys pragma:", err) + } + if foreignKeys != 1 { + t.Error("Foreign keys should be enabled") + } + + // Check journal mode + var journalMode string + err = conn.QueryRow("PRAGMA journal_mode").Scan(&journalMode) + if err != nil { + t.Error("Failed to check journal_mode:", err) + } + // Should be WAL mode for better concurrency + if journalMode != "wal" { + t.Errorf("Expected WAL journal mode, got: %s", journalMode) + } +} \ No newline at end of file diff --git a/internal/database/loader_test.go b/internal/database/loader_test.go new file mode 100644 index 0000000..9bfebe8 --- /dev/null +++ b/internal/database/loader_test.go @@ -0,0 +1,177 @@ +package database + +import ( + "strings" + "testing" +) + +func TestDataLoader_Creation(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + loader := NewDataLoader(db.GetConnection()) + if loader == nil { + t.Fatal("NewDataLoader returned nil") + } +} + +func TestDataLoader_LoadOpenFlightsAirlines(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + loader := NewDataLoader(db.GetConnection()) + + // Create a test data source for OpenFlights Airlines + source := DataSource{ + Name: "OpenFlights Airlines Test", + License: "ODbL 1.0", + URL: "https://raw.githubusercontent.com/jpatokal/openflights/master/data/airlines.dat", + Format: "openflights", + Version: "2024-test", + } + + result, err := loader.LoadDataSource(source) + if err != nil { + // Network issues in tests are acceptable + if strings.Contains(err.Error(), "connection") || + strings.Contains(err.Error(), "timeout") || + strings.Contains(err.Error(), "no such host") { + t.Skipf("Skipping network test due to connectivity issue: %v", err) + } + t.Fatal("LoadDataSource failed:", err) + } + + if result == nil { + t.Fatal("Expected load result, got nil") + } + + t.Logf("Loaded airlines: Total=%d, New=%d, Errors=%d, Duration=%v", + result.RecordsTotal, result.RecordsNew, result.RecordsError, result.Duration) + + // Verify some data was processed + if result.RecordsTotal == 0 { + t.Error("No records were processed") + } +} + +func TestDataLoader_LoadOurAirports(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + loader := NewDataLoader(db.GetConnection()) + + // Create a test data source for OurAirports + source := DataSource{ + Name: "OurAirports Test", + License: "CC0 1.0", + URL: "https://davidmegginson.github.io/ourairports-data/airports.csv", + Format: "ourairports", + Version: "2024-test", + } + + result, err := loader.LoadDataSource(source) + if err != nil { + // Network issues in tests are acceptable + if strings.Contains(err.Error(), "connection") || + strings.Contains(err.Error(), "timeout") || + strings.Contains(err.Error(), "no such host") { + t.Skipf("Skipping network test due to connectivity issue: %v", err) + } + t.Fatal("LoadDataSource failed:", err) + } + + if result != nil { + t.Logf("Loaded airports: Total=%d, New=%d, Errors=%d, Duration=%v", + result.RecordsTotal, result.RecordsNew, result.RecordsError, result.Duration) + } +} + +func TestDataLoader_GetLoadedDataSources(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + loader := NewDataLoader(db.GetConnection()) + + sources, err := loader.GetLoadedDataSources() + if err != nil { + t.Fatal("GetLoadedDataSources failed:", err) + } + + // Initially should be empty or minimal + t.Logf("Found %d loaded data sources", len(sources)) +} + +func TestDataLoader_ClearDataSource(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + loader := NewDataLoader(db.GetConnection()) + + // Test clearing a non-existent source (should not error) + err := loader.ClearDataSource("nonexistent") + if err != nil { + t.Error("ClearDataSource should not error on nonexistent source:", err) + } +} + +func TestDataSource_Struct(t *testing.T) { + source := DataSource{ + Name: "Test Source", + License: "Test License", + URL: "https://example.com/data.csv", + RequiresConsent: false, + UserAcceptedLicense: true, + Format: "csv", + Version: "1.0", + } + + // Test that all fields are accessible + if source.Name != "Test Source" { + t.Error("Name field not preserved") + } + if source.License != "Test License" { + t.Error("License field not preserved") + } + if source.URL != "https://example.com/data.csv" { + t.Error("URL field not preserved") + } + if source.RequiresConsent != false { + t.Error("RequiresConsent field not preserved") + } + if source.UserAcceptedLicense != true { + t.Error("UserAcceptedLicense field not preserved") + } + if source.Format != "csv" { + t.Error("Format field not preserved") + } + if source.Version != "1.0" { + t.Error("Version field not preserved") + } +} + +func TestLoadResult_Struct(t *testing.T) { + result := LoadResult{ + Source: "Test Source", + RecordsTotal: 100, + RecordsNew: 80, + RecordsError: 5, + Errors: []string{"error1", "error2"}, + } + + // Test that all fields are accessible + if result.Source != "Test Source" { + t.Error("Source field not preserved") + } + if result.RecordsTotal != 100 { + t.Error("RecordsTotal field not preserved") + } + if result.RecordsNew != 80 { + t.Error("RecordsNew field not preserved") + } + if result.RecordsError != 5 { + t.Error("RecordsError field not preserved") + } + if len(result.Errors) != 2 { + t.Error("Errors field not preserved") + } +} \ No newline at end of file diff --git a/internal/database/manager_callsign_test.go b/internal/database/manager_callsign_test.go new file mode 100644 index 0000000..731f61a --- /dev/null +++ b/internal/database/manager_callsign_test.go @@ -0,0 +1,268 @@ +package database + +import ( + "testing" +) + +func TestCallsignManager_Creation(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + if manager == nil { + t.Fatal("NewCallsignManager returned nil") + } +} + +func TestCallsignManager_ParseCallsign(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + testCases := []struct { + callsign string + expectedValid bool + expectedAirline string + expectedFlight string + }{ + {"UAL123", true, "UAL", "123"}, + {"BA4567", true, "BA", "4567"}, + {"AFR89", true, "AFR", "89"}, + {"N123AB", false, "", ""}, // Aircraft registration, not callsign + {"INVALID", false, "", ""}, // No numbers + {"123", false, "", ""}, // Only numbers + {"A", false, "", ""}, // Too short + {"", false, "", ""}, // Empty + } + + for _, tc := range testCases { + result := manager.ParseCallsign(tc.callsign) + if result.IsValid != tc.expectedValid { + t.Errorf("ParseCallsign(%s): expected valid=%v, got %v", + tc.callsign, tc.expectedValid, result.IsValid) + } + if result.IsValid && result.AirlineCode != tc.expectedAirline { + t.Errorf("ParseCallsign(%s): expected airline=%s, got %s", + tc.callsign, tc.expectedAirline, result.AirlineCode) + } + if result.IsValid && result.FlightNumber != tc.expectedFlight { + t.Errorf("ParseCallsign(%s): expected flight=%s, got %s", + tc.callsign, tc.expectedFlight, result.FlightNumber) + } + } +} + +func TestCallsignManager_GetCallsignInfo(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + // Insert test airline data + conn := db.GetConnection() + _, err := conn.Exec(` + INSERT INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source) + VALUES (1, 'Test Airways', 'Test', 'TA', 'TST', 'TESTAIR', 'United States', 1, 'test') + `) + if err != nil { + t.Fatal("Failed to insert test data:", err) + } + + // Test valid callsign + info, err := manager.GetCallsignInfo("TST123") + if err != nil { + t.Fatal("GetCallsignInfo failed:", err) + } + + if info == nil { + t.Fatal("Expected callsign info, got nil") + } + + if info.OriginalCallsign != "TST123" { + t.Errorf("Expected callsign TST123, got %s", info.OriginalCallsign) + } + if info.AirlineCode != "TST" { + t.Errorf("Expected airline code TST, got %s", info.AirlineCode) + } + if info.FlightNumber != "123" { + t.Errorf("Expected flight number 123, got %s", info.FlightNumber) + } + if info.AirlineName != "Test Airways" { + t.Errorf("Expected airline name 'Test Airways', got %s", info.AirlineName) + } + if info.AirlineCountry != "United States" { + t.Errorf("Expected airline country 'United States', got %s", info.AirlineCountry) + } +} + +func TestCallsignManager_GetCallsignInfo_InvalidCallsign(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + // Test with invalid callsign format + info, err := manager.GetCallsignInfo("INVALID") + if err != nil { + t.Error("GetCallsignInfo should not error on invalid format:", err) + } + if info == nil { + t.Fatal("Expected info structure even for invalid callsign") + } + if info.IsValid { + t.Error("Invalid callsign should not be marked as valid") + } + + // Test with unknown airline + info, err = manager.GetCallsignInfo("UNK123") + if err != nil { + t.Error("GetCallsignInfo should not error on unknown airline:", err) + } + if info == nil { + t.Fatal("Expected info structure for unknown airline") + } +} + +func TestCallsignManager_GetCallsignInfo_EmptyCallsign(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + // Test with empty callsign + info, err := manager.GetCallsignInfo("") + if err == nil { + t.Error("GetCallsignInfo should error on empty callsign") + } + if info != nil { + t.Error("Expected nil info for empty callsign") + } +} + +func TestCallsignManager_ClearExpiredCache(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + err := manager.ClearExpiredCache() + if err != nil { + t.Error("ClearExpiredCache should not error:", err) + } +} + +func TestCallsignManager_GetCacheStats(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + stats, err := manager.GetCacheStats() + if err != nil { + t.Error("GetCacheStats should not error:", err) + } + + if stats == nil { + t.Error("Expected cache stats, got nil") + } + + t.Logf("Cache stats: %+v", stats) +} + +func TestCallsignManager_SearchAirlines(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + // Insert test airline data + conn := db.GetConnection() + _, err := conn.Exec(` + INSERT INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source) + VALUES (1, 'Test Airways', 'Test', 'TA', 'TST', 'TESTAIR', 'United States', 1, 'test'), + (2, 'Another Airline', 'Another', 'AA', 'ANO', 'ANOTHER', 'Canada', 1, 'test') + `) + if err != nil { + t.Fatal("Failed to insert test data:", err) + } + + // Search for airlines + airlines, err := manager.SearchAirlines("Test") + if err != nil { + t.Fatal("SearchAirlines failed:", err) + } + + found := false + for _, airline := range airlines { + if airline.Name == "Test Airways" { + found = true + break + } + } + if !found { + t.Error("Expected to find Test Airways in search results") + } + + t.Logf("Found %d airlines matching 'Test'", len(airlines)) +} + +func TestCallsignManager_GetAirlinesByCountry(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + manager := NewCallsignManager(db.GetConnection()) + + // Insert test airline data + conn := db.GetConnection() + _, err := conn.Exec(` + INSERT INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source) + VALUES (1, 'US Airways', 'US', 'US', 'USA', 'USAIR', 'United States', 1, 'test'), + (2, 'Canada Air', 'CA', 'CA', 'CAN', 'CANAIR', 'Canada', 1, 'test') + `) + if err != nil { + t.Fatal("Failed to insert test data:", err) + } + + // Get airlines by country + airlines, err := manager.GetAirlinesByCountry("United States") + if err != nil { + t.Fatal("GetAirlinesByCountry failed:", err) + } + + found := false + for _, airline := range airlines { + if airline.Name == "US Airways" { + found = true + break + } + } + if !found { + t.Error("Expected to find US Airways for United States") + } + + t.Logf("Found %d airlines in United States", len(airlines)) +} + +func TestCallsignParseResult_Struct(t *testing.T) { + result := &CallsignParseResult{ + OriginalCallsign: "UAL123", + AirlineCode: "UAL", + FlightNumber: "123", + IsValid: true, + } + + // Test that all fields are accessible + if result.OriginalCallsign != "UAL123" { + t.Error("OriginalCallsign field not preserved") + } + if result.AirlineCode != "UAL" { + t.Error("AirlineCode field not preserved") + } + if result.FlightNumber != "123" { + t.Error("FlightNumber field not preserved") + } + if !result.IsValid { + t.Error("IsValid field not preserved") + } +} \ No newline at end of file diff --git a/internal/database/optimization_test.go b/internal/database/optimization_test.go new file mode 100644 index 0000000..c1aeb74 --- /dev/null +++ b/internal/database/optimization_test.go @@ -0,0 +1,307 @@ +package database + +import ( + "os" + "testing" + "time" +) + +func TestOptimizationManager_VacuumDatabase(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + config := &Config{Path: db.config.Path} + optimizer := NewOptimizationManager(db, config) + + err := optimizer.VacuumDatabase() + if err != nil { + t.Fatal("VacuumDatabase failed:", err) + } + + // Verify vacuum was successful by checking database integrity + conn := db.GetConnection() + var result string + err = conn.QueryRow("PRAGMA integrity_check").Scan(&result) + if err != nil { + t.Error("Failed to run integrity check:", err) + } + if result != "ok" { + t.Errorf("Database integrity check failed: %s", result) + } +} + +func TestOptimizationManager_OptimizeDatabase(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + config := &Config{Path: db.config.Path} + optimizer := NewOptimizationManager(db, config) + + err := optimizer.OptimizeDatabase() + if err != nil { + t.Fatal("OptimizeDatabase failed:", err) + } + + // Check that auto_vacuum was set + conn := db.GetConnection() + var autoVacuum int + err = conn.QueryRow("PRAGMA auto_vacuum").Scan(&autoVacuum) + if err != nil { + t.Error("Failed to check auto_vacuum setting:", err) + } + // Should be 2 (INCREMENTAL) after optimization + if autoVacuum != 2 { + t.Errorf("Expected auto_vacuum = 2 (INCREMENTAL), got %d", autoVacuum) + } +} + +func TestOptimizationManager_OptimizePageSize(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + config := &Config{Path: db.config.Path} + optimizer := NewOptimizationManager(db, config) + + // Get current page size + conn := db.GetConnection() + var currentPageSize int + err := conn.QueryRow("PRAGMA page_size").Scan(¤tPageSize) + if err != nil { + t.Fatal("Failed to get current page size:", err) + } + + // Set a different page size + targetPageSize := 8192 + if currentPageSize == targetPageSize { + targetPageSize = 4096 // Use different size if already at target + } + + err = optimizer.OptimizePageSize(targetPageSize) + if err != nil { + t.Fatal("OptimizePageSize failed:", err) + } + + // Verify page size was changed + var newPageSize int + err = conn.QueryRow("PRAGMA page_size").Scan(&newPageSize) + if err != nil { + t.Error("Failed to get new page size:", err) + } + if newPageSize != targetPageSize { + t.Errorf("Expected page size %d, got %d", targetPageSize, newPageSize) + } + + // Test setting same page size (should be no-op) + err = optimizer.OptimizePageSize(targetPageSize) + if err != nil { + t.Error("OptimizePageSize failed for same page size:", err) + } +} + +func TestOptimizationManager_GetOptimizationStats(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + config := &Config{Path: db.config.Path} + optimizer := NewOptimizationManager(db, config) + + // Insert some test data to make stats more meaningful + conn := db.GetConnection() + _, err := conn.Exec(` + INSERT INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source) + VALUES (1, 'Test Airways', 'Test', 'TA', 'TST', 'TESTAIR', 'United States', 1, 'test') + `) + if err != nil { + t.Error("Failed to insert test data:", err) + } + + stats, err := optimizer.GetOptimizationStats() + if err != nil { + t.Fatal("GetOptimizationStats failed:", err) + } + + if stats == nil { + t.Fatal("Expected stats, got nil") + } + + // Check basic stats + if stats.DatabaseSize <= 0 { + t.Error("Database size should be greater than 0") + } + if stats.PageSize <= 0 { + t.Error("Page size should be greater than 0") + } + if stats.PageCount <= 0 { + t.Error("Page count should be greater than 0") + } + if stats.UsedPages < 0 { + t.Error("Used pages should be non-negative") + } + if stats.FreePages < 0 { + t.Error("Free pages should be non-negative") + } + if stats.Efficiency < 0 || stats.Efficiency > 100 { + t.Errorf("Efficiency should be between 0-100%%, got %.2f%%", stats.Efficiency) + } + + t.Logf("Database stats: Size=%d bytes, Pages=%d (used=%d, free=%d), Efficiency=%.1f%%", + stats.DatabaseSize, stats.PageCount, stats.UsedPages, stats.FreePages, stats.Efficiency) +} + +func TestOptimizationManager_PerformMaintenance(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + config := &Config{ + Path: db.config.Path, + VacuumInterval: time.Millisecond, // Very short interval for testing + } + optimizer := NewOptimizationManager(db, config) + + // Should perform vacuum due to short interval + err := optimizer.PerformMaintenance() + if err != nil { + t.Fatal("PerformMaintenance failed:", err) + } + + // Check that lastVacuum was updated + if optimizer.lastVacuum.IsZero() { + t.Error("lastVacuum should be set after maintenance") + } + + // Wait a bit and run again with longer interval + config.VacuumInterval = time.Hour // Long interval + err = optimizer.PerformMaintenance() + if err != nil { + t.Fatal("Second PerformMaintenance failed:", err) + } +} + +func TestOptimizationManager_getDatabaseSize(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + config := &Config{Path: db.config.Path} + optimizer := NewOptimizationManager(db, config) + + size, err := optimizer.getDatabaseSize() + if err != nil { + t.Fatal("getDatabaseSize failed:", err) + } + + if size <= 0 { + t.Error("Database size should be greater than 0") + } + + // Verify size matches actual file size + stat, err := os.Stat(db.config.Path) + if err != nil { + t.Fatal("Failed to stat database file:", err) + } + + if size != stat.Size() { + t.Errorf("getDatabaseSize returned %d, but file size is %d", size, stat.Size()) + } +} + +func TestOptimizationManager_InvalidPath(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + // Test with invalid path + config := &Config{Path: "/nonexistent/path/database.db"} + optimizer := NewOptimizationManager(db, config) + + _, err := optimizer.getDatabaseSize() + if err == nil { + t.Error("getDatabaseSize should fail with invalid path") + } +} + +func TestOptimizationStats_JSON(t *testing.T) { + stats := &OptimizationStats{ + DatabaseSize: 1024000, + PageSize: 4096, + PageCount: 250, + UsedPages: 200, + FreePages: 50, + Efficiency: 80.0, + AutoVacuumEnabled: true, + LastVacuum: time.Now(), + } + + // Test that all fields are accessible + if stats.DatabaseSize != 1024000 { + t.Error("DatabaseSize not preserved") + } + if stats.PageSize != 4096 { + t.Error("PageSize not preserved") + } + if stats.Efficiency != 80.0 { + t.Error("Efficiency not preserved") + } + if !stats.AutoVacuumEnabled { + t.Error("AutoVacuumEnabled not preserved") + } +} + +func TestOptimizationManager_WithRealData(t *testing.T) { + db, cleanup := setupTestDatabase(t) + defer cleanup() + + // Load some real data to make optimization more realistic + // Skip actual data loading in tests as it requires network access + // Just insert minimal test data + conn := db.GetConnection() + _, err := conn.Exec(`INSERT INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source) + VALUES (1, 'Test Airways', 'Test', 'TA', 'TST', 'TESTAIR', 'United States', 1, 'test')`) + if err != nil { + t.Fatal("Failed to insert test data:", err) + } + + config := &Config{Path: db.config.Path} + optimizer := NewOptimizationManager(db, config) + + // Get stats before optimization + statsBefore, err := optimizer.GetOptimizationStats() + if err != nil { + t.Fatal("Failed to get stats before optimization:", err) + } + + // Run optimization + err = optimizer.OptimizeDatabase() + if err != nil { + t.Fatal("OptimizeDatabase failed:", err) + } + + err = optimizer.VacuumDatabase() + if err != nil { + t.Fatal("VacuumDatabase failed:", err) + } + + // Get stats after optimization + statsAfter, err := optimizer.GetOptimizationStats() + if err != nil { + t.Fatal("Failed to get stats after optimization:", err) + } + + // Compare efficiency + t.Logf("Optimization results: %.2f%% → %.2f%% efficiency", + statsBefore.Efficiency, statsAfter.Efficiency) + + // After optimization, we should have auto-vacuum enabled + if !statsAfter.AutoVacuumEnabled { + t.Error("Auto-vacuum should be enabled after optimization") + } + + // Database should still be functional + conn = db.GetConnection() + var count int + err = conn.QueryRow("SELECT COUNT(*) FROM airlines").Scan(&count) + if err != nil { + t.Error("Database not functional after optimization:", err) + } + if count == 0 { + t.Error("Data lost during optimization") + } +} \ No newline at end of file diff --git a/internal/database/test_helpers.go b/internal/database/test_helpers.go new file mode 100644 index 0000000..9d4bcee --- /dev/null +++ b/internal/database/test_helpers.go @@ -0,0 +1,36 @@ +package database + +import ( + "os" + "testing" +) + +// setupTestDatabase creates a temporary database for testing +func setupTestDatabase(t *testing.T) (*Database, func()) { + tempFile, err := os.CreateTemp("", "test_skyview_*.db") + if err != nil { + t.Fatal("Failed to create temp database file:", err) + } + tempFile.Close() + + config := &Config{Path: tempFile.Name()} + db, err := NewDatabase(config) + if err != nil { + t.Fatal("Failed to create database:", err) + } + + // Initialize the database (run migrations) + err = db.Initialize() + if err != nil { + db.Close() + os.Remove(tempFile.Name()) + t.Fatal("Failed to initialize database:", err) + } + + cleanup := func() { + db.Close() + os.Remove(tempFile.Name()) + } + + return db, cleanup +} \ No newline at end of file