mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
492 lines
14 KiB
Go
492 lines
14 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func openBareTestSQLiteDB(t *testing.T, name string) *gorm.DB {
|
|
t.Helper()
|
|
|
|
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), name)), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite db: %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("get sql db: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = sqlDB.Close()
|
|
})
|
|
return db
|
|
}
|
|
|
|
func openTestSQLiteDB(t *testing.T, name string) *gorm.DB {
|
|
t.Helper()
|
|
|
|
db := openBareTestSQLiteDB(t, name)
|
|
if err := autoMigrateAll(db); err != nil {
|
|
t.Fatalf("auto migrate db: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func findDBModelByTableName(t *testing.T, tableName string) dbModel {
|
|
t.Helper()
|
|
|
|
models, err := buildDBModels()
|
|
if err != nil {
|
|
t.Fatalf("build db models: %v", err)
|
|
}
|
|
for _, item := range models {
|
|
if item.tableName == tableName {
|
|
return item
|
|
}
|
|
}
|
|
t.Fatalf("db model not found for table %s", tableName)
|
|
return dbModel{}
|
|
}
|
|
|
|
func TestIsDatabaseEmpty(t *testing.T) {
|
|
db := openTestSQLiteDB(t, "empty.db")
|
|
|
|
empty, err := isDatabaseEmpty(db)
|
|
if err != nil {
|
|
t.Fatalf("isDatabaseEmpty returned error: %v", err)
|
|
}
|
|
if !empty {
|
|
t.Fatal("expected database to be empty")
|
|
}
|
|
|
|
if err := db.Create(&User{
|
|
Username: "alice",
|
|
Password: "secret",
|
|
DisplayName: "Alice",
|
|
Role: 1,
|
|
Status: 1,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed user: %v", err)
|
|
}
|
|
|
|
empty, err = isDatabaseEmpty(db)
|
|
if err != nil {
|
|
t.Fatalf("isDatabaseEmpty after seed returned error: %v", err)
|
|
}
|
|
if empty {
|
|
t.Fatal("expected database to be non-empty")
|
|
}
|
|
}
|
|
|
|
func TestMigrateTableDataCopiesRows(t *testing.T) {
|
|
source := openTestSQLiteDB(t, "source.db")
|
|
target := openTestSQLiteDB(t, "target.db")
|
|
|
|
user := User{
|
|
Id: 1,
|
|
Username: "root",
|
|
Password: "hashed",
|
|
DisplayName: "Root User",
|
|
Role: 100,
|
|
Status: 1,
|
|
}
|
|
option := Option{
|
|
Key: "AgentHeartbeatInterval",
|
|
Value: "10000",
|
|
}
|
|
|
|
if err := source.Create(&user).Error; err != nil {
|
|
t.Fatalf("seed source user: %v", err)
|
|
}
|
|
if err := source.Create(&option).Error; err != nil {
|
|
t.Fatalf("seed source option: %v", err)
|
|
}
|
|
|
|
if err := migrateTableData(source, target, findDBModelByTableName(t, "users")); err != nil {
|
|
t.Fatalf("migrate users: %v", err)
|
|
}
|
|
if err := migrateTableData(source, target, findDBModelByTableName(t, "options")); err != nil {
|
|
t.Fatalf("migrate options: %v", err)
|
|
}
|
|
|
|
var gotUser User
|
|
if err := target.First(&gotUser, 1).Error; err != nil {
|
|
t.Fatalf("query migrated user: %v", err)
|
|
}
|
|
if gotUser.Username != user.Username || gotUser.DisplayName != user.DisplayName {
|
|
t.Fatalf("unexpected migrated user: %+v", gotUser)
|
|
}
|
|
|
|
var gotOption Option
|
|
if err := target.First(&gotOption, "key = ?", option.Key).Error; err != nil {
|
|
t.Fatalf("query migrated option: %v", err)
|
|
}
|
|
if gotOption.Value != option.Value {
|
|
t.Fatalf("unexpected migrated option value: %s", gotOption.Value)
|
|
}
|
|
}
|
|
|
|
func TestRegisterShardingAutoMigratesShardTables(t *testing.T) {
|
|
db := openBareTestSQLiteDB(t, "sharded.db")
|
|
if err := registerSharding(db, "sqlite"); err != nil {
|
|
t.Fatalf("register sharding: %v", err)
|
|
}
|
|
if err := autoMigrateAll(db); err != nil {
|
|
t.Fatalf("auto migrate db: %v", err)
|
|
}
|
|
|
|
for _, table := range []string{
|
|
"node_metric_snapshots_00",
|
|
"node_metric_snapshots_09",
|
|
"node_request_reports_00",
|
|
"node_request_reports_09",
|
|
"node_access_logs_00",
|
|
"node_access_logs_09",
|
|
} {
|
|
if !db.Migrator().HasTable(table) {
|
|
t.Fatalf("expected sharded table %s to exist", table)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMigrateObservabilityLegacyColumnsBackfillsHealthEventMetadata(t *testing.T) {
|
|
db := openTestSQLiteDB(t, "legacy-health-events.db")
|
|
|
|
if err := db.Exec("ALTER TABLE node_health_events ADD COLUMN raw_json TEXT").Error; err != nil {
|
|
t.Fatalf("add raw_json column: %v", err)
|
|
}
|
|
rawJSON, err := json.Marshal(map[string]any{
|
|
"event_type": "sync_error",
|
|
"metadata": map[string]string{
|
|
"reason": "checksum_mismatch",
|
|
"scope": "routes",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal raw json: %v", err)
|
|
}
|
|
event := &NodeHealthEvent{
|
|
NodeID: "node-legacy",
|
|
EventType: "sync_error",
|
|
Severity: "warning",
|
|
Status: "active",
|
|
Message: "checksum mismatch",
|
|
FirstTriggeredAt: time.Now().Add(-time.Minute),
|
|
LastTriggeredAt: time.Now(),
|
|
ReportedAt: time.Now(),
|
|
}
|
|
if err := db.Create(event).Error; err != nil {
|
|
t.Fatalf("create health event: %v", err)
|
|
}
|
|
if err := db.Exec("UPDATE node_health_events SET raw_json = ? WHERE id = ?", string(rawJSON), event.ID).Error; err != nil {
|
|
t.Fatalf("seed legacy raw_json: %v", err)
|
|
}
|
|
|
|
if err := migrateObservabilityLegacyColumns(db); err != nil {
|
|
t.Fatalf("migrateObservabilityLegacyColumns: %v", err)
|
|
}
|
|
|
|
var got NodeHealthEvent
|
|
if err := db.First(&got, event.ID).Error; err != nil {
|
|
t.Fatalf("query health event: %v", err)
|
|
}
|
|
if got.MetadataJSON == "" {
|
|
t.Fatal("expected metadata_json to be backfilled")
|
|
}
|
|
}
|
|
|
|
func TestEnsureDatabaseSchemaUpToDateInitializesFreshDatabase(t *testing.T) {
|
|
db := openBareTestSQLiteDB(t, "fresh-schema.db")
|
|
if err := registerSharding(db, "sqlite"); err != nil {
|
|
t.Fatalf("register sharding: %v", err)
|
|
}
|
|
|
|
if err := ensureDatabaseSchemaUpToDate(db, "sqlite"); err != nil {
|
|
t.Fatalf("ensureDatabaseSchemaUpToDate: %v", err)
|
|
}
|
|
|
|
version, exists, err := loadDatabaseSchemaVersion(db)
|
|
if err != nil {
|
|
t.Fatalf("loadDatabaseSchemaVersion: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected database schema version to be recorded")
|
|
}
|
|
if version != currentDatabaseSchemaVersion {
|
|
t.Fatalf("unexpected schema version: got %d want %d", version, currentDatabaseSchemaVersion)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDatabaseSchemaUpToDateUpgradesLegacyDatabase(t *testing.T) {
|
|
db := openBareTestSQLiteDB(t, "legacy-schema.db")
|
|
if err := registerSharding(db, "sqlite"); err != nil {
|
|
t.Fatalf("register sharding: %v", err)
|
|
}
|
|
if err := autoMigrateAll(db); err != nil {
|
|
t.Fatalf("auto migrate db: %v", err)
|
|
}
|
|
if err := db.Create(&User{
|
|
Username: "legacy",
|
|
Password: "secret",
|
|
DisplayName: "Legacy User",
|
|
Role: 1,
|
|
Status: 1,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed legacy user: %v", err)
|
|
}
|
|
|
|
if err := ensureDatabaseSchemaUpToDate(db, "sqlite"); err != nil {
|
|
t.Fatalf("ensureDatabaseSchemaUpToDate: %v", err)
|
|
}
|
|
|
|
version, exists, err := loadDatabaseSchemaVersion(db)
|
|
if err != nil {
|
|
t.Fatalf("loadDatabaseSchemaVersion: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected legacy database to gain a schema version record")
|
|
}
|
|
if version != currentDatabaseSchemaVersion {
|
|
t.Fatalf("unexpected schema version: got %d want %d", version, currentDatabaseSchemaVersion)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDatabaseSchemaUpToDateMigratesObservabilityShardsToID(t *testing.T) {
|
|
db := openBareTestSQLiteDB(t, "legacy-observability-shards.db")
|
|
if err := registerSharding(db, "sqlite"); err != nil {
|
|
t.Fatalf("register sharding: %v", err)
|
|
}
|
|
if err := autoMigrateAll(db); err != nil {
|
|
t.Fatalf("auto migrate db: %v", err)
|
|
}
|
|
if err := autoMigrateSchemaMetadata(db); err != nil {
|
|
t.Fatalf("auto migrate schema metadata: %v", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
if err := db.Table("node_metric_snapshots_00").Create(&NodeMetricSnapshot{
|
|
ID: 1,
|
|
NodeID: "node-a",
|
|
CapturedAt: now.Add(-2 * time.Minute),
|
|
CPUUsagePercent: 22,
|
|
MemoryUsedBytes: 2,
|
|
MemoryTotalBytes: 8,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed metric snapshot shard 00: %v", err)
|
|
}
|
|
if err := db.Table("node_metric_snapshots_01").Create(&NodeMetricSnapshot{
|
|
ID: 1,
|
|
NodeID: "node-b",
|
|
CapturedAt: now.Add(-time.Minute),
|
|
CPUUsagePercent: 44,
|
|
MemoryUsedBytes: 4,
|
|
MemoryTotalBytes: 8,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed metric snapshot shard 01: %v", err)
|
|
}
|
|
if err := db.Table("node_request_reports_00").Create(&NodeRequestReport{
|
|
ID: 1,
|
|
NodeID: "node-a",
|
|
WindowStartedAt: now.Add(-3 * time.Minute),
|
|
WindowEndedAt: now.Add(-2 * time.Minute),
|
|
RequestCount: 12,
|
|
ErrorCount: 1,
|
|
UniqueVisitorCount: 6,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed request report shard 00: %v", err)
|
|
}
|
|
if err := db.Table("node_request_reports_01").Create(&NodeRequestReport{
|
|
ID: 1,
|
|
NodeID: "node-b",
|
|
WindowStartedAt: now.Add(-2 * time.Minute),
|
|
WindowEndedAt: now.Add(-time.Minute),
|
|
RequestCount: 21,
|
|
ErrorCount: 2,
|
|
UniqueVisitorCount: 9,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed request report shard 01: %v", err)
|
|
}
|
|
if err := db.Table("node_access_logs_00").Create(&NodeAccessLog{
|
|
ID: 1,
|
|
NodeID: "node-a",
|
|
LoggedAt: now.Add(-90 * time.Second),
|
|
RemoteAddr: "203.0.113.10",
|
|
Host: "a.example.com",
|
|
Path: "/alpha",
|
|
StatusCode: 200,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed access log shard 00: %v", err)
|
|
}
|
|
if err := db.Table("node_access_logs_01").Create(&NodeAccessLog{
|
|
ID: 1,
|
|
NodeID: "node-b",
|
|
LoggedAt: now.Add(-60 * time.Second),
|
|
RemoteAddr: "203.0.113.11",
|
|
Host: "b.example.com",
|
|
Path: "/beta",
|
|
StatusCode: 502,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed access log shard 01: %v", err)
|
|
}
|
|
if err := saveDatabaseSchemaVersion(db, 2); err != nil {
|
|
t.Fatalf("save schema version: %v", err)
|
|
}
|
|
|
|
previousDB := DB
|
|
DB = db
|
|
t.Cleanup(func() {
|
|
DB = previousDB
|
|
})
|
|
|
|
if err := ensureDatabaseSchemaUpToDate(db, "sqlite"); err != nil {
|
|
t.Fatalf("ensureDatabaseSchemaUpToDate: %v", err)
|
|
}
|
|
|
|
version, exists, err := loadDatabaseSchemaVersion(db)
|
|
if err != nil {
|
|
t.Fatalf("loadDatabaseSchemaVersion: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("expected migrated database to keep schema version record")
|
|
}
|
|
if version != currentDatabaseSchemaVersion {
|
|
t.Fatalf("unexpected schema version: got %d want %d", version, currentDatabaseSchemaVersion)
|
|
}
|
|
|
|
for _, baseTable := range shardedObservabilityBaseTables() {
|
|
for _, table := range observabilityShardTables(baseTable) {
|
|
legacyTable := legacyObservabilityShardTableName(table)
|
|
if db.Migrator().HasTable(legacyTable) {
|
|
t.Fatalf("expected legacy shard table %s to be removed", legacyTable)
|
|
}
|
|
}
|
|
}
|
|
|
|
snapshots, err := ListMetricSnapshotsSince(time.Time{})
|
|
if err != nil {
|
|
t.Fatalf("ListMetricSnapshotsSince failed: %v", err)
|
|
}
|
|
if len(snapshots) != 2 {
|
|
t.Fatalf("expected 2 migrated metric snapshots, got %+v", snapshots)
|
|
}
|
|
reports, err := ListRequestReportsSince(time.Time{})
|
|
if err != nil {
|
|
t.Fatalf("ListRequestReportsSince failed: %v", err)
|
|
}
|
|
if len(reports) != 2 {
|
|
t.Fatalf("expected 2 migrated request reports, got %+v", reports)
|
|
}
|
|
logs, err := ListNodeAccessLogs(NodeAccessLogQuery{Page: 0, PageSize: 10})
|
|
if err != nil {
|
|
t.Fatalf("ListNodeAccessLogs failed: %v", err)
|
|
}
|
|
if len(logs) != 2 {
|
|
t.Fatalf("expected 2 migrated access logs, got %+v", logs)
|
|
}
|
|
|
|
seenSnapshotIDs := make(map[uint]struct{}, len(snapshots))
|
|
for _, item := range snapshots {
|
|
if item == nil || item.ID == 0 {
|
|
t.Fatalf("expected migrated metric snapshot to have a new non-zero id: %+v", item)
|
|
}
|
|
if _, exists := seenSnapshotIDs[item.ID]; exists {
|
|
t.Fatalf("expected migrated metric snapshot ids to be unique, got duplicate %d", item.ID)
|
|
}
|
|
seenSnapshotIDs[item.ID] = struct{}{}
|
|
targetTable := observabilityShardTableForID("node_metric_snapshots", item.ID)
|
|
var count int64
|
|
if err := db.Table(targetTable).Where("id = ?", item.ID).Count(&count).Error; err != nil {
|
|
t.Fatalf("count migrated metric snapshot in target shard: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("expected migrated metric snapshot id %d to be stored in %s", item.ID, targetTable)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMigrateOriginsSchemaBackfillsOrigins(t *testing.T) {
|
|
db := openBareTestSQLiteDB(t, "legacy-origins.db")
|
|
if err := registerSharding(db, "sqlite"); err != nil {
|
|
t.Fatalf("register sharding: %v", err)
|
|
}
|
|
if err := applyCurrentSchema(db, "sqlite"); err != nil {
|
|
t.Fatalf("applyCurrentSchema: %v", err)
|
|
}
|
|
now := time.Now().UTC()
|
|
route := &ProxyRoute{
|
|
Domain: "app.example.com",
|
|
OriginURL: "https://origin-a.internal:8443/api",
|
|
Upstreams: `["https://origin-a.internal:8443/api"]`,
|
|
Enabled: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if err := db.Create(route).Error; err != nil {
|
|
t.Fatalf("seed proxy route: %v", err)
|
|
}
|
|
if err := db.Exec(`DELETE FROM origins`).Error; err != nil {
|
|
t.Fatalf("clear origins: %v", err)
|
|
}
|
|
if err := db.Model(&ProxyRoute{}).Where("id = ?", route.ID).Update("origin_id", nil).Error; err != nil {
|
|
t.Fatalf("clear route origin_id: %v", err)
|
|
}
|
|
|
|
if err := backfillOriginsFromProxyRoutes(db); err != nil {
|
|
t.Fatalf("backfillOriginsFromProxyRoutes: %v", err)
|
|
}
|
|
|
|
if !db.Migrator().HasTable(&Origin{}) {
|
|
t.Fatal("expected origins table to exist")
|
|
}
|
|
if !db.Migrator().HasColumn(&ProxyRoute{}, "origin_id") {
|
|
t.Fatal("expected proxy_routes.origin_id column to exist")
|
|
}
|
|
|
|
reloadedRoute := &ProxyRoute{}
|
|
if err := db.First(reloadedRoute, route.ID).Error; err != nil {
|
|
t.Fatalf("query proxy route: %v", err)
|
|
}
|
|
if reloadedRoute.OriginID == nil || *reloadedRoute.OriginID == 0 {
|
|
t.Fatal("expected migrated route to be linked to a backfilled origin")
|
|
}
|
|
|
|
origin := &Origin{}
|
|
if err := db.First(origin, *reloadedRoute.OriginID).Error; err != nil {
|
|
t.Fatalf("query origin: %v", err)
|
|
}
|
|
if origin.Address != "origin-a.internal" {
|
|
t.Fatalf("unexpected backfilled origin address: %s", origin.Address)
|
|
}
|
|
}
|
|
|
|
func TestRunDatabaseSchemaMigrationDoesNotAdvanceVersionWhenValidationFails(t *testing.T) {
|
|
db := openBareTestSQLiteDB(t, "failed-validation.db")
|
|
|
|
err := runDatabaseSchemaMigration(db, "sqlite", databaseSchemaMigration{
|
|
fromVersion: legacyDatabaseSchemaVersion,
|
|
toVersion: currentDatabaseSchemaVersion,
|
|
migrate: func(tx *gorm.DB, backend string) error {
|
|
return autoMigrateSchemaMetadata(tx)
|
|
},
|
|
validate: func(tx *gorm.DB, backend string) error {
|
|
return gorm.ErrInvalidDB
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected migration validation to fail")
|
|
}
|
|
|
|
_, exists, loadErr := loadDatabaseSchemaVersion(db)
|
|
if loadErr != nil {
|
|
t.Fatalf("loadDatabaseSchemaVersion: %v", loadErr)
|
|
}
|
|
if exists {
|
|
t.Fatal("expected schema version to remain unset after failed validation")
|
|
}
|
|
}
|