| package schema |
|
|
| import ( |
| "context" |
| "database/sql" |
| "fmt" |
| "os" |
| "strings" |
| "testing" |
|
|
| _ "github.com/go-sql-driver/mysql" |
| _ "modernc.org/sqlite" |
| ) |
|
|
| |
| type TestSuiteIntegration struct { |
| dbSQLite *sql.DB |
| dbMySQL *sql.DB |
| mysqlDSN string |
| skipMySQL bool |
| tablesDefs []func() *TableBuilder |
| tableNames []string |
| } |
|
|
| |
| func setupIntegrationTest(t *testing.T) *TestSuiteIntegration { |
| suite := &TestSuiteIntegration{ |
| tablesDefs: []func() *TableBuilder{ |
| DefineChannelsTable, |
| DefineAPIKeysTable, |
| DefineChannelModelsTable, |
| DefineAuthTokensTable, |
| DefineSystemSettingsTable, |
| DefineAdminSessionsTable, |
| DefineLogsTable, |
| }, |
| tableNames: []string{ |
| "channels", |
| "api_keys", |
| "channel_models", |
| "auth_tokens", |
| "system_settings", |
| "admin_sessions", |
| "logs", |
| }, |
| } |
|
|
| |
| sqliteDB, err := sql.Open("sqlite", ":memory:") |
| if err != nil { |
| t.Fatalf("Failed to open SQLite: %v", err) |
| } |
| suite.dbSQLite = sqliteDB |
|
|
| |
| suite.mysqlDSN = os.Getenv("CCLOAD_TEST_MYSQL_DSN") |
| if suite.mysqlDSN == "" { |
| t.Logf("MySQL DSN not set, skipping MySQL tests") |
| suite.skipMySQL = true |
| } else { |
| mysqlDB, err := sql.Open("mysql", suite.mysqlDSN) |
| if err != nil { |
| t.Logf("Failed to open MySQL: %v, skipping MySQL tests", err) |
| suite.skipMySQL = true |
| } else { |
| suite.dbMySQL = mysqlDB |
| } |
| } |
|
|
| return suite |
| } |
|
|
| |
| func teardownIntegrationTest(suite *TestSuiteIntegration, _ *testing.T) { |
| if suite.dbSQLite != nil { |
| _ = suite.dbSQLite.Close() |
| } |
| if suite.dbMySQL != nil && !suite.skipMySQL { |
| _ = suite.dbMySQL.Close() |
| } |
| } |
|
|
| |
| func TestAllTablesSQLiteIntegration(t *testing.T) { |
| suite := setupIntegrationTest(t) |
| defer teardownIntegrationTest(suite, t) |
|
|
| ctx := context.Background() |
|
|
| |
| for i, tableDef := range suite.tablesDefs { |
| tableName := suite.tableNames[i] |
| t.Run(tableName, func(t *testing.T) { |
| |
| builder := tableDef() |
| sqliteDDL := builder.BuildSQLite() |
| t.Logf("SQLite DDL for %s:\n%s", tableName, sqliteDDL) |
|
|
| |
| _, err := suite.dbSQLite.ExecContext(ctx, sqliteDDL) |
| if err != nil { |
| t.Fatalf("Failed to create table %s: %v", tableName, err) |
| } |
|
|
| |
| verifyTableExists(t, suite.dbSQLite, tableName, "SQLite") |
|
|
| |
| verifyTableStructure(t, suite.dbSQLite, tableName, builder, "SQLite") |
|
|
| |
| indexes := builder.GetIndexesSQLite() |
| for _, idx := range indexes { |
| _, err := suite.dbSQLite.ExecContext(ctx, idx.SQL) |
| if err != nil { |
| t.Fatalf("Failed to create index %s: %v", idx.Name, err) |
| } |
| } |
|
|
| |
| t.Logf("Attempting to verify indexes for %s...", tableName) |
| verifyIndexesCreated(t, suite.dbSQLite, tableName, indexes, "SQLite") |
|
|
| |
| testBasicInsert(t, suite.dbSQLite, tableName) |
| }) |
| } |
|
|
| |
| t.Run("TableRelationships", func(t *testing.T) { |
| testTableRelationships(t, suite.dbSQLite) |
| }) |
| } |
|
|
| |
| func TestAllTablesMySQLIntegration(t *testing.T) { |
| if testing.Short() { |
| t.Skip("Skipping MySQL integration test in short mode") |
| } |
|
|
| suite := setupIntegrationTest(t) |
| defer teardownIntegrationTest(suite, t) |
|
|
| if suite.skipMySQL { |
| t.Skip("MySQL tests skipped") |
| } |
|
|
| ctx := context.Background() |
|
|
| |
| for i, tableDef := range suite.tablesDefs { |
| tableName := suite.tableNames[i] |
| t.Run(tableName, func(t *testing.T) { |
| |
| builder := tableDef() |
| mysqlDDL := builder.BuildMySQL() |
| t.Logf("MySQL DDL for %s:\n%s", tableName, mysqlDDL) |
|
|
| |
| _, err := suite.dbMySQL.ExecContext(ctx, mysqlDDL) |
| if err != nil { |
| t.Fatalf("Failed to create table %s: %v", tableName, err) |
| } |
|
|
| |
| verifyTableExists(t, suite.dbMySQL, tableName, "MySQL") |
|
|
| |
| verifyTableStructure(t, suite.dbMySQL, tableName, builder, "MySQL") |
|
|
| |
| verifyIndexesCreated(t, suite.dbMySQL, tableName, builder.GetIndexesMySQL(), "MySQL") |
|
|
| |
| testBasicInsert(t, suite.dbMySQL, tableName) |
| }) |
| } |
|
|
| |
| t.Run("TableRelationships", func(t *testing.T) { |
| testTableRelationships(t, suite.dbMySQL) |
| }) |
| } |
|
|
| |
| func verifyTableExists(t *testing.T, db *sql.DB, tableName, dbType string) { |
| var exists bool |
| var query string |
| var args []any |
|
|
| switch dbType { |
| case "SQLite": |
| query = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?" |
| args = []any{tableName} |
| case "MySQL": |
| query = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=?" |
| args = []any{tableName} |
| } |
|
|
| err := db.QueryRow(query, args...).Scan(&exists) |
| if err != nil { |
| t.Fatalf("Failed to check if table %s exists: %v", tableName, err) |
| } |
| if !exists { |
| t.Errorf("Table %s was not created", tableName) |
| } |
| } |
|
|
| |
| func verifyTableStructure(t *testing.T, db *sql.DB, tableName string, _ *TableBuilder, dbType string) { |
| var query string |
| switch dbType { |
| case "SQLite": |
| query = fmt.Sprintf("PRAGMA table_info(%s)", tableName) |
| case "MySQL": |
| query = fmt.Sprintf("DESCRIBE %s", tableName) |
| } |
|
|
| rows, err := db.Query(query) |
| if err != nil { |
| t.Fatalf("Failed to get table structure for %s: %v", tableName, err) |
| } |
| defer func() { _ = rows.Close() }() |
|
|
| |
| var actualColumns []string |
| for rows.Next() { |
| var colName, colType, nullable, key, defaultValue, extra string |
|
|
| switch dbType { |
| case "SQLite": |
| var cid int |
| var dfltValue any |
| err := rows.Scan(&cid, &colName, &colType, &nullable, &dfltValue, &extra) |
| if err != nil { |
| t.Errorf("Failed to scan column info: %v", err) |
| continue |
| } |
| actualColumns = append(actualColumns, fmt.Sprintf("%s %s", colName, colType)) |
| case "MySQL": |
| err := rows.Scan(&colName, &colType, &nullable, &key, &defaultValue, &extra) |
| if err != nil { |
| t.Errorf("Failed to scan column info: %v", err) |
| continue |
| } |
| actualColumns = append(actualColumns, fmt.Sprintf("%s %s", colName, colType)) |
| } |
| } |
|
|
| t.Logf("Table %s structure (%s):", tableName, dbType) |
| for _, col := range actualColumns { |
| t.Logf(" - %s", col) |
| } |
|
|
| |
| if len(actualColumns) == 0 { |
| t.Errorf("No columns found in table %s", tableName) |
| } |
| } |
|
|
| |
| func verifyIndexesCreated(t *testing.T, db *sql.DB, tableName string, indexes []IndexDef, dbType string) { |
| for _, idx := range indexes { |
| t.Logf("Verifying index: %s", idx.SQL) |
|
|
| var query string |
| var result any |
|
|
| switch dbType { |
| case "SQLite": |
| |
| query = fmt.Sprintf("SELECT name FROM pragma_index_list('%s') WHERE name='%s'", tableName, idx.Name) |
| err := db.QueryRow(query).Scan(&result) |
| if err != nil { |
| |
| t.Errorf("Index %s not found in SQLite: %v", idx.Name, err) |
| continue |
| } |
| t.Logf("Index %s found in SQLite", idx.Name) |
| case "MySQL": |
| |
| query = fmt.Sprintf("SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='%s' AND index_name='%s'", tableName, idx.Name) |
| var count int |
| err := db.QueryRow(query).Scan(&count) |
| if err != nil { |
| t.Errorf("Index %s verification failed in MySQL: %v", idx.Name, err) |
| continue |
| } |
| if count == 0 { |
| |
| t.Errorf("Index %s not found in MySQL", idx.Name) |
| } else { |
| t.Logf("Index %s verified successfully in MySQL", idx.Name) |
| } |
| } |
| } |
| } |
|
|
| |
| func testBasicInsert(t *testing.T, db *sql.DB, tableName string) { |
| |
| switch tableName { |
| case "channels": |
| |
| _, err := db.Exec("INSERT INTO channels (name, url, channel_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", |
| "test-channel", "http://example.com", "anthropic", 1234567890, 1234567890) |
| if err != nil { |
| t.Logf("Warning: Failed to insert test data into %s: %v", tableName, err) |
| } |
| case "auth_tokens": |
| _, err := db.Exec("INSERT INTO auth_tokens (token, description, created_at, is_active) VALUES (?, ?, ?, ?)", |
| "test-token", "Test Token", 1234567890, 1) |
| if err != nil { |
| t.Logf("Warning: Failed to insert test data into %s: %v", tableName, err) |
| } |
| case "system_settings": |
| _, err := db.Exec("INSERT OR IGNORE INTO system_settings (key, value, value_type, description, default_value, updated_at) VALUES (?, ?, ?, ?, ?, ?)", |
| "test_setting", "test_value", "string", "Test Setting", "test_value", 1234567890) |
| if err != nil { |
| t.Logf("Warning: Failed to insert test data into %s: %v", tableName, err) |
| } |
| default: |
| |
| t.Logf("Skipping insert test for %s (foreign key dependencies)", tableName) |
| } |
| } |
|
|
| |
| func testTableRelationships(t *testing.T, db *sql.DB) { |
| |
| |
| var foreignKeysSupported bool |
| err := db.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeysSupported) |
| if err == nil && foreignKeysSupported { |
| t.Log("Testing foreign key constraints...") |
|
|
| |
| |
| result, err := db.Exec("INSERT INTO channels (name, url, channel_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", |
| "test-channel-rel", "http://example.com", "anthropic", 1234567890, 1234567890) |
| if err != nil { |
| t.Fatalf("Failed to insert test channel: %v", err) |
| } |
|
|
| channelID, _ := result.LastInsertId() |
|
|
| |
| _, err = db.Exec("INSERT INTO api_keys (channel_id, key_index, api_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", |
| channelID, 0, "test-api-key", 1234567890, 1234567890) |
| if err != nil { |
| t.Fatalf("Failed to insert related api_key: %v", err) |
| } |
|
|
| |
| _, err = db.Exec("INSERT INTO api_keys (channel_id, key_index, api_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", |
| 99999, 0, "test-api-key-invalid", 1234567890, 1234567890) |
| if err == nil { |
| t.Error("Expected foreign key constraint violation for invalid channel_id") |
| } |
|
|
| t.Log("Foreign key constraints working correctly") |
| } else { |
| t.Log("Foreign key constraints not supported or disabled") |
| } |
| } |
|
|
| |
| func TestTypeConversionCorrectness(t *testing.T) { |
| testCases := []struct { |
| mysqlCol string |
| expectedSQLite string |
| description string |
| }{ |
| {"INT PRIMARY KEY AUTO_INCREMENT", "INTEGER PRIMARY KEY AUTOINCREMENT", "Auto increment primary key"}, |
| {"INT NOT NULL", "INTEGER NOT NULL", "Integer column"}, |
| {"BIGINT NOT NULL", "INTEGER NOT NULL", "Big integer column"}, |
| {"VARCHAR(191) NOT NULL", "TEXT NOT NULL", "Varchar column"}, |
| {"TEXT NOT NULL", "TEXT NOT NULL", "Text column (unchanged)"}, |
| {"TINYINT NOT NULL DEFAULT 1", "INTEGER NOT NULL DEFAULT 1", "Tinyint column"}, |
| {"DOUBLE NOT NULL DEFAULT 0.0", "REAL NOT NULL DEFAULT 0.0", "Double column"}, |
| {"VARCHAR(255) UNIQUE", "TEXT UNIQUE", "Varchar with unique constraint"}, |
| {"INT PRIMARY KEY", "INTEGER PRIMARY KEY", "Primary key without auto increment"}, |
| } |
|
|
| for _, tc := range testCases { |
| t.Run(tc.description, func(t *testing.T) { |
| |
| builder := NewTable("test"). |
| Column(tc.mysqlCol) |
|
|
| sqliteDDL := builder.BuildSQLite() |
|
|
| |
| if !strings.Contains(sqliteDDL, tc.expectedSQLite) { |
| t.Errorf("Expected %s in SQLite DDL, but got:\n%s", tc.expectedSQLite, sqliteDDL) |
| } |
|
|
| |
| if tc.expectedSQLite != tc.mysqlCol && strings.Contains(sqliteDDL, tc.mysqlCol) { |
| t.Errorf("Original MySQL type %s should not appear in SQLite DDL", tc.mysqlCol) |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestIndexGeneration(t *testing.T) { |
| builder := NewTable("test_table"). |
| Column("id INT PRIMARY KEY AUTO_INCREMENT"). |
| Column("name VARCHAR(191) NOT NULL"). |
| Column("created_at BIGINT NOT NULL"). |
| Index("idx_name", "name"). |
| Index("idx_created_at", "created_at DESC") |
|
|
| |
| mysqlIndexes := builder.GetIndexesMySQL() |
| if len(mysqlIndexes) != 2 { |
| t.Errorf("Expected 2 MySQL indexes, got %d", len(mysqlIndexes)) |
| } |
|
|
| |
| for _, idx := range mysqlIndexes { |
| if !strings.Contains(idx.SQL, "CREATE INDEX") { |
| t.Errorf("MySQL index should contain 'CREATE INDEX': %s", idx.SQL) |
| } |
| if strings.Contains(idx.SQL, "IF NOT EXISTS") { |
| t.Errorf("MySQL index should not contain 'IF NOT EXISTS': %s", idx.SQL) |
| } |
| } |
|
|
| |
| sqliteIndexes := builder.GetIndexesSQLite() |
| if len(sqliteIndexes) != 2 { |
| t.Errorf("Expected 2 SQLite indexes, got %d", len(sqliteIndexes)) |
| } |
|
|
| |
| for _, idx := range sqliteIndexes { |
| if !strings.Contains(idx.SQL, "CREATE INDEX IF NOT EXISTS") { |
| t.Errorf("SQLite index should contain 'CREATE INDEX IF NOT EXISTS': %s", idx.SQL) |
| } |
| } |
| } |
|
|
| |
| func TestBuilderChain(t *testing.T) { |
| builder := NewTable("test"). |
| Column("id INT PRIMARY KEY AUTO_INCREMENT"). |
| Column("name VARCHAR(100) NOT NULL"). |
| Index("idx_name", "name") |
|
|
| if builder.name != "test" { |
| t.Errorf("Table name not set correctly") |
| } |
|
|
| if len(builder.columns) != 2 { |
| t.Errorf("Expected 2 columns, got %d", len(builder.columns)) |
| } |
|
|
| if len(builder.indexes) != 1 { |
| t.Errorf("Expected 1 index, got %d", len(builder.indexes)) |
| } |
|
|
| |
| mysqlDDL := builder.BuildMySQL() |
| if !strings.Contains(mysqlDDL, "CREATE TABLE IF NOT EXISTS test") { |
| t.Errorf("MySQL DDL should contain table creation statement") |
| } |
|
|
| |
| sqliteDDL := builder.BuildSQLite() |
| if !strings.Contains(sqliteDDL, "CREATE TABLE IF NOT EXISTS test") { |
| t.Errorf("SQLite DDL should contain table creation statement") |
| } |
| } |
|
|