RKNnoVPN/runtime/internal/config/storage_test.go

181 lines
7.2 KiB
Go

package config
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestValidateRejectsUnsupportedSchemaVersion(t *testing.T) {
cfg := DefaultConfig()
cfg.SchemaVersion = CurrentSchemaVersion + 1
if err := cfg.Validate(); err == nil {
t.Fatalf("newer schema version should be rejected")
}
cfg.SchemaVersion = CurrentSchemaVersion - 1
if err := cfg.Validate(); err == nil {
t.Fatalf("older schema version should be rejected")
}
}
func TestValidateChecksProfileProjectionSchema(t *testing.T) {
cfg := DefaultConfig()
cfg.Profile.Nodes = []json.RawMessage{
json.RawMessage(`{"id":"node-1","protocol":"vless","server":"example.com","port":443,"stale":true,"source":{"type":"MANUAL"}}`),
}
if err := cfg.Validate(); err == nil {
t.Fatalf("config validation should reject invalid profile projection schema")
}
}
func TestValidateRequiresClashAPISecretWhenEnabled(t *testing.T) {
cfg := DefaultConfig()
cfg.Proxy.APIPort = 9090
if err := cfg.Validate(); err == nil {
t.Fatalf("api_port without api_secret should be rejected")
}
cfg.Proxy.APISecret = "secret"
if err := cfg.Validate(); err != nil {
t.Fatalf("api_port with api_secret should validate: %v", err)
}
}
func TestWriteFileAtomicReplacesFileWithoutLeavingTemp(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
if err := os.WriteFile(path, []byte("old"), 0600); err != nil {
t.Fatal(err)
}
if err := writeFileAtomic(path, []byte("new\n"), 0600, "config"); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != "new\n" {
t.Fatalf("atomic replacement wrote %q", string(data))
}
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
t.Fatalf("temporary file should be gone after atomic write, stat err=%v", err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0600 {
t.Fatalf("atomic replacement mode = %o, want 0600", info.Mode().Perm())
}
}
func TestLoadRejectsRemovedTopLevelNodeAndTransport(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
raw := []byte(`{
"schema_version": 5,
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
"transport": {"protocol":"reality","tls_server":"","fingerprint":"chrome"},
"node": [],
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","fake_ip":false},
"ipv6": {"mode":"mirror"},
"sharing": {"enabled":false},
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
"autostart": false
}`)
if err := os.WriteFile(path, raw, 0600); err != nil {
t.Fatal(err)
}
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "transport") {
t.Fatalf("removed top-level transport/node fields must be rejected, got %v", err)
}
}
func TestLoadRejectsRemovedBlockQuicDNSField(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
raw := []byte(`{
"schema_version": 5,
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","block_quic_dns":true,"fake_ip":false},
"ipv6": {"mode":"mirror"},
"sharing": {"enabled":false},
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
"autostart": false
}`)
if err := os.WriteFile(path, raw, 0600); err != nil {
t.Fatal(err)
}
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "block_quic_dns") {
t.Fatalf("removed block_quic_dns field should be rejected, got %v", err)
}
}
func TestLoadRejectsRemovedProfileProjectionPayloads(t *testing.T) {
for _, field := range []string{"tun", "extra", "subscriptions"} {
t.Run(field, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
raw := []byte(`{
"schema_version": 5,
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
"profile": {"id":"default","name":"Default","` + field + `":{}},
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","fake_ip":false},
"ipv6": {"mode":"mirror"},
"sharing": {"enabled":false},
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
"autostart": false
}`)
if err := os.WriteFile(path, raw, 0600); err != nil {
t.Fatal(err)
}
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), field) {
t.Fatalf("removed profile.%s field should be rejected, got %v", field, err)
}
})
}
}
func TestLoadRejectsApkOnlyProfileNodeMetadata(t *testing.T) {
for _, field := range []string{"source", "createdAt", "latencyMs", "responseMs", "throughputBps", "testStatus"} {
t.Run(field, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
raw := []byte(`{
"schema_version": 5,
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
"profile": {
"id":"default",
"name":"Default",
"nodes":[{"id":"node-1","name":"Node","protocol":"vless","server":"example.com","port":443,"outbound":{}, "` + field + `": "removed"}]
},
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","fake_ip":false},
"ipv6": {"mode":"mirror"},
"sharing": {"enabled":false},
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
"autostart": false
}`)
if err := os.WriteFile(path, raw, 0600); err != nil {
t.Fatal(err)
}
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), field) {
t.Fatalf("APK-only profile node field %s should be rejected, got %v", field, err)
}
})
}
}