diff options
| author | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
|---|---|---|
| committer | sergei <sergei@em-sysadmin.xyz> | 2026-04-14 06:23:55 +0400 |
| commit | 3d51aa455006903345f554a2dd90034993796114 (patch) | |
| tree | 62a7be2faf047f5eb7886feebc3b815556f03d7f /internal/sync/fetcher_test.go | |
| download | vpnem-3d51aa455006903345f554a2dd90034993796114.tar.gz vpnem-3d51aa455006903345f554a2dd90034993796114.tar.bz2 vpnem-3d51aa455006903345f554a2dd90034993796114.zip | |
- 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.go | 300 |
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) + } +} |
