summaryrefslogtreecommitdiff
path: root/internal/control/runtime_test.go
diff options
context:
space:
mode:
authorsergei <sergei@em-sysadmin.xyz>2026-04-14 06:23:55 +0400
committersergei <sergei@em-sysadmin.xyz>2026-04-14 06:23:55 +0400
commit3d51aa455006903345f554a2dd90034993796114 (patch)
tree62a7be2faf047f5eb7886feebc3b815556f03d7f /internal/control/runtime_test.go
downloadvpnem-main.tar.gz
vpnem-main.tar.bz2
vpnem-main.zip
vpnem: VPN infrastructure with load-balanced multi-protocol nodesHEADmain
- Multi-protocol VPS nodes (VLESS-REALITY + Hysteria2 + SOCKS5) - Smart load balancing via recommendation API - Windows/Linux client (Go + Wails + sing-box) - Server API with RealIP detection and connection tracking - Auto-deployment via vpnui control plane - Silent Windows installer with UAC elevation - Load-based server recommendation (no sticky sessions) - Best Server one-click connection workflow
Diffstat (limited to 'internal/control/runtime_test.go')
-rw-r--r--internal/control/runtime_test.go307
1 files changed, 307 insertions, 0 deletions
diff --git a/internal/control/runtime_test.go b/internal/control/runtime_test.go
new file mode 100644
index 0000000..1f38dd7
--- /dev/null
+++ b/internal/control/runtime_test.go
@@ -0,0 +1,307 @@
+package control
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestRenderRuntimeBundle(t *testing.T) {
+ t.Parallel()
+
+ dir := t.TempDir()
+ node := Node{
+ ID: "nl-01",
+ Name: "NL 01",
+ Region: "nl",
+ Host: "203.0.113.10",
+ Domain: "nl-01.example.com",
+ ACMEEmail: "admin@example.com",
+ Enabled: true,
+ SSH: SSHConfig{
+ User: "root",
+ Port: 22,
+ Auth: "key",
+ },
+ Protocols: []ProtocolProfile{
+ {
+ Type: "vless",
+ Enabled: true,
+ Port: 443,
+ TLS: &TLSProfile{
+ Enabled: true,
+ ServerName: "nl-01.example.com",
+ },
+ Auth: &AuthProfile{
+ UUID: "11111111-1111-1111-1111-111111111111",
+ },
+ Extra: map[string]any{
+ "path": "/ws",
+ },
+ },
+ },
+ }
+
+ if err := RenderRuntimeBundle(dir, node, "20260401-123000"); err != nil {
+ t.Fatalf("RenderRuntimeBundle error = %v", err)
+ }
+
+ data, err := os.ReadFile(filepath.Join(dir, "docker-compose.yml"))
+ if err != nil {
+ t.Fatalf("ReadFile docker-compose.yml error = %v", err)
+ }
+ if !strings.Contains(string(data), "sing-box:") {
+ t.Fatal("expected sing-box service in runtime compose")
+ }
+ if !strings.Contains(string(data), "caddy:") {
+ t.Fatal("expected caddy service in runtime compose")
+ }
+
+ caddyfile, err := os.ReadFile(filepath.Join(dir, "Caddyfile"))
+ if err != nil {
+ t.Fatalf("ReadFile Caddyfile error = %v", err)
+ }
+ if !strings.Contains(string(caddyfile), "nl-01.example.com") {
+ t.Fatal("expected domain in Caddyfile")
+ }
+
+ serverConfig, err := os.ReadFile(filepath.Join(dir, "sing-box.server.json"))
+ if err != nil {
+ t.Fatalf("ReadFile sing-box.server.json error = %v", err)
+ }
+ if !strings.Contains(string(serverConfig), "\"type\": \"vless\"") {
+ t.Fatal("expected vless inbound in sing-box config")
+ }
+}
+
+func TestRenderRuntimeBundleReality(t *testing.T) {
+ t.Parallel()
+
+ dir := t.TempDir()
+ node := Node{
+ ID: "nl-reality",
+ Name: "NL Reality",
+ Region: "nl",
+ Host: "203.0.113.20",
+ Enabled: true,
+ SSH: SSHConfig{
+ User: "root",
+ Port: 22,
+ Auth: "key",
+ },
+ Protocols: []ProtocolProfile{
+ {
+ Type: "vless-reality",
+ Enabled: true,
+ Port: 443,
+ Auth: &AuthProfile{
+ UUID: "33333333-3333-3333-3333-333333333333",
+ },
+ Reality: &VLESSRealityProfile{
+ ServerName: "login.microsoftonline.com",
+ ServerPort: 443,
+ PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+ PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+ ShortID: "0123456789abcdef",
+ Fingerprint: "chrome",
+ },
+ },
+ },
+ }
+
+ if err := RenderRuntimeBundle(dir, node, "20260408-180000"); err != nil {
+ t.Fatalf("RenderRuntimeBundle error = %v", err)
+ }
+
+ data, err := os.ReadFile(filepath.Join(dir, "docker-compose.yml"))
+ if err != nil {
+ t.Fatalf("ReadFile docker-compose.yml error = %v", err)
+ }
+ if !strings.Contains(string(data), "sing-box:") {
+ t.Fatal("expected sing-box service in runtime compose")
+ }
+ if strings.Contains(string(data), "caddy:") {
+ t.Fatal("did not expect caddy service for reality runtime")
+ }
+
+ if _, err := os.Stat(filepath.Join(dir, "Caddyfile")); !os.IsNotExist(err) {
+ t.Fatal("did not expect Caddyfile for reality runtime")
+ }
+
+ serverConfig, err := os.ReadFile(filepath.Join(dir, "sing-box.server.json"))
+ if err != nil {
+ t.Fatalf("ReadFile sing-box.server.json error = %v", err)
+ }
+ s := string(serverConfig)
+ if !strings.Contains(s, "\"private_key\": \"UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc\"") {
+ t.Fatal("expected reality private key in sing-box config")
+ }
+ if !strings.Contains(s, "\"short_id\": [") || !strings.Contains(s, "0123456789abcdef") {
+ t.Fatal("expected reality short id in sing-box config")
+ }
+ if !strings.Contains(s, "login.microsoftonline.com") {
+ t.Fatal("expected reality handshake destination in sing-box config")
+ }
+}
+
+func TestHysteria2Bundle(t *testing.T) {
+ t.Parallel()
+
+ dir := t.TempDir()
+ node := Node{
+ ID: "nl-hy2",
+ Name: "NL Hysteria2",
+ Region: "nl",
+ Host: "203.0.113.30",
+ Enabled: true,
+ SSH: SSHConfig{
+ User: "root",
+ Port: 22,
+ Auth: "key",
+ },
+ Protocols: []ProtocolProfile{
+ {
+ Type: "hysteria2",
+ Enabled: true,
+ Port: 443,
+ Auth: &AuthProfile{
+ Password: "user-password",
+ },
+ Hysteria2: &Hysteria2Profile{
+ Port: 443,
+ UpMbps: 100,
+ DownMbps: 100,
+ ObfsPassword: "obfs-password",
+ UserPassword: "user-password",
+ CertPath: "/etc/sing-box/cert.pem",
+ KeyPath: "/etc/sing-box/key.pem",
+ },
+ },
+ },
+ }
+
+ if err := RenderRuntimeBundle(dir, node, "20260408-220000"); err != nil {
+ t.Fatalf("RenderRuntimeBundle error = %v", err)
+ }
+
+ data, err := os.ReadFile(filepath.Join(dir, "docker-compose.yml"))
+ if err != nil {
+ t.Fatalf("ReadFile docker-compose.yml error = %v", err)
+ }
+ compose := string(data)
+ if !strings.Contains(compose, "443:443/udp") {
+ t.Fatal("expected udp port mapping for hysteria2 runtime")
+ }
+ if !strings.Contains(compose, "127.0.0.1:1080:1080/tcp") {
+ t.Fatal("expected local tcp health port mapping for hysteria2 runtime")
+ }
+ if strings.Contains(compose, "caddy:") {
+ t.Fatal("did not expect caddy service for hysteria2 runtime")
+ }
+
+ serverConfig, err := os.ReadFile(filepath.Join(dir, "sing-box.server.json"))
+ if err != nil {
+ t.Fatalf("ReadFile sing-box.server.json error = %v", err)
+ }
+ config := string(serverConfig)
+ if !strings.Contains(config, "\"type\": \"hysteria2\"") {
+ t.Fatal("expected hysteria2 inbound in sing-box config")
+ }
+ if !strings.Contains(config, "\"salamander\"") {
+ t.Fatal("expected salamander obfuscation in sing-box config")
+ }
+ if !strings.Contains(config, "\"listen_port\": 1080") {
+ t.Fatal("expected mixed health inbound in sing-box config")
+ }
+ if !strings.Contains(config, "\"certificate_path\": \"/etc/sing-box/cert.pem\"") {
+ t.Fatal("expected embedded certificate path in sing-box config")
+ }
+ if _, err := os.Stat(filepath.Join(dir, "cert.pem")); err != nil {
+ t.Fatalf("expected generated cert.pem: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "key.pem")); err != nil {
+ t.Fatalf("expected generated key.pem: %v", err)
+ }
+}
+
+func TestRenderRuntimeBundleMultiProtocol(t *testing.T) {
+ t.Parallel()
+
+ dir := t.TempDir()
+ node := Node{
+ ID: "nl-multi",
+ Name: "NL Multi",
+ Region: "nl",
+ Host: "203.0.113.40",
+ Enabled: true,
+ SSH: SSHConfig{
+ User: "root",
+ Port: 22,
+ Auth: "key",
+ },
+ Protocols: []ProtocolProfile{
+ {
+ Type: "vless-reality",
+ Enabled: true,
+ Port: 443,
+ Auth: &AuthProfile{
+ UUID: "33333333-3333-3333-3333-333333333333",
+ },
+ Reality: &VLESSRealityProfile{
+ ServerName: "www.microsoft.com",
+ ServerPort: 443,
+ PrivateKey: "UuMBgl7MXTPx9inmQp2UC7Jcnwc6XYbwDNebonM-FCc",
+ PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+ ShortID: "0123456789abcdef",
+ Fingerprint: "chrome",
+ },
+ },
+ {
+ Type: "hysteria2",
+ Enabled: true,
+ Port: 443,
+ Auth: &AuthProfile{
+ Password: "user-password",
+ },
+ Hysteria2: &Hysteria2Profile{
+ Port: 443,
+ UpMbps: 100,
+ DownMbps: 100,
+ ObfsPassword: "obfs-password",
+ UserPassword: "user-password",
+ CertPath: "/etc/sing-box/cert.pem",
+ KeyPath: "/etc/sing-box/key.pem",
+ },
+ },
+ },
+ }
+
+ if err := RenderRuntimeBundle(dir, node, "20260409-120000"); err != nil {
+ t.Fatalf("RenderRuntimeBundle error = %v", err)
+ }
+
+ data, err := os.ReadFile(filepath.Join(dir, "docker-compose.yml"))
+ if err != nil {
+ t.Fatalf("ReadFile docker-compose.yml error = %v", err)
+ }
+ compose := string(data)
+ if !strings.Contains(compose, "network_mode: host") {
+ t.Fatal("expected host networking for multi protocol runtime")
+ }
+
+ serverConfig, err := os.ReadFile(filepath.Join(dir, "sing-box.server.json"))
+ if err != nil {
+ t.Fatalf("ReadFile sing-box.server.json error = %v", err)
+ }
+ config := string(serverConfig)
+ if !strings.Contains(config, "\"tag\": \"vless-reality-in\"") {
+ t.Fatal("expected reality inbound in sing-box config")
+ }
+ if !strings.Contains(config, "\"tag\": \"hysteria2-in\"") {
+ t.Fatal("expected hysteria2 inbound in sing-box config")
+ }
+ if !strings.Contains(config, "\"tag\": \"hy2-health-in\"") {
+ t.Fatal("expected hysteria2 health inbound for multi runtime")
+ }
+}