137 lines
5.1 KiB
Go
137 lines
5.1 KiB
Go
package migrations
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestMigrationPairsAndProductionTables(t *testing.T) {
|
|
upFiles, err := filepath.Glob("*.up.sql")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
downFiles, err := filepath.Glob("*.down.sql")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(upFiles) == 0 || len(upFiles) != len(downFiles) {
|
|
t.Fatalf("migration pairs mismatch: up=%d down=%d", len(upFiles), len(downFiles))
|
|
}
|
|
wantFiles := []string{
|
|
"000001_mvp0_base.up.sql",
|
|
"000002_admin_baseline.up.sql",
|
|
"000003_mvp1_routing.up.sql",
|
|
"000004_mvp1_admin_seed.up.sql",
|
|
"000005_provider_plaintext_credentials.up.sql",
|
|
}
|
|
slices.Sort(upFiles)
|
|
if !slices.Equal(upFiles, wantFiles) {
|
|
t.Fatalf("up migrations = %v, want %v", upFiles, wantFiles)
|
|
}
|
|
|
|
for _, upFile := range upFiles {
|
|
downFile := strings.TrimSuffix(upFile, ".up.sql") + ".down.sql"
|
|
if _, err := os.Stat(downFile); err != nil {
|
|
t.Errorf("%s has no matching down migration: %v", upFile, err)
|
|
}
|
|
}
|
|
|
|
content, err := os.ReadFile("000001_mvp0_base.up.sql")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sql := string(content)
|
|
for _, table := range []string{"users", "providers", "provider_models", "prompt_templates", "generations", "generation_inputs", "generation_outputs"} {
|
|
pattern := regexp.MustCompile(`(?i)CREATE TABLE\s+` + regexp.QuoteMeta(table) + `\s*\(`)
|
|
if !pattern.MatchString(sql) {
|
|
t.Errorf("migration does not create %s", table)
|
|
}
|
|
}
|
|
for _, required := range []string{"rendered_prompt", "attempts", "error_code", "error_message", "lease_token", "uq_generations_user_idempotency", "idx_generations_queue"} {
|
|
if !strings.Contains(sql, required) {
|
|
t.Errorf("migration is missing %s", required)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMVP1MigrationContracts(t *testing.T) {
|
|
read := func(name string) string {
|
|
t.Helper()
|
|
content, err := os.ReadFile(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(content)
|
|
}
|
|
|
|
adminBaseline := read("000002_admin_baseline.up.sql")
|
|
for _, table := range []string{
|
|
"sys_user", "sys_role", "sys_menu", "sys_api", "sys_role_menu", "sys_menu_api_rule",
|
|
"sys_casbin_rule", "sys_tables", "sys_columns",
|
|
} {
|
|
if !regexp.MustCompile(`(?i)CREATE TABLE\s+` + regexp.QuoteMeta(table) + `\s*\(`).MatchString(adminBaseline) {
|
|
t.Errorf("admin baseline does not create %s", table)
|
|
}
|
|
}
|
|
if strings.Contains(strings.ToLower(adminBaseline), "automigrate") {
|
|
t.Error("admin baseline must not use AutoMigrate")
|
|
}
|
|
|
|
routing := read("000003_mvp1_routing.up.sql")
|
|
for _, required := range []string{
|
|
"provider_credentials", "provider_model_capabilities", "route_pools", "active_routes",
|
|
"route_pool_members", "route_member_runtime", "provider_connectivity_checks", "admin_audit_events",
|
|
"active_credential_id", "route_snapshot", "provider_attempt_count", "default_role_rule", "note",
|
|
"uq_provider_credentials_version", "uq_route_pools_slug", "uq_route_pool_members_model",
|
|
"idx_provider_connectivity_checks_model", "uq_admin_audit_events_request_action",
|
|
"DROP CHECK chk_providers_auth", "UPDATE providers SET api_key_enc = NULL",
|
|
} {
|
|
if !strings.Contains(routing, required) {
|
|
t.Errorf("MVP-1 routing migration is missing %s", required)
|
|
}
|
|
}
|
|
|
|
seed := read("000004_mvp1_admin_seed.up.sql")
|
|
for _, required := range []string{"chorus_operator", "chorus.providers.credential.rotate", "sys_role_menu", "sys_menu_api_rule", "sys_casbin_rule"} {
|
|
if !strings.Contains(seed, required) {
|
|
t.Errorf("admin seed is missing %s", required)
|
|
}
|
|
}
|
|
if strings.Contains(seed, "INSERT INTO sys_user") {
|
|
t.Error("admin seed must not create a known administrator account")
|
|
}
|
|
plaintext := read("000005_provider_plaintext_credentials.up.sql")
|
|
for _, required := range []string{
|
|
"chorus_plaintext_credentials_up_guard", "EXISTS (SELECT 1 FROM provider_credentials)",
|
|
"DROP COLUMN api_key_enc", "DROP COLUMN key_id", "ADD COLUMN api_key VARCHAR(4096)",
|
|
"chorus.providers.credential.get", "Cache-Control",
|
|
} {
|
|
if !strings.Contains(plaintext, required) {
|
|
t.Errorf("plaintext credential migration is missing %s", required)
|
|
}
|
|
}
|
|
plaintextDown := read("000005_provider_plaintext_credentials.down.sql")
|
|
for _, required := range []string{"chorus_plaintext_credentials_down_guard", "EXISTS (SELECT 1 FROM provider_credentials)", "ADD COLUMN api_key_enc JSON", "ADD COLUMN key_id"} {
|
|
if !strings.Contains(plaintextDown, required) {
|
|
t.Errorf("plaintext credential rollback is missing %s", required)
|
|
}
|
|
}
|
|
adminDown := read("000002_admin_baseline.down.sql")
|
|
for _, required := range []string{"chorus_admin_baseline_down_guard", "INSERT INTO chorus_admin_baseline_down_guard", "EXISTS (SELECT 1 FROM sys_user)"} {
|
|
if !strings.Contains(adminDown, required) {
|
|
t.Errorf("admin baseline down migration is missing rollback guard %s", required)
|
|
}
|
|
}
|
|
|
|
for _, name := range []string{"000002_admin_baseline.down.sql", "000003_mvp1_routing.down.sql", "000004_mvp1_admin_seed.down.sql"} {
|
|
down := read(name)
|
|
if strings.Contains(strings.ToLower(down), "delete from sys_menu where title") || strings.Contains(strings.ToLower(down), "delete from sys_api where title") {
|
|
t.Errorf("%s must delete Chorus seed data by stable keys, not titles", name)
|
|
}
|
|
}
|
|
}
|