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) } }