mirror of
https://github.com/alireza0/s-ui.git
synced 2026-10-24 12:54:32 +00:00
database: migrate options deprecated in sing-box 1.14
One-shot migration over the stored config and TLS rows, guarded by a setting so it runs once: - dns.independent_cache is dropped (1.14 reads it only to warn; the value is never passed to the DNS client) - experimental.cache_file.store_rdrc becomes store_dns - route.rule_set[].download_detour becomes the http_client 1.14 builds for it - inline tls.acme becomes tls.certificate_provider with type "acme" Legacy DNS address filters and the `strategy` DNS rule action are NOT migrated automatically. Converting them means inserting evaluate rules and reordering, which changes how names resolve, so they are logged with the upstream migration link for the operator to handle. Tests cover each rewrite, that unrelated options survive, that a config with nothing to migrate is left byte-identical, and that the migrated shapes build a box with no deprecation warning.
This commit is contained in:
parent
8089e95a5e
commit
f41da0dfdd
8 changed files with 581 additions and 0 deletions
5
core/acme_built_stub_test.go
Normal file
5
core/acme_built_stub_test.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//go:build !with_acme
|
||||
|
||||
package core
|
||||
|
||||
const acmeBuilt = false
|
||||
7
core/acme_built_test.go
Normal file
7
core/acme_built_test.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//go:build with_acme
|
||||
|
||||
package core
|
||||
|
||||
// acmeBuilt reports whether this build includes ACME, so tests over configs
|
||||
// that declare an ACME certificate provider can skip when it is absent.
|
||||
const acmeBuilt = true
|
||||
|
|
@ -123,3 +123,55 @@ func TestConfigCompat(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigCompatClean asserts that the post-migration shapes produced by
|
||||
// database.migrateSingBox114 start without any deprecation warning, so the
|
||||
// migration is known to actually resolve what it claims to.
|
||||
//
|
||||
// These configs are only constructed, not started: an ACME provider would try
|
||||
// to reach Let's Encrypt on start. Every deprecation these shapes could raise
|
||||
// is reported while the box is being built.
|
||||
func TestConfigCompatClean(t *testing.T) {
|
||||
logger.InitLogger(logging.ERROR)
|
||||
acmeDir := t.TempDir()
|
||||
|
||||
files, err := filepath.Glob(filepath.Join("testdata", "clean", "*.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Fatal("no clean test configs found")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
t.Run(filepath.Base(file), func(t *testing.T) {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := strings.ReplaceAll(string(raw), "__ACMEDIR__", acmeDir)
|
||||
if !acmeBuilt && strings.Contains(data, `"type": "acme"`) {
|
||||
t.Skip("build does not include ACME")
|
||||
}
|
||||
|
||||
ctx := Context(context.Background(), InboundRegistry(), OutboundRegistry(),
|
||||
EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry(), CertificateProviderRegistry())
|
||||
notes := &collectManager{}
|
||||
ctx = service.ContextWith[deprecated.Manager](ctx, notes)
|
||||
|
||||
var opts option.Options
|
||||
if err = opts.UnmarshalJSONContext(ctx, []byte(data)); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
instance, err := NewBox(Options{Context: ctx, Options: opts})
|
||||
if err != nil {
|
||||
t.Fatalf("create box: %v", err)
|
||||
}
|
||||
instance.Close()
|
||||
|
||||
for _, note := range notes.notes {
|
||||
t.Errorf("migrated config still reports deprecation %q: %s", note.Name, note.Description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
core/testdata/clean/migrated-acme.json
vendored
Normal file
18
core/testdata/clean/migrated-acme.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"log": {"level": "error"},
|
||||
"inbounds": [{
|
||||
"type": "vless", "tag": "vless-in", "listen": "127.0.0.1", "listen_port": 42444,
|
||||
"users": [{"uuid": "b831381d-6324-4d53-ad4f-8cda48b30811"}],
|
||||
"tls": {
|
||||
"enabled": true, "server_name": "example.com",
|
||||
"certificate_provider": {
|
||||
"type": "acme",
|
||||
"domain": ["example.com"],
|
||||
"email": "a@example.com",
|
||||
"data_directory": "__ACMEDIR__",
|
||||
"dns01_challenge": {"provider": "cloudflare", "api_token": "tok"}
|
||||
}
|
||||
}
|
||||
}],
|
||||
"outbounds": [{"type": "direct", "tag": "direct"}]
|
||||
}
|
||||
6
core/testdata/clean/migrated-cache.json
vendored
Normal file
6
core/testdata/clean/migrated-cache.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"log": {"level": "error"},
|
||||
"dns": {"servers": [{"type": "udp", "tag": "google", "server": "8.8.8.8"}], "rules": [], "cache_capacity": 4096},
|
||||
"experimental": {"cache_file": {"enabled": true, "store_dns": true}},
|
||||
"outbounds": [{"type": "direct", "tag": "direct"}]
|
||||
}
|
||||
|
|
@ -124,6 +124,12 @@ func InitDB(dbPath string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = migrateSingBox114()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reportSingBox114Manual()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
300
database/migration_singbox114.go
Normal file
300
database/migration_singbox114.go
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/alireza0/s-ui/database/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// migratedKeySingBox114 marks that the one-shot sing-box 1.14 option migration
|
||||
// has already run, so it is not attempted again on every start.
|
||||
const migratedKeySingBox114 = "migratedSingBox114"
|
||||
|
||||
// migrateSingBox114 rewrites the options sing-box deprecated in 1.14.0 into
|
||||
// their replacements. Only the lossless rewrites are done automatically:
|
||||
//
|
||||
// - dns.independent_cache is dropped; 1.14 ignores the field entirely
|
||||
// - experimental.cache_file.store_rdrc becomes store_dns
|
||||
// - route.rule_set[].download_detour becomes http_client
|
||||
// - an inline tls.acme block becomes tls.certificate_provider
|
||||
//
|
||||
// The two remaining 1.14 deprecations, legacy DNS address filters and the
|
||||
// `strategy` DNS rule action option, are deliberately left alone: migrating
|
||||
// them means reordering DNS rules and inserting evaluate actions, which
|
||||
// changes how names resolve. Those are reported by reportSingBox114Manual so
|
||||
// the operator can convert them by hand.
|
||||
func migrateSingBox114() error {
|
||||
var flag model.Setting
|
||||
err := db.Where("key = ?", migratedKeySingBox114).First(&flag).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
changed, err := migrateSingBox114Config(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsChanged, err := migrateSingBox114Tls(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changed += tlsChanged
|
||||
if changed > 0 {
|
||||
log.Printf("sing-box 1.14: migrated %d deprecated option(s)", changed)
|
||||
}
|
||||
return tx.Create(&model.Setting{Key: migratedKeySingBox114, Value: "true"}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// migrateSingBox114Config migrates the dns, experimental and route sections
|
||||
// held in the `config` setting, returning how many options it rewrote.
|
||||
func migrateSingBox114Config(tx *gorm.DB) (int, error) {
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", "config").First(&setting).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var root map[string]json.RawMessage
|
||||
if err = json.Unmarshal([]byte(setting.Value), &root); err != nil {
|
||||
// A config we cannot parse is left untouched rather than discarded.
|
||||
log.Printf("sing-box 1.14: skipping config migration, cannot parse: %v", err)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
changed := 0
|
||||
sections := []struct {
|
||||
key string
|
||||
migrate func(map[string]any) int
|
||||
}{
|
||||
{"dns", migrateDNSSection},
|
||||
{"experimental", migrateExperimentalSection},
|
||||
{"route", migrateRouteSection},
|
||||
}
|
||||
for _, section := range sections {
|
||||
if err = migrateSection(root, section.key, section.migrate, &changed); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if changed == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
encoded, err := json.MarshalIndent(root, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setting.Value = string(encoded)
|
||||
if err = tx.Save(&setting).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// migrateSection decodes one top-level config section, hands it to migrate and
|
||||
// writes it back when migrate reports a change.
|
||||
func migrateSection(root map[string]json.RawMessage, key string, migrate func(map[string]any) int, changed *int) error {
|
||||
raw, ok := root[key]
|
||||
if !ok || len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
var section map[string]any
|
||||
if err := json.Unmarshal(raw, §ion); err != nil {
|
||||
return nil
|
||||
}
|
||||
count := migrate(section)
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
encoded, err := json.Marshal(section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root[key] = encoded
|
||||
*changed += count
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateDNSSection drops independent_cache, which 1.14 reads only to warn on.
|
||||
func migrateDNSSection(dns map[string]any) int {
|
||||
if _, ok := dns["independent_cache"]; !ok {
|
||||
return 0
|
||||
}
|
||||
delete(dns, "independent_cache")
|
||||
return 1
|
||||
}
|
||||
|
||||
// migrateExperimentalSection renames cache_file.store_rdrc to store_dns.
|
||||
func migrateExperimentalSection(experimental map[string]any) int {
|
||||
cacheFile, ok := experimental["cache_file"].(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
storeRDRC, ok := cacheFile["store_rdrc"]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
delete(cacheFile, "store_rdrc")
|
||||
if enabled, isBool := storeRDRC.(bool); isBool && enabled {
|
||||
cacheFile["store_dns"] = true
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// migrateRouteSection converts each remote rule-set's download_detour into the
|
||||
// equivalent http_client, matching what 1.14 builds internally for the legacy
|
||||
// field.
|
||||
func migrateRouteSection(route map[string]any) int {
|
||||
ruleSets, ok := route["rule_set"].([]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
changed := 0
|
||||
for _, entry := range ruleSets {
|
||||
ruleSet, isObject := entry.(map[string]any)
|
||||
if !isObject {
|
||||
continue
|
||||
}
|
||||
detour, hasDetour := ruleSet["download_detour"].(string)
|
||||
if !hasDetour {
|
||||
continue
|
||||
}
|
||||
delete(ruleSet, "download_detour")
|
||||
// http_client and download_detour conflict in 1.14, so an existing
|
||||
// http_client wins and the legacy field is simply dropped.
|
||||
if _, hasClient := ruleSet["http_client"]; hasClient {
|
||||
changed++
|
||||
continue
|
||||
}
|
||||
if detour != "" {
|
||||
ruleSet["http_client"] = map[string]any{
|
||||
"detour": detour,
|
||||
"disable_empty_direct_check": true,
|
||||
}
|
||||
}
|
||||
changed++
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// migrateSingBox114Tls moves an inline acme block on every stored TLS config
|
||||
// into certificate_provider, which carries the same fields under a type.
|
||||
func migrateSingBox114Tls(tx *gorm.DB) (int, error) {
|
||||
var configs []model.Tls
|
||||
if err := tx.Find(&configs).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
changed := 0
|
||||
for _, tlsConfig := range configs {
|
||||
if len(tlsConfig.Server) == 0 {
|
||||
continue
|
||||
}
|
||||
var server map[string]any
|
||||
if err := json.Unmarshal(tlsConfig.Server, &server); err != nil {
|
||||
continue
|
||||
}
|
||||
acme, ok := server["acme"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(server, "acme")
|
||||
// certificate_provider and acme cannot both apply, so an existing
|
||||
// provider wins and the legacy block is dropped.
|
||||
if _, hasProvider := server["certificate_provider"]; !hasProvider {
|
||||
provider := make(map[string]any, len(acme)+1)
|
||||
for key, value := range acme {
|
||||
provider[key] = value
|
||||
}
|
||||
provider["type"] = "acme"
|
||||
server["certificate_provider"] = provider
|
||||
}
|
||||
encoded, err := json.Marshal(server)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = tx.Model(&model.Tls{}).Where("id = ?", tlsConfig.Id).
|
||||
Update("server", json.RawMessage(encoded)).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
changed++
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// reportSingBox114Manual logs the 1.14 deprecations that need a human decision.
|
||||
// Both change how DNS names resolve, so they are reported rather than rewritten.
|
||||
func reportSingBox114Manual() {
|
||||
var setting model.Setting
|
||||
if err := db.Where("key = ?", "config").First(&setting).Error; err != nil {
|
||||
return
|
||||
}
|
||||
var root struct {
|
||||
DNS struct {
|
||||
Rules []map[string]any `json:"rules"`
|
||||
} `json:"dns"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(setting.Value), &root); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
addressFilter, strategy := 0, 0
|
||||
for _, rule := range root.DNS.Rules {
|
||||
if dnsRuleUsesAddressFilter(rule) {
|
||||
addressFilter++
|
||||
}
|
||||
if _, ok := rule["strategy"]; ok {
|
||||
strategy++
|
||||
}
|
||||
}
|
||||
if addressFilter > 0 {
|
||||
log.Printf("sing-box 1.14: %d DNS rule(s) use legacy address filters (ip_cidr, ip_is_private, "+
|
||||
"ip_accept_any or an IP rule-set). They still work but are removed in 1.16; migrate them to "+
|
||||
"match_response: https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching",
|
||||
addressFilter)
|
||||
}
|
||||
if strategy > 0 {
|
||||
log.Printf("sing-box 1.14: %d DNS rule(s) use the deprecated `strategy` action option. "+
|
||||
"They still work but are removed in 1.16; migrate them to rule items: "+
|
||||
"https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items",
|
||||
strategy)
|
||||
}
|
||||
}
|
||||
|
||||
// dnsRuleUsesAddressFilter reports whether a DNS rule matches on the resolved
|
||||
// address without opting into response matching.
|
||||
func dnsRuleUsesAddressFilter(rule map[string]any) bool {
|
||||
if matchResponse, ok := rule["match_response"].(bool); ok && matchResponse {
|
||||
return false
|
||||
}
|
||||
for _, field := range []string{"ip_cidr", "rule_set", "ip_is_private", "ip_accept_any", "rule_set_ip_cidr_accept_empty"} {
|
||||
value, ok := rule[field]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
if typed {
|
||||
return true
|
||||
}
|
||||
case []any:
|
||||
if len(typed) > 0 {
|
||||
return true
|
||||
}
|
||||
case string:
|
||||
if typed != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
187
database/migration_singbox114_test.go
Normal file
187
database/migration_singbox114_test.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/alireza0/s-ui/database/model"
|
||||
)
|
||||
|
||||
// openTestDB gives each test its own on-disk database, since OpenDB stores the
|
||||
// handle in the package-level db used by the migration.
|
||||
func openTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
if err := OpenDB(filepath.Join(t.TempDir(), "test.db")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Setting{}, &model.Tls{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func readConfig(t *testing.T) map[string]any {
|
||||
t.Helper()
|
||||
var setting model.Setting
|
||||
if err := db.Where("key = ?", "config").First(&setting).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var root map[string]any
|
||||
if err := json.Unmarshal([]byte(setting.Value), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
func section(t *testing.T, root map[string]any, key string) map[string]any {
|
||||
t.Helper()
|
||||
value, ok := root[key].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing %q section in %v", key, root)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func TestMigrateSingBox114Config(t *testing.T) {
|
||||
openTestDB(t)
|
||||
legacy := `{
|
||||
"log": {"level": "info"},
|
||||
"dns": {"servers": [], "rules": [], "independent_cache": true, "cache_capacity": 4096},
|
||||
"experimental": {"cache_file": {"enabled": true, "store_rdrc": true}},
|
||||
"route": {
|
||||
"rules": [{"action": "sniff"}],
|
||||
"rule_set": [
|
||||
{"type": "remote", "tag": "geoip-cn", "url": "https://example.com/a.srs", "download_detour": "direct"},
|
||||
{"type": "remote", "tag": "geosite-ads", "url": "https://example.com/b.srs"}
|
||||
]
|
||||
}
|
||||
}`
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: legacy}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := migrateSingBox114(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := readConfig(t)
|
||||
|
||||
dns := section(t, root, "dns")
|
||||
if _, ok := dns["independent_cache"]; ok {
|
||||
t.Error("independent_cache should have been dropped")
|
||||
}
|
||||
if dns["cache_capacity"] != float64(4096) {
|
||||
t.Errorf("unrelated dns options must be preserved, got %v", dns["cache_capacity"])
|
||||
}
|
||||
if root["log"] == nil {
|
||||
t.Error("unrelated top-level sections must be preserved")
|
||||
}
|
||||
|
||||
cacheFile := section(t, section(t, root, "experimental"), "cache_file")
|
||||
if _, ok := cacheFile["store_rdrc"]; ok {
|
||||
t.Error("store_rdrc should have been renamed")
|
||||
}
|
||||
if cacheFile["store_dns"] != true {
|
||||
t.Errorf("store_rdrc should become store_dns, got %v", cacheFile["store_dns"])
|
||||
}
|
||||
|
||||
ruleSets, ok := section(t, root, "route")["rule_set"].([]any)
|
||||
if !ok || len(ruleSets) != 2 {
|
||||
t.Fatalf("expected 2 rule sets, got %v", ruleSets)
|
||||
}
|
||||
migrated, _ := ruleSets[0].(map[string]any)
|
||||
if _, ok = migrated["download_detour"]; ok {
|
||||
t.Error("download_detour should have been replaced")
|
||||
}
|
||||
httpClient, ok := migrated["http_client"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected http_client, got %v", migrated)
|
||||
}
|
||||
if httpClient["detour"] != "direct" || httpClient["disable_empty_direct_check"] != true {
|
||||
t.Errorf("unexpected http_client: %v", httpClient)
|
||||
}
|
||||
if untouched, _ := ruleSets[1].(map[string]any); untouched["http_client"] != nil {
|
||||
t.Error("rule sets without download_detour must be left alone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateSingBox114Tls(t *testing.T) {
|
||||
openTestDB(t)
|
||||
server := `{"enabled": true, "server_name": "example.com", "acme": {"domain": ["example.com"], "email": "a@example.com", "dns01_challenge": {"provider": "cloudflare", "api_token": "tok"}}}`
|
||||
if err := db.Create(&model.Tls{Name: "cert", Server: json.RawMessage(server)}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := migrateSingBox114(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var stored model.Tls
|
||||
if err := db.First(&stored).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(stored.Server, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := decoded["acme"]; ok {
|
||||
t.Error("inline acme should have been removed")
|
||||
}
|
||||
if decoded["server_name"] != "example.com" {
|
||||
t.Error("unrelated tls options must be preserved")
|
||||
}
|
||||
provider, ok := decoded["certificate_provider"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected certificate_provider, got %v", decoded)
|
||||
}
|
||||
if provider["type"] != "acme" {
|
||||
t.Errorf("expected type acme, got %v", provider["type"])
|
||||
}
|
||||
if provider["email"] != "a@example.com" {
|
||||
t.Errorf("acme fields must carry over, got %v", provider)
|
||||
}
|
||||
if _, ok = provider["dns01_challenge"].(map[string]any); !ok {
|
||||
t.Errorf("nested acme options must carry over, got %v", provider)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrateSingBox114RunsOnce guards that a config edited back to a legacy
|
||||
// shape after the migration is not silently rewritten again.
|
||||
func TestMigrateSingBox114RunsOnce(t *testing.T) {
|
||||
openTestDB(t)
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: `{"dns": {"independent_cache": true}}`}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migrateSingBox114(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Model(&model.Setting{}).Where("key = ?", "config").
|
||||
Update("value", `{"dns": {"independent_cache": true}}`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migrateSingBox114(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := section(t, readConfig(t), "dns")["independent_cache"]; !ok {
|
||||
t.Error("migration should not run a second time")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrateSingBox114Idempotent guards the common case: a config that has
|
||||
// nothing to migrate must come through byte-identical.
|
||||
func TestMigrateSingBox114Idempotent(t *testing.T) {
|
||||
openTestDB(t)
|
||||
clean := `{"log":{"level":"info"},"dns":{"servers":[],"rules":[]},"experimental":{}}`
|
||||
if err := db.Create(&model.Setting{Key: "config", Value: clean}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migrateSingBox114(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var setting model.Setting
|
||||
if err := db.Where("key = ?", "config").First(&setting).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if setting.Value != clean {
|
||||
t.Errorf("config with nothing to migrate was rewritten:\n got %s\nwant %s", setting.Value, clean)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue