HH-441: harden durable storage and recovery (#92)
* HH-441: harden durable storage and recovery * HH-441: clear recovery review blockers * HH-441: enforce offsite backup failure domain --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestHighRiskMigrationsPreserveRollbackData(t *testing.T) {
|
||||
if os.Getenv("GOCHAT_TEST_DB") == "sqlite" {
|
||||
t.Skip("requires PostgreSQL migration semantics")
|
||||
}
|
||||
dsn := os.Getenv("GOCHAT_TEST_DB_URL")
|
||||
if dsn == "" {
|
||||
dsn = "host=localhost port=5432 user=postgres password=postgres dbname=gochat_test sslmode=disable"
|
||||
}
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
schema := "high_risk_migrations_" + time.Now().Format("20060102150405000000000")
|
||||
require.NoError(t, db.Exec("CREATE SCHEMA "+schema).Error)
|
||||
t.Cleanup(func() { _ = db.Exec("DROP SCHEMA " + schema + " CASCADE").Error })
|
||||
require.NoError(t, db.Exec("SET search_path TO "+schema).Error)
|
||||
require.NoError(t, db.Exec(`
|
||||
CREATE TABLE reporting_events_rollups (
|
||||
id SERIAL PRIMARY KEY, account_id INTEGER NOT NULL, dimension VARCHAR(50) NOT NULL,
|
||||
dimension_value VARCHAR(255) NOT NULL, metric_name VARCHAR(50) NOT NULL,
|
||||
value DOUBLE PRECISION NOT NULL, value_in_business_hours DOUBLE PRECISION,
|
||||
period VARCHAR(50) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ,
|
||||
UNIQUE(account_id, dimension, dimension_value, metric_name, period)
|
||||
);
|
||||
INSERT INTO reporting_events_rollups
|
||||
(account_id, dimension, dimension_value, metric_name, value, value_in_business_hours, period)
|
||||
VALUES (1, 'inbox', '42', 'conversations_count', 7, 3, '2026-08-21');
|
||||
CREATE TABLE custom_attribute_definitions (attribute_model TEXT, attribute_name TEXT);
|
||||
CREATE TABLE conversations (id BIGINT PRIMARY KEY, custom_attributes JSONB, updated_at TIMESTAMPTZ);
|
||||
INSERT INTO custom_attribute_definitions VALUES ('conversation_attribute', 'swt_source_url');
|
||||
INSERT INTO conversations VALUES (1, '{"swt_source_url":"https://example.test"}', NOW());
|
||||
`).Error)
|
||||
|
||||
up48, err := os.ReadFile(filepath.Join("..", "..", "migrations", "000048_fix_reporting_events_rollups_schema.up.sql"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Exec(string(up48)).Error)
|
||||
var rollup struct {
|
||||
DimensionID int64
|
||||
Count int64
|
||||
SumValue float64
|
||||
}
|
||||
require.NoError(t, db.Table("reporting_events_rollups").Select("dimension_id, count, sum_value").Scan(&rollup).Error)
|
||||
assert.Equal(t, int64(42), rollup.DimensionID)
|
||||
assert.Equal(t, int64(7), rollup.Count)
|
||||
assert.Equal(t, float64(7), rollup.SumValue)
|
||||
|
||||
up76, err := os.ReadFile(filepath.Join("..", "..", "migrations", "000076_replace_shangwutong_source_attributes_with_messages.up.sql"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Exec(string(up76)).Error)
|
||||
var definitions, conversations int64
|
||||
require.NoError(t, db.Table("custom_attribute_definitions").Count(&definitions).Error)
|
||||
require.NoError(t, db.Table("conversations").Where("custom_attributes->>'swt_source_url' IS NOT NULL").Count(&conversations).Error)
|
||||
assert.Equal(t, int64(1), definitions)
|
||||
assert.Equal(t, int64(1), conversations)
|
||||
|
||||
down48, err := os.ReadFile(filepath.Join("..", "..", "migrations", "000048_fix_reporting_events_rollups_schema.down.sql"))
|
||||
require.NoError(t, err)
|
||||
require.ErrorContains(t, db.Exec(string(down48)).Error, "irreversible")
|
||||
}
|
||||
|
||||
func TestMigration48RejectsUnsafeLegacyData(t *testing.T) {
|
||||
if os.Getenv("GOCHAT_TEST_DB") == "sqlite" {
|
||||
t.Skip("requires PostgreSQL migration semantics")
|
||||
}
|
||||
dsn := os.Getenv("GOCHAT_TEST_DB_URL")
|
||||
if dsn == "" {
|
||||
dsn = "host=localhost port=5432 user=postgres password=postgres dbname=gochat_test sslmode=disable"
|
||||
}
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
|
||||
up48, err := os.ReadFile(filepath.Join("..", "..", "migrations", "000048_fix_reporting_events_rollups_schema.up.sql"))
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name, values, want string
|
||||
rows int64
|
||||
}{
|
||||
{"invalid_period", "(1, 'inbox', '42', 'conversations_count', 1, 'not-a-date')", "cannot convert to date", 1},
|
||||
{"invalid_dimension", "(1, 'inbox', 'vip', 'conversations_count', 1, '2026-08-21')", "cannot convert to bigint", 1},
|
||||
{"converted_key_collision", "(1, 'inbox', '042', 'conversations_count', 1, '2026-08-21'), (1, 'inbox', '42', 'conversations_count', 2, '2026-08-21')", "unique-key collision", 2},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
schema := "migration_48_rejection_" + tt.name
|
||||
t.Cleanup(func() { _ = db.Exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE").Error })
|
||||
require.NoError(t, db.Exec("DROP SCHEMA IF EXISTS "+schema+" CASCADE").Error)
|
||||
require.NoError(t, db.Exec("CREATE SCHEMA "+schema).Error)
|
||||
require.NoError(t, db.Exec("SET search_path TO "+schema).Error)
|
||||
require.NoError(t, db.Exec(`
|
||||
CREATE TABLE reporting_events_rollups (
|
||||
id SERIAL PRIMARY KEY, account_id INTEGER NOT NULL, dimension VARCHAR(50) NOT NULL,
|
||||
dimension_value VARCHAR(255) NOT NULL, metric_name VARCHAR(50) NOT NULL,
|
||||
value DOUBLE PRECISION NOT NULL, value_in_business_hours DOUBLE PRECISION,
|
||||
period VARCHAR(50) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ,
|
||||
UNIQUE(account_id, dimension, dimension_value, metric_name, period)
|
||||
);
|
||||
INSERT INTO reporting_events_rollups
|
||||
(account_id, dimension, dimension_value, metric_name, value, period)
|
||||
VALUES `+tt.values).Error)
|
||||
|
||||
require.ErrorContains(t, db.Exec(string(up48)).Error, tt.want)
|
||||
require.NoError(t, db.Exec("ROLLBACK").Error)
|
||||
var rows int64
|
||||
require.NoError(t, db.Table(schema+".reporting_events_rollups").Count(&rows).Error)
|
||||
assert.Equal(t, tt.rows, rows)
|
||||
require.NoError(t, db.Exec("SET search_path TO public").Error)
|
||||
require.NoError(t, db.Exec("DROP SCHEMA "+schema+" CASCADE").Error)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user