package control import ( "os" "path/filepath" "strings" "testing" "vpnem/internal/models" ) func TestBuildLegacyCatalog(t *testing.T) { t.Parallel() nodes := []Node{ { ID: "nl-01", Name: "NL 01", Region: "nl", Host: "203.0.113.10", Domain: "nl-01.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{ "transport_type": "ws", "path": "/ws", }, }, { Type: "shadowsocks", Enabled: true, Port: 8443, Auth: &AuthProfile{ Method: "2022-blake3-aes-128-gcm", Password: "secret", }, }, }, }, } resp, err := BuildLegacyCatalog(nodes) if err != nil { t.Fatalf("BuildLegacyCatalog error = %v", err) } if len(resp.Servers) != 2 { t.Fatalf("len(resp.Servers) = %d, want 2", len(resp.Servers)) } if resp.Servers[0].Tag != "nl-01-shadowsocks" { t.Fatalf("unexpected first tag %q", resp.Servers[0].Tag) } if resp.Servers[1].Tag != "nl-01-vless" { t.Fatalf("unexpected second tag %q", resp.Servers[1].Tag) } if resp.Servers[1].Transport == nil || resp.Servers[1].Transport.Type != "ws" { t.Fatalf("expected ws transport, got %+v", resp.Servers[1].Transport) } } func TestBuildLegacyCatalogRejectsUnsupportedProtocol(t *testing.T) { t.Parallel() _, err := BuildLegacyCatalog([]Node{ { ID: "nl-01", Name: "NL 01", Region: "nl", Host: "203.0.113.10", Enabled: true, SSH: SSHConfig{ User: "root", Port: 22, Auth: "key", }, Protocols: []ProtocolProfile{ {Type: "hysteria2", Enabled: true, Port: 443}, }, }, }) if err == nil { t.Fatal("expected unsupported protocol error") } } func TestPublishableNodes(t *testing.T) { t.Parallel() nodes := []Node{ {ID: "healthy", Name: "healthy", Region: "nl", Host: "1.1.1.1", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{{Type: "socks5", Enabled: true, Port: 1080}}}, {ID: "failed", Name: "failed", Region: "nl", Host: "1.1.1.2", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{{Type: "socks5", Enabled: true, Port: 1080}}}, {ID: "nostate", Name: "nostate", Region: "nl", Host: "1.1.1.3", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{{Type: "socks5", Enabled: true, Port: 1080}}}, } states := map[string]*NodeState{ "healthy": {NodeID: "healthy", BootstrapStatus: "healthy"}, "failed": {NodeID: "failed", BootstrapStatus: "failed"}, } got := PublishableNodes(nodes, states) if len(got) != 1 { t.Fatalf("len(PublishableNodes) = %d, want 1", len(got)) } if got[0].ID != "healthy" { t.Fatalf("expected healthy node, got %s", got[0].ID) } } func TestPublishableNodesRequiresRunningServicesWhenKnown(t *testing.T) { t.Parallel() nodes := []Node{ {ID: "healthy", Name: "healthy", Region: "nl", Host: "1.1.1.1", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{{Type: "socks5", Enabled: true, Port: 1080}}}, {ID: "degraded", Name: "degraded", Region: "nl", Host: "1.1.1.2", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{{Type: "socks5", Enabled: true, Port: 1080}}}, } states := map[string]*NodeState{ "healthy": { NodeID: "healthy", BootstrapStatus: "healthy", Services: []ServiceStatus{{Type: "socks5", Status: "running", Port: 1080}}, Metadata: map[string]any{"healthz_http_code": 200}, }, "degraded": { NodeID: "degraded", BootstrapStatus: "healthy", Services: []ServiceStatus{{Type: "socks5", Status: "unknown", Port: 1080}}, Metadata: map[string]any{"healthz_http_code": 503}, }, } got := PublishableNodes(nodes, states) if len(got) != 1 { t.Fatalf("len(PublishableNodes) = %d, want 1", len(got)) } if got[0].ID != "healthy" { t.Fatalf("expected healthy node, got %s", got[0].ID) } } func TestPublishDecisionForNode(t *testing.T) { t.Parallel() node := Node{ ID: "nl-01", Name: "NL 01", Region: "nl", Host: "203.0.113.10", Domain: "nl-01.example.com", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{ {Type: "vless", Enabled: true, Port: 443}, }, } blocked := PublishDecisionForNode(node, &NodeState{ NodeID: "nl-01", BootstrapStatus: "healthy", Services: []ServiceStatus{{Type: "vless", Status: "configured", Port: 443}}, Metadata: map[string]any{"healthz_http_code": 503}, }) if blocked.Eligible { t.Fatal("expected blocked publish decision") } if len(blocked.Reasons) == 0 { t.Fatal("expected reasons for blocked decision") } ready := PublishDecisionForNode(node, &NodeState{ NodeID: "nl-01", BootstrapStatus: "healthy", PublicHost: "nl-01.example.com", Services: []ServiceStatus{{Type: "vless", Status: "running", Port: 443}}, Metadata: map[string]any{"healthz_http_code": 200}, }) if !ready.Eligible { t.Fatalf("expected ready decision, got reasons: %v", ready.Reasons) } if ready.PublicHost != "nl-01.example.com" { t.Fatalf("unexpected public host %q", ready.PublicHost) } } func TestBuildCatalogV2(t *testing.T) { t.Parallel() nodes := []Node{ { ID: "nl-01", Name: "NL 01", Provider: "custom-vps", Region: "nl", Host: "203.0.113.10", Domain: "nl-01.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"}}, {Type: "hysteria2", Enabled: true, Port: 9443, Auth: &AuthProfile{Password: "hidden"}, Extra: map[string]any{"obfs_password": "masked"}}, }, }, } states := map[string]*NodeState{ "nl-01": {NodeID: "nl-01", BootstrapStatus: "healthy", PublicHost: "nl-01.example.com", Metadata: map[string]any{"healthz_http_code": 200}}, } catalog := BuildCatalogV2(nodes, states) if catalog.Version != "2" { t.Fatalf("catalog.Version = %q, want 2", catalog.Version) } if len(catalog.Nodes) != 1 { t.Fatalf("len(catalog.Nodes) = %d, want 1", len(catalog.Nodes)) } if catalog.Nodes[0].PublicHost != "nl-01.example.com" { t.Fatalf("unexpected public host %q", catalog.Nodes[0].PublicHost) } if len(catalog.Nodes[0].Protocols) != 2 { t.Fatalf("expected 2 protocols, got %d", len(catalog.Nodes[0].Protocols)) } if catalog.Nodes[0].Protocols[0].Type != "vless" { t.Fatalf("unexpected first protocol %q", catalog.Nodes[0].Protocols[0].Type) } } func TestMergeLegacyServersPreservesStaticEntries(t *testing.T) { t.Parallel() static := []models.Server{ {Tag: "nl-1", Type: "socks", Server: "1.1.1.1", ServerPort: 1080}, {Tag: "nl-ss-1", Type: "shadowsocks", Server: "ss.example.com", ServerPort: 443}, } dynamic := []models.Server{ {Tag: "node-1-vless", Type: "vless", Server: "2.2.2.2", ServerPort: 443}, } merged := MergeLegacyServers(static, dynamic) if len(merged) != 3 { t.Fatalf("len(merged) = %d, want 3", len(merged)) } } func TestWriteLegacyCatalogMergesStaticServers(t *testing.T) { t.Parallel() dir := t.TempDir() staticPath := filepath.Join(dir, "static-servers.json") if err := os.WriteFile(staticPath, []byte(`{"servers":[{"tag":"nl-1","region":"NL","type":"socks","server":"1.1.1.1","server_port":1080}]}`), 0o644); err != nil { t.Fatalf("write static servers: %v", err) } err := WriteLegacyCatalog(filepath.Join(dir, "servers.json"), []Node{ { ID: "node-1", Name: "Node 1", Region: "nl", Host: "2.2.2.2", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{ {Type: "socks5", Enabled: true, Port: 1081}, }, }, }) if err != nil { t.Fatalf("WriteLegacyCatalog error = %v", err) } data, err := os.ReadFile(filepath.Join(dir, "servers.json")) if err != nil { t.Fatalf("read merged servers: %v", err) } text := string(data) if !strings.Contains(text, `"tag": "nl-1"`) { t.Fatalf("expected static server in merged catalog: %s", text) } if !strings.Contains(text, `"tag": "node-1-socks5"`) { t.Fatalf("expected dynamic server in merged catalog: %s", text) } } func TestWriteCatalogV2DoesNotMergeStaticLegacyServers(t *testing.T) { t.Parallel() dir := t.TempDir() staticPath := filepath.Join(dir, "static-servers.json") if err := os.WriteFile(staticPath, []byte(`{"servers":[{"tag":"nl-ss-1","region":"NL","type":"shadowsocks","server":"ss.example.com","server_port":443,"method":"chacha20-ietf-poly1305","password":"secret"}]}`), 0o644); err != nil { t.Fatalf("write static servers: %v", err) } err := WriteCatalogV2(filepath.Join(dir, "catalog-v2.json"), []Node{ { ID: "node-1", Name: "Node 1", Region: "nl", Host: "2.2.2.2", Enabled: true, SSH: SSHConfig{User: "root", Port: 22, Auth: "key"}, Protocols: []ProtocolProfile{ {Type: "vless", Enabled: true, Port: 443, Auth: &AuthProfile{UUID: "11111111-1111-1111-1111-111111111111"}}, }, }, }, map[string]*NodeState{}) if err != nil { t.Fatalf("WriteCatalogV2 error = %v", err) } data, err := os.ReadFile(filepath.Join(dir, "catalog-v2.json")) if err != nil { t.Fatalf("read catalog v2: %v", err) } text := string(data) if strings.Contains(text, `"id": "nl-ss-1"`) { t.Fatalf("did not expect static legacy node in catalog v2: %s", text) } if !strings.Contains(text, `"id": "node-1"`) { t.Fatalf("expected dynamic node in catalog v2: %s", text) } }