test: Add comprehensive test suite for database functionality
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
0f16748224
commit
5733209251
5 changed files with 955 additions and 0 deletions
167
internal/database/database_test.go
Normal file
167
internal/database/database_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
177
internal/database/loader_test.go
Normal file
177
internal/database/loader_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
268
internal/database/manager_callsign_test.go
Normal file
268
internal/database/manager_callsign_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
307
internal/database/optimization_test.go
Normal file
307
internal/database/optimization_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
36
internal/database/test_helpers.go
Normal file
36
internal/database/test_helpers.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue