summaryrefslogtreecommitdiff
path: root/internal/sync/fetcher_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/sync/fetcher_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/sync/fetcher_test.go')
-rw-r--r--internal/sync/fetcher_test.go300
1 files changed, 300 insertions, 0 deletions
diff --git a/internal/sync/fetcher_test.go b/internal/sync/fetcher_test.go
new file mode 100644
index 0000000..cdf3e73
--- /dev/null
+++ b/internal/sync/fetcher_test.go
@@ -0,0 +1,300 @@
+package sync
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "vpnem/internal/models"
+)
+
+func TestCatalogToServers(t *testing.T) {
+ catalog := &models.CatalogV2{
+ Version: "2",
+ Nodes: []models.CatalogNode{
+ {
+ ID: "nl-01",
+ Name: "NL 01",
+ Region: "nl",
+ Host: "203.0.113.10",
+ PublicHost: "nl-01.example.com",
+ Protocols: []models.CatalogProtocol{
+ {
+ Type: "vless",
+ Enabled: true,
+ Port: 443,
+ TLS: &models.TLS{Enabled: true, ServerName: "nl-01.example.com"},
+ Auth: &models.CatalogAuth{UUID: "11111111-1111-1111-1111-111111111111"},
+ Extra: map[string]any{"transport_type": "ws", "path": "/ws"},
+ },
+ {
+ Type: "vmess",
+ Enabled: true,
+ Port: 8444,
+ TLS: &models.TLS{Enabled: true, ServerName: "nl-01.example.com"},
+ Auth: &models.CatalogAuth{UUID: "22222222-2222-2222-2222-222222222222"},
+ Extra: map[string]any{"path": "/vmess"},
+ },
+ {
+ Type: "hysteria2",
+ Enabled: true,
+ Port: 9443,
+ TLS: &models.TLS{Enabled: true, ServerName: "nl-01.example.com"},
+ Auth: &models.CatalogAuth{Password: "hy2-secret"},
+ Extra: map[string]any{"obfs_password": "obfs-secret", "up_mbps": 80, "down_mbps": 90},
+ },
+ },
+ },
+ },
+ }
+
+ servers := CatalogToServers(catalog)
+ if len(servers) != 3 {
+ t.Fatalf("len(servers) = %d, want 3", len(servers))
+ }
+ if servers[1].Type != "vmess" {
+ t.Fatalf("expected vmess, got %q", servers[1].Type)
+ }
+ if servers[2].Type != "hysteria2" {
+ t.Fatalf("expected hysteria2, got %q", servers[2].Type)
+ }
+ if servers[2].ObfsPassword != "obfs-secret" {
+ t.Fatalf("unexpected hysteria2 obfs password")
+ }
+}
+
+func TestFetchServersPrefersCatalogV2(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v2/catalog":
+ _ = json.NewEncoder(w).Encode(models.CatalogV2{
+ Version: "2",
+ Nodes: []models.CatalogNode{
+ {
+ ID: "nl-01",
+ Name: "NL 01",
+ Region: "nl",
+ Host: "203.0.113.10",
+ PublicHost: "nl-01.example.com",
+ Protocols: []models.CatalogProtocol{
+ {Type: "vmess", Enabled: true, Port: 8444, Auth: &models.CatalogAuth{UUID: "22222222-2222-2222-2222-222222222222"}},
+ },
+ },
+ },
+ })
+ case "/api/v1/servers":
+ t.Fatal("legacy servers endpoint should not be used when catalog-v2 is available")
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer server.Close()
+
+ fetcher := NewFetcher(server.URL)
+ resp, err := fetcher.FetchServers()
+ if err != nil {
+ t.Fatalf("FetchServers error = %v", err)
+ }
+ if len(resp.Servers) != 1 {
+ t.Fatalf("expected 1 server, got %d", len(resp.Servers))
+ }
+ if resp.Servers[0].Type != "vmess" {
+ t.Fatalf("expected vmess, got %q", resp.Servers[0].Type)
+ }
+}
+
+func TestFetchServersFallsBackToLegacy(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v2/catalog":
+ http.NotFound(w, r)
+ case "/api/v1/servers":
+ _ = json.NewEncoder(w).Encode(models.ServersResponse{
+ Servers: []models.Server{{Tag: "legacy", Type: "socks", Server: "1.2.3.4", ServerPort: 1080}},
+ })
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer server.Close()
+
+ fetcher := NewFetcher(server.URL)
+ resp, err := fetcher.FetchServers()
+ if err != nil {
+ t.Fatalf("FetchServers error = %v", err)
+ }
+ if len(resp.Servers) != 1 || !strings.EqualFold(resp.Servers[0].Tag, "legacy") {
+ t.Fatalf("unexpected legacy response: %+v", resp.Servers)
+ }
+}
+
+func TestFetchCatalogFallsBackToLegacy(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v2/catalog":
+ http.NotFound(w, r)
+ case "/api/v1/servers":
+ _ = json.NewEncoder(w).Encode(models.ServersResponse{
+ Servers: []models.Server{
+ {Tag: "legacy-vless", Region: "nl", Type: "vless", Server: "legacy.example.com", ServerPort: 443, UUID: "11111111-1111-1111-1111-111111111111"},
+ },
+ })
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer server.Close()
+
+ fetcher := NewFetcher(server.URL)
+ catalog, err := fetcher.FetchCatalog()
+ if err != nil {
+ t.Fatalf("FetchCatalog error = %v", err)
+ }
+ if catalog.Version != "legacy-adapter" {
+ t.Fatalf("expected legacy-adapter version, got %q", catalog.Version)
+ }
+ if len(catalog.Nodes) != 1 || len(catalog.Nodes[0].Protocols) != 1 {
+ t.Fatalf("unexpected catalog shape: %+v", catalog)
+ }
+ if catalog.Nodes[0].Protocols[0].Type != "vless" {
+ t.Fatalf("expected vless protocol, got %q", catalog.Nodes[0].Protocols[0].Type)
+ }
+}
+
+func TestFetchRoutingPolicyFallsBackToDefault(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.NotFound(w, r)
+ }))
+ defer server.Close()
+
+ fetcher := NewFetcher(server.URL)
+ policy, err := fetcher.FetchRoutingPolicy()
+ if err != nil {
+ t.Fatalf("FetchRoutingPolicy error = %v", err)
+ }
+ if policy.Version == "" {
+ t.Fatalf("expected default policy version")
+ }
+ if len(policy.AlwaysDirectProcesses) == 0 {
+ t.Fatalf("expected default direct processes")
+ }
+}
+
+func TestFetchRoutingPolicy(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/api/v1/routing-policy":
+ _ = json.NewEncoder(w).Encode(models.RoutingPolicy{
+ Version: "remote-policy",
+ AlwaysDirectProcesses: []string{"chromium.exe"},
+ BlockedDomains: []string{"example.com"},
+ })
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ defer server.Close()
+
+ fetcher := NewFetcher(server.URL)
+ policy, err := fetcher.FetchRoutingPolicy()
+ if err != nil {
+ t.Fatalf("FetchRoutingPolicy error = %v", err)
+ }
+ if policy.Version != "remote-policy" {
+ t.Fatalf("expected remote-policy, got %q", policy.Version)
+ }
+ if len(policy.AlwaysDirectProcesses) != 1 || policy.AlwaysDirectProcesses[0] != "chromium.exe" {
+ t.Fatalf("unexpected routing policy: %+v", policy)
+ }
+}
+
+func TestServersToCatalog(t *testing.T) {
+ catalog := ServersToCatalog([]models.Server{
+ {
+ Tag: "nl-01-vless",
+ Region: "nl",
+ Type: "vless",
+ Server: "nl-01.example.com",
+ ServerPort: 443,
+ UUID: "11111111-1111-1111-1111-111111111111",
+ TLS: &models.TLS{Enabled: true, ServerName: "nl-01.example.com"},
+ Transport: &models.Transport{Type: "ws", Path: "/ws"},
+ },
+ {
+ Tag: "nl-01-hysteria2",
+ Region: "nl",
+ Type: "hysteria2",
+ Server: "nl-01.example.com",
+ ServerPort: 9443,
+ Password: "hy2-secret",
+ ObfsPassword: "obfs-secret",
+ },
+ })
+
+ if catalog.Version != "legacy-adapter" {
+ t.Fatalf("unexpected version %q", catalog.Version)
+ }
+ if len(catalog.Nodes) != 1 {
+ t.Fatalf("expected one node, got %d", len(catalog.Nodes))
+ }
+ if len(catalog.Nodes[0].Protocols) != 2 {
+ t.Fatalf("expected two protocols, got %d", len(catalog.Nodes[0].Protocols))
+ }
+ if catalog.Nodes[0].Protocols[1].Extra["obfs_password"] != "obfs-secret" {
+ t.Fatalf("expected obfs password in extra")
+ }
+}
+
+func TestCatalogToServersMultiProtocolNode(t *testing.T) {
+ catalog := &models.CatalogV2{
+ Version: "2",
+ Nodes: []models.CatalogNode{
+ {
+ ID: "nl-multi-01",
+ Name: "NL Multi",
+ Region: "nl",
+ Host: "203.0.113.55",
+ PublicHost: "203.0.113.55",
+ Protocols: []models.CatalogProtocol{
+ {
+ Type: "vless-reality",
+ Enabled: true,
+ Port: 443,
+ Auth: &models.CatalogAuth{UUID: "11111111-1111-1111-1111-111111111111"},
+ TLS: &models.TLS{
+ Enabled: true,
+ ServerName: "www.microsoft.com",
+ Reality: &models.Reality{
+ Enabled: true,
+ PublicKey: "pubkey",
+ ShortID: "shortid",
+ Fingerprint: "chrome",
+ },
+ },
+ },
+ {
+ Type: "hysteria2",
+ Enabled: true,
+ Port: 443,
+ Auth: &models.CatalogAuth{Password: "hy2-secret"},
+ TLS: &models.TLS{Enabled: true, Insecure: true, ALPN: []string{"h3"}},
+ Extra: map[string]any{"obfs_password": "obfs-secret", "up_mbps": 100, "down_mbps": 100},
+ },
+ },
+ },
+ },
+ }
+
+ servers := CatalogToServers(catalog)
+ if len(servers) != 1 {
+ t.Fatalf("expected 1 synthetic multi server, got %d", len(servers))
+ }
+ if servers[0].Tag != "nl-multi-01-multi" {
+ t.Fatalf("unexpected synthetic tag %q", servers[0].Tag)
+ }
+ if len(servers[0].Companions) != 1 || servers[0].Companions[0].Type != "hysteria2" {
+ t.Fatalf("expected hysteria2 companion, got %+v", servers[0].Companions)
+ }
+}