Some checks failed
CI/CD Pipeline / Test (push) Failing after 22m19s
CI/CD Pipeline / Security Scan (push) Failing after 5m57s
CI/CD Pipeline / Build (amd64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (amd64, linux) (push) Has been skipped
CI/CD Pipeline / Build (amd64, windows) (push) Has been skipped
CI/CD Pipeline / Build (arm64, darwin) (push) Has been skipped
CI/CD Pipeline / Build (arm64, linux) (push) Has been skipped
CI/CD Pipeline / Build Docker Image (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
1775 lines
52 KiB
Go
1775 lines
52 KiB
Go
package e2e
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"strconv"
|
|
|
|
yaml "gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func newHybridHTTPClient(hosts ...string) *http.Client {
|
|
allowed := make(map[string]struct{}, len(hosts))
|
|
for _, h := range hosts {
|
|
allowed[h] = struct{}{}
|
|
}
|
|
|
|
return &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
if _, ok := allowed[req.URL.Host]; ok {
|
|
return http.DefaultTransport.RoundTrip(req)
|
|
}
|
|
body := subscriptionFixture(req.URL.String())
|
|
status := http.StatusOK
|
|
if body == "" {
|
|
status = http.StatusNotFound
|
|
}
|
|
resp := &http.Response{
|
|
StatusCode: status,
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
}
|
|
resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
|
return resp, nil
|
|
}),
|
|
}
|
|
}
|
|
|
|
func TestHeadSubEndpoint(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26600
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
conversion:
|
|
cache_timeout: 60
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
|
|
server := app.GetHTTPServer()
|
|
|
|
resp := doRequest(t, server, http.MethodHead, "/sub?target=clash&url=https://mock-subscribe.example.com")
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for HEAD /sub, got %d", resp.StatusCode)
|
|
}
|
|
|
|
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text") {
|
|
t.Fatalf("expected text content type, got %q", ct)
|
|
}
|
|
|
|
if resp.ContentLength > 0 {
|
|
t.Fatalf("expected empty body for HEAD request, content length %d", resp.ContentLength)
|
|
}
|
|
|
|
if resp.Body != nil {
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed reading head body: %v", err)
|
|
}
|
|
if len(data) != 0 {
|
|
t.Fatalf("expected zero-length body, got %d", len(data))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSub2ClashREndpoint(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26610
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
conversion:
|
|
cache_timeout: 60
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
t.Run("MissingSublink", func(t *testing.T) {
|
|
resp := doRequest(t, server, http.MethodGet, "/sub2clashr")
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for missing sublink, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("PlaceholderSublink", func(t *testing.T) {
|
|
resp := doRequest(t, server, http.MethodGet, "/sub2clashr?sublink=sublink")
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for placeholder sublink, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidSublink", func(t *testing.T) {
|
|
resp := doRequest(t, server, http.MethodGet, "/sub2clashr?sublink=https://mock-subscribe.example.com")
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for valid sublink, got %d", resp.StatusCode)
|
|
}
|
|
|
|
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text") {
|
|
t.Fatalf("expected text content type, got %q", ct)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read sub2clashr body: %v", err)
|
|
}
|
|
if len(body) == 0 {
|
|
t.Fatalf("expected non-empty body from sub2clashr")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSurge2ClashEndpoint(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26620
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
conversion:
|
|
cache_timeout: 60
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
t.Run("MissingLink", func(t *testing.T) {
|
|
resp := doRequest(t, server, http.MethodGet, "/surge2clash")
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for missing link, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidLink", func(t *testing.T) {
|
|
resp := doRequest(t, server, http.MethodGet, "/surge2clash?link=https://mock-subscribe.example.com")
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for surge2clash conversion, got %d", resp.StatusCode)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if len(body) == 0 {
|
|
t.Fatalf("expected surge2clash body to be non-empty")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetLocalAndRemoteEndpoints(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26630
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
conversion:
|
|
cache_timeout: 60
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
tempDir := t.TempDir()
|
|
localFile := filepath.Join(tempDir, "sample.txt")
|
|
if err := os.WriteFile(localFile, []byte("local-content"), 0o644); err != nil {
|
|
t.Fatalf("failed to write local file: %v", err)
|
|
}
|
|
|
|
t.Run("GetLocal", func(t *testing.T) {
|
|
pathParam := url.QueryEscape(localFile)
|
|
resp := doRequest(t, server, http.MethodGet, fmt.Sprintf("/getlocal?path=%s", pathParam))
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for getlocal, got %d", resp.StatusCode)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read getlocal body: %v", err)
|
|
}
|
|
if string(body) != "local-content" {
|
|
t.Fatalf("unexpected getlocal content: %q", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("GetRemote", func(t *testing.T) {
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("remote-content"))
|
|
})
|
|
serverTS := httptest.NewServer(handler)
|
|
defer serverTS.Close()
|
|
|
|
resp := doRequest(t, server, http.MethodGet, fmt.Sprintf("/get?url=%s", url.QueryEscape(serverTS.URL)))
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for get remote, got %d", resp.StatusCode)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read get body: %v", err)
|
|
}
|
|
if string(body) != "remote-content" {
|
|
t.Fatalf("unexpected remote content: %q", string(body))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRenderAndGetProfileRules(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26640
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
conversion:
|
|
cache_timeout: 60
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
t.Run("RenderTemplate", func(t *testing.T) {
|
|
params := url.Values{}
|
|
params.Set("path", "GeneralClashConfig.tpl")
|
|
params.Set("target", "clash")
|
|
resp := doRequest(t, server, http.MethodGet, "/render?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for render, got %d", resp.StatusCode)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read render body: %v", err)
|
|
}
|
|
if len(body) == 0 {
|
|
t.Fatalf("expected non-empty render output")
|
|
}
|
|
})
|
|
|
|
t.Run("GetProfile", func(t *testing.T) {
|
|
profileDir := t.TempDir()
|
|
profileFile := filepath.Join(profileDir, "profile.ini")
|
|
content := "[Profile]\nprofile_token=test-token\nurl=https://example.com/sub\n"
|
|
if err := os.WriteFile(profileFile, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("failed to write profile file: %v", err)
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("name", profileFile)
|
|
params.Set("token", "test-token")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/getprofile?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for getprofile, got %d", resp.StatusCode)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read profile body: %v", err)
|
|
}
|
|
if string(body) != content {
|
|
t.Fatalf("unexpected profile content: %q", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("GetRuleset", func(t *testing.T) {
|
|
rulesetPath := filepath.Join("rules", "LocalAreaNetwork.list")
|
|
encoded := base64.StdEncoding.EncodeToString([]byte("ruleset," + rulesetPath))
|
|
params := url.Values{}
|
|
params.Set("type", "1")
|
|
params.Set("url", encoded)
|
|
resp := doRequest(t, server, http.MethodGet, "/getruleset?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for getruleset, got %d", resp.StatusCode)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read ruleset body: %v", err)
|
|
}
|
|
if len(body) == 0 {
|
|
t.Fatalf("expected non-empty ruleset content")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIConvertRequestOptions(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26650
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("emoji", "true")
|
|
params.Set("udp", "true")
|
|
params.Set("ipv6", "true")
|
|
params.Set("insert", "true")
|
|
params.Set("strict", "true")
|
|
params.Set("list", "true")
|
|
params.Set("append_type", "true")
|
|
params.Set("tfo", "true")
|
|
params.Set("script", "true")
|
|
params.Set("scv", "true")
|
|
params.Set("fdn", "true")
|
|
params.Set("expand", "true")
|
|
params.Set("append_info", "true")
|
|
params.Set("prepend", "true")
|
|
params.Set("classic", "true")
|
|
params.Set("tls13", "true")
|
|
params.Set("add_emoji", "true")
|
|
params.Set("remove_emoji", "true")
|
|
params.Set("upload", "true")
|
|
params.Set("group", "test-group")
|
|
params.Set("config", "https://example.com/config.yaml")
|
|
params.Set("include", "hk,sg")
|
|
params.Set("exclude", "cn")
|
|
params.Set("upload_path", "https://example.com/upload")
|
|
params.Set("rename", "foo`bar")
|
|
params.Set("filter_script", "filters.lua")
|
|
params.Set("dev_id", "device-123")
|
|
params.Set("interval", "7200")
|
|
groupPayload := base64.StdEncoding.EncodeToString([]byte("name=TestGroup&test=true"))
|
|
params.Set("groups", groupPayload)
|
|
rulesetPayload := base64.StdEncoding.EncodeToString([]byte("ruleset,rules/LocalAreaNetwork.list"))
|
|
params.Set("ruleset", rulesetPayload)
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for api convert, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
optionsVal, ok := body["request_options"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("missing request_options in response: %v", body)
|
|
}
|
|
|
|
assertBool := func(key string, expected bool) {
|
|
val, ok := optionsVal[key]
|
|
if !ok {
|
|
t.Fatalf("missing option %s", key)
|
|
}
|
|
boolVal, ok := val.(bool)
|
|
if !ok {
|
|
t.Fatalf("option %s not boolean: %T", key, val)
|
|
}
|
|
if boolVal != expected {
|
|
t.Fatalf("option %s expected %v got %v", key, expected, boolVal)
|
|
}
|
|
}
|
|
|
|
assertString := func(key, expected string) {
|
|
val, ok := optionsVal[key]
|
|
if !ok {
|
|
t.Fatalf("missing option %s", key)
|
|
}
|
|
strVal, ok := val.(string)
|
|
if !ok {
|
|
t.Fatalf("option %s not string: %T", key, val)
|
|
}
|
|
if strVal != expected {
|
|
t.Fatalf("option %s expected %s got %s", key, expected, strVal)
|
|
}
|
|
}
|
|
|
|
assertBool("emoji", true)
|
|
assertBool("udp", true)
|
|
assertBool("ipv6", true)
|
|
assertBool("insert", true)
|
|
assertBool("strict", true)
|
|
assertBool("list", true)
|
|
assertBool("append_type", true)
|
|
assertBool("tfo", true)
|
|
assertBool("script", true)
|
|
assertBool("scv", true)
|
|
assertBool("fdn", true)
|
|
assertBool("expand", true)
|
|
assertBool("append_info", true)
|
|
assertBool("prepend", true)
|
|
assertBool("classic", true)
|
|
assertBool("tls13", true)
|
|
assertBool("add_emoji", true)
|
|
assertBool("remove_emoji", true)
|
|
assertBool("upload", true)
|
|
assertString("group", "test-group")
|
|
assertString("config", "https://example.com/config.yaml")
|
|
assertString("include", "hk,sg")
|
|
assertString("exclude", "cn")
|
|
assertString("upload_path", "https://example.com/upload")
|
|
assertString("rename", "foo`bar")
|
|
assertString("filter_script", "filters.lua")
|
|
assertString("dev_id", "device-123")
|
|
assertString("interval", "7200")
|
|
assertString("groups", "name=TestGroup&test=true")
|
|
assertString("ruleset", "ruleset,rules/LocalAreaNetwork.list")
|
|
}
|
|
|
|
func TestClashProxyProviders(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26665
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
groupValues := url.Values{}
|
|
groupValues.Set("name", "ProviderSelect")
|
|
groupValues.Set("type", "select")
|
|
groupValues.Set("use", "HK,JP")
|
|
groupEncoded := base64.StdEncoding.EncodeToString([]byte(groupValues.Encode()))
|
|
|
|
providerHK := url.Values{}
|
|
providerHK.Set("name", "HK")
|
|
providerHK.Set("type", "http")
|
|
providerHK.Set("url", "https://example.com/providers/hk.yaml")
|
|
providerHK.Set("path", "proxy-providers/HK.yaml")
|
|
providerHK.Set("interval", "86400")
|
|
providerHK.Set("health_enable", "true")
|
|
providerHK.Set("health_url", "http://www.gstatic.com/generate_204")
|
|
providerHK.Set("health_interval", "300")
|
|
|
|
providerJP := url.Values{}
|
|
providerJP.Set("name", "JP")
|
|
providerJP.Set("type", "http")
|
|
providerJP.Set("url", "https://example.com/providers/jp.yaml")
|
|
providerJP.Set("path", "proxy-providers/JP.yaml")
|
|
providerJP.Set("interval", "43200")
|
|
providerJP.Set("health_enable", "false")
|
|
|
|
providersEncoded := encodeProvidersForTest(providerHK, providerJP)
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("groups", groupEncoded)
|
|
params.Set("providers", providersEncoded)
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read provider response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 from api convert with providers, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &body); err != nil {
|
|
t.Fatalf("failed to decode api response: %v", err)
|
|
}
|
|
|
|
content, ok := body["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("missing content in api response: %#v", body)
|
|
}
|
|
|
|
generated := parseClashYAML(t, content)
|
|
|
|
providersAny, ok := generated["proxy-providers"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("proxy-providers section missing: %#v", generated)
|
|
}
|
|
|
|
hkAny, ok := providersAny["HK"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("HK provider missing: %#v", providersAny)
|
|
}
|
|
if hkAny["type"] != "http" {
|
|
t.Fatalf("unexpected HK provider type: %#v", hkAny["type"])
|
|
}
|
|
if hkAny["url"] != "https://example.com/providers/hk.yaml" {
|
|
t.Fatalf("unexpected HK provider url: %#v", hkAny["url"])
|
|
}
|
|
if hkAny["path"] != "proxy-providers/HK.yaml" {
|
|
t.Fatalf("unexpected HK provider path: %#v", hkAny["path"])
|
|
}
|
|
if interval, ok := asNumber(hkAny["interval"]); !ok || interval != 86400 {
|
|
t.Fatalf("unexpected HK provider interval: %#v", hkAny["interval"])
|
|
}
|
|
|
|
healthAny, ok := hkAny["health-check"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("health-check block missing for HK provider: %#v", hkAny)
|
|
}
|
|
if healthAny["enable"] != true {
|
|
t.Fatalf("health-check enable not true: %#v", healthAny["enable"])
|
|
}
|
|
if healthAny["url"] != "http://www.gstatic.com/generate_204" {
|
|
t.Fatalf("unexpected health-check url: %#v", healthAny["url"])
|
|
}
|
|
if val, ok := asNumber(healthAny["interval"]); !ok || val != 300 {
|
|
t.Fatalf("unexpected health-check interval: %#v", healthAny["interval"])
|
|
}
|
|
|
|
jpAny, ok := providersAny["JP"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("JP provider missing: %#v", providersAny)
|
|
}
|
|
if val, ok := asNumber(jpAny["interval"]); !ok || val != 43200 {
|
|
t.Fatalf("unexpected JP interval: %#v", jpAny["interval"])
|
|
}
|
|
|
|
groupsAny, ok := generated["proxy-groups"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("proxy-groups missing: %#v", generated)
|
|
}
|
|
|
|
found := false
|
|
for _, raw := range groupsAny {
|
|
group, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if group["name"] == "ProviderSelect" {
|
|
found = true
|
|
useList, ok := group["use"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected ProviderSelect group to contain use list: %#v", group)
|
|
}
|
|
if len(useList) != 2 || useList[0] != "HK" || useList[1] != "JP" {
|
|
t.Fatalf("unexpected provider use list: %#v", useList)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Fatalf("custom group ProviderSelect not found: %#v", groupsAny)
|
|
}
|
|
}
|
|
|
|
func TestSurgeProxyProviders(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26666
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
groupEncoded := encodeGroupForTest(url.Values{
|
|
"name": {"ProviderSelect"},
|
|
"type": {"select"},
|
|
"use": {"HK,JP"},
|
|
})
|
|
|
|
providerHK := url.Values{}
|
|
providerHK.Set("name", "HK")
|
|
providerHK.Set("type", "http")
|
|
providerHK.Set("url", "https://example.com/providers/hk.yaml")
|
|
providerHK.Set("path", "proxy-providers/HK.yaml")
|
|
providerHK.Set("interval", "86400")
|
|
providerHK.Set("health_enable", "true")
|
|
providerHK.Set("health_url", "http://www.gstatic.com/generate_204")
|
|
providerHK.Set("health_interval", "300")
|
|
|
|
providerJP := url.Values{}
|
|
providerJP.Set("name", "JP")
|
|
providerJP.Set("type", "http")
|
|
providerJP.Set("url", "https://example.com/providers/jp.yaml")
|
|
providerJP.Set("path", "proxy-providers/JP.yaml")
|
|
providerJP.Set("interval", "43200")
|
|
|
|
providersEncoded := encodeProvidersForTest(providerHK, providerJP)
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "surge")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("groups", groupEncoded)
|
|
params.Set("providers", providersEncoded)
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read surge provider response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 from api convert with surge providers, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &body); err != nil {
|
|
t.Fatalf("failed to decode api response: %v", err)
|
|
}
|
|
|
|
content, ok := body["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("missing content in api response: %#v", body)
|
|
}
|
|
|
|
if !strings.Contains(content, "[Proxy Provider]") {
|
|
t.Fatalf("expected Proxy Provider section in surge output: %s", content)
|
|
}
|
|
if !strings.Contains(content, "HK = http, https://example.com/providers/hk.yaml") {
|
|
t.Fatalf("expected HK provider line, got: %s", content)
|
|
}
|
|
if !strings.Contains(content, "path=proxy-providers/HK.yaml") {
|
|
t.Fatalf("expected HK provider path entry, got: %s", content)
|
|
}
|
|
if !strings.Contains(content, "interval=86400") {
|
|
t.Fatalf("expected HK provider interval entry, got: %s", content)
|
|
}
|
|
if !strings.Contains(content, "health-check-url=http://www.gstatic.com/generate_204") {
|
|
t.Fatalf("expected health-check url entry, got: %s", content)
|
|
}
|
|
if !strings.Contains(content, "ProviderSelect = select") || !strings.Contains(content, "use-provider=HK") || !strings.Contains(content, "use-provider=JP") {
|
|
t.Fatalf("expected ProviderSelect custom group referencing providers, got: %s", content)
|
|
}
|
|
}
|
|
|
|
func TestQuantumultXProxyProviders(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26667
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
groupEncoded := encodeGroupForTest(url.Values{
|
|
"name": {"ProviderSelect"},
|
|
"type": {"select"},
|
|
"use": {"HK,JP"},
|
|
})
|
|
|
|
providerHK := url.Values{}
|
|
providerHK.Set("name", "HK")
|
|
providerHK.Set("type", "http")
|
|
providerHK.Set("url", "https://example.com/providers/hk.yaml")
|
|
providerHK.Set("path", "proxy-providers/HK.yaml")
|
|
providerHK.Set("interval", "86400")
|
|
|
|
providerJP := url.Values{}
|
|
providerJP.Set("name", "JP")
|
|
providerJP.Set("type", "http")
|
|
providerJP.Set("url", "https://example.com/providers/jp.yaml")
|
|
providerJP.Set("path", "proxy-providers/JP.yaml")
|
|
providerJP.Set("interval", "43200")
|
|
|
|
providersEncoded := encodeProvidersForTest(providerHK, providerJP)
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "quanx")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("groups", groupEncoded)
|
|
params.Set("providers", providersEncoded)
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read quantumultx provider response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 from api convert with quantumultx providers, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &body); err != nil {
|
|
t.Fatalf("failed to decode api response: %v", err)
|
|
}
|
|
|
|
content, ok := body["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("missing content in api response: %#v", body)
|
|
}
|
|
|
|
if !strings.Contains(content, "[Server Remote]") {
|
|
t.Fatalf("expected Server Remote section in quantumultx output: %s", content)
|
|
}
|
|
if !strings.Contains(content, "HK = https://example.com/providers/hk.yaml") {
|
|
t.Fatalf("expected HK remote server entry, got: %s", content)
|
|
}
|
|
if !strings.Contains(content, "tag=HK") {
|
|
t.Fatalf("expected tag=HK entry, got: %s", content)
|
|
}
|
|
if !strings.Contains(content, "update-interval=86400") {
|
|
t.Fatalf("expected update-interval entry, got: %s", content)
|
|
}
|
|
if !strings.Contains(content, "server-tag-regex=^(HK|JP)$") {
|
|
t.Fatalf("expected policy referencing provider tags, got: %s", content)
|
|
}
|
|
}
|
|
|
|
func TestProviderValidationErrors(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26668
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
// Missing path value should trigger validation failure
|
|
invalidProvider := url.Values{}
|
|
invalidProvider.Set("name", "Broken")
|
|
invalidProvider.Set("type", "http")
|
|
invalidProvider.Set("url", "https://example.com/broken.yaml")
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("providers", base64.StdEncoding.EncodeToString([]byte(invalidProvider.Encode())))
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid provider, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode error response: %v", err)
|
|
}
|
|
if success, _ := body["success"].(bool); success {
|
|
t.Fatalf("expected success=false for invalid provider, got: %#v", body)
|
|
}
|
|
}
|
|
|
|
func encodeGroupForTest(values url.Values) string {
|
|
return base64.StdEncoding.EncodeToString([]byte(values.Encode()))
|
|
}
|
|
|
|
func encodeProvidersForTest(defs ...url.Values) string {
|
|
encoded := make([]string, 0, len(defs))
|
|
for _, v := range defs {
|
|
encoded = append(encoded, v.Encode())
|
|
}
|
|
return base64.StdEncoding.EncodeToString([]byte(strings.Join(encoded, "@")))
|
|
}
|
|
|
|
func parseClashYAML(t *testing.T, content string) map[string]interface{} {
|
|
t.Helper()
|
|
var parsed map[string]interface{}
|
|
if err := yaml.Unmarshal([]byte(content), &parsed); err != nil {
|
|
t.Fatalf("failed to decode generated clash content: %v", err)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func asNumber(value interface{}) (int, bool) {
|
|
switch v := value.(type) {
|
|
case int:
|
|
return v, true
|
|
case int64:
|
|
return int(v), true
|
|
case float64:
|
|
return int(v), true
|
|
case string:
|
|
if v == "" {
|
|
return 0, false
|
|
}
|
|
if i, err := strconv.Atoi(v); err == nil {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func hasEmojiPrefix(name string) bool {
|
|
trimmed := strings.TrimSpace(name)
|
|
if trimmed == "" {
|
|
return false
|
|
}
|
|
r, _ := utf8.DecodeRuneInString(trimmed)
|
|
switch r {
|
|
case '🛰', '🛸', '🐴':
|
|
return true
|
|
}
|
|
// Regional indicator symbols (flags)
|
|
if r >= 0x1F1E6 && r <= 0x1F1FF {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestSubscriptionFilenameDisposition(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26655
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
path := "/sub?target=clash&url=https://mock-subscribe.example.com&filename=custom.yaml"
|
|
resp := doRequest(t, server, http.MethodGet, path)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for /sub with filename, got %d", resp.StatusCode)
|
|
}
|
|
|
|
cd := resp.Header.Get("Content-Disposition")
|
|
if cd == "" {
|
|
t.Fatalf("expected Content-Disposition header")
|
|
}
|
|
if !strings.Contains(cd, "attachment") || !strings.Contains(cd, "custom.yaml") {
|
|
t.Fatalf("unexpected content-disposition header: %q", cd)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read subscription body: %v", err)
|
|
}
|
|
if len(body) == 0 {
|
|
t.Fatalf("expected non-empty body for subscription response")
|
|
}
|
|
}
|
|
|
|
func TestAPIAutoTargetResolution(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26690
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
path := "/api/convert?target=auto&url=https://mock-subscribe.example.com"
|
|
resp := doRequestWithBody(t, server, http.MethodGet, path, nil, map[string]string{"User-Agent": "Surge/1400"})
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for auto target with UA, got %d", resp.StatusCode)
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if format, _ := body["target_format"].(string); format != "surge" {
|
|
t.Fatalf("expected resolved target_format surge, got %q", format)
|
|
}
|
|
|
|
resp = doRequest(t, server, http.MethodGet, path)
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for auto target without UA, got %d", resp.StatusCode)
|
|
}
|
|
body = map[string]interface{}{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode fallback response: %v", err)
|
|
}
|
|
if format, _ := body["target_format"].(string); format != "clash" {
|
|
t.Fatalf("expected fallback target_format clash, got %q", format)
|
|
}
|
|
}
|
|
|
|
func TestRemoteFetchUsesRequestUserAgent(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26705
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
engine := app.GetConversionEngine()
|
|
if engine == nil {
|
|
t.Fatalf("expected conversion engine instance")
|
|
}
|
|
|
|
var clashFetches int32
|
|
var clashFallbackFetches int32
|
|
client := &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
resp := &http.Response{
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("")),
|
|
}
|
|
resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
|
switch req.URL.Path {
|
|
case "/clash":
|
|
if ua := req.Header.Get("User-Agent"); ua != "ClashForWindows/0.20.0" {
|
|
resp.StatusCode = http.StatusForbidden
|
|
resp.Body = io.NopCloser(strings.NewReader("unexpected ua"))
|
|
return resp, nil
|
|
}
|
|
atomic.AddInt32(&clashFetches, 1)
|
|
resp.StatusCode = http.StatusOK
|
|
resp.Body = io.NopCloser(strings.NewReader("ss://aes-256-cfb:password@198.51.100.2:8388"))
|
|
return resp, nil
|
|
case "/clash-default":
|
|
if ua := req.Header.Get("User-Agent"); ua != "ClashForWindows/0.20.0" {
|
|
resp.StatusCode = http.StatusForbidden
|
|
resp.Body = io.NopCloser(strings.NewReader("unexpected ua"))
|
|
return resp, nil
|
|
}
|
|
atomic.AddInt32(&clashFallbackFetches, 1)
|
|
resp.StatusCode = http.StatusOK
|
|
resp.Body = io.NopCloser(strings.NewReader("ss://aes-256-cfb:password@198.51.100.3:8388"))
|
|
return resp, nil
|
|
default:
|
|
resp.StatusCode = http.StatusNotFound
|
|
return resp, nil
|
|
}
|
|
}),
|
|
}
|
|
engine.SetHTTPClient(client)
|
|
server := app.GetHTTPServer()
|
|
|
|
t.Run("forwards client user agent", func(t *testing.T) {
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://ua-verify.test/clash")
|
|
resp := doRequestWithBody(t, server, http.MethodGet, "/api/convert?"+params.Encode(), nil, map[string]string{"User-Agent": "ClashForWindows/0.20.0"})
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 when forwarding UA, got %d", resp.StatusCode)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if ok, _ := payload["success"].(bool); !ok {
|
|
t.Fatalf("expected success flag in response")
|
|
}
|
|
if got := atomic.LoadInt32(&clashFetches); got != 1 {
|
|
t.Fatalf("expected exactly one clash fetch, got %d", got)
|
|
}
|
|
})
|
|
|
|
t.Run("falls back to target default user agent", func(t *testing.T) {
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://ua-verify.test/clash-default")
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for default UA fallback, got %d", resp.StatusCode)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if ok, _ := payload["success"].(bool); !ok {
|
|
t.Fatalf("expected success flag in fallback response")
|
|
}
|
|
if got := atomic.LoadInt32(&clashFallbackFetches); got != 1 {
|
|
t.Fatalf("expected exactly one fallback fetch, got %d", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIMultiSourceSubscriptionMerge(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26710
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://multi-source.example.com/first|https://multi-source.example.com/second")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("expected 200 for multi-source merge, got %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
|
|
if ok, _ := payload["success"].(bool); !ok {
|
|
content, _ := json.Marshal(payload)
|
|
t.Fatalf("expected success in multi-source response, got %s", string(content))
|
|
}
|
|
|
|
nodeCount, ok := payload["node_count"].(float64)
|
|
if !ok {
|
|
t.Fatalf("expected numeric node_count, got %T", payload["node_count"])
|
|
}
|
|
if int(nodeCount) != 2 {
|
|
content, _ := json.Marshal(payload)
|
|
t.Fatalf("expected node_count 2, got %d payload=%s", int(nodeCount), string(content))
|
|
}
|
|
|
|
content, ok := payload["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("expected content string, got %T", payload["content"])
|
|
}
|
|
if !strings.Contains(content, "multi-ss") {
|
|
t.Fatalf("expected merged content to include first source proxy name, got %s", content)
|
|
}
|
|
if !strings.Contains(content, "multi-vmess") {
|
|
t.Fatalf("expected merged content to include second source proxy name, got %s", content)
|
|
}
|
|
|
|
if opts, ok := payload["request_options"].(map[string]interface{}); ok {
|
|
if val, ok := opts["url"].(string); ok {
|
|
expected := "https://multi-source.example.com/first|https://multi-source.example.com/second"
|
|
if val != expected {
|
|
t.Fatalf("expected request_options url %q, got %q", expected, val)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionListMode(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26695
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
path := "/sub?target=clash&url=https://mock-subscribe.example.com&list=true"
|
|
resp := doRequest(t, server, http.MethodGet, path)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for list mode, got %d", resp.StatusCode)
|
|
}
|
|
|
|
if ct := resp.Header.Get("Content-Type"); ct != "text/plain; charset=utf-8" {
|
|
t.Fatalf("expected text/plain content type, got %q", ct)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read list body: %v", err)
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(string(body)), "\n")
|
|
if len(lines) < 2 {
|
|
t.Fatalf("expected multiple entries in list output, got %v", lines)
|
|
}
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
t.Fatalf("found empty entry in list output: %v", lines)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPIRenameRules(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26700
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("rename", "ss-@renamed-")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for rename rule, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, ok := body["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("content missing or not string: %v", body)
|
|
}
|
|
if !strings.Contains(content, "renamed-") {
|
|
t.Fatalf("expected renamed proxy name in content, got %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIRenameScript(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26701
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("list", "true")
|
|
params.Set("rename", "!!script:function rename(node) { return 'JS-' + node.Remark; }")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for rename script, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := payload["content"].(string)
|
|
if !strings.Contains(content, "JS-") {
|
|
t.Fatalf("expected JS- prefix in content, got %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIRenameImportScript(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26703
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
scriptDir := t.TempDir()
|
|
scriptPath := filepath.Join(scriptDir, "rename.js")
|
|
scriptContent := "function rename(node) { return 'IM-' + node.Remark; }"
|
|
if err := os.WriteFile(scriptPath, []byte(scriptContent), 0o644); err != nil {
|
|
t.Fatalf("failed to write script file: %v", err)
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("list", "true")
|
|
params.Set("rename", "!!import:"+scriptPath)
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for imported rename script, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := payload["content"].(string)
|
|
if !strings.Contains(content, "IM-") {
|
|
t.Fatalf("expected IM- prefix in content, got %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIRenameGeoIPScript(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26706
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("list", "true")
|
|
params.Set("rename", "!!script:function rename(node) { const info = JSON.parse(node.ProxyInfo); const geo = JSON.parse(geoip(info.Hostname)); if (geo.country_code === 'PRIVATE') { return 'LAN-' + node.Remark; } }")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for geoip rename script, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := payload["content"].(string)
|
|
if !strings.Contains(content, "LAN-") {
|
|
t.Fatalf("expected LAN- prefix in content, got %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIRenameImportScriptRemote(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26707
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
scriptServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprint(w, "function rename(node) { return 'REM-' + node.Remark; }")
|
|
}))
|
|
defer scriptServer.Close()
|
|
scriptURL, err := url.Parse(scriptServer.URL)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse script server url: %v", err)
|
|
}
|
|
app.GetConversionEngine().SetHTTPClient(newHybridHTTPClient(scriptURL.Host))
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("list", "true")
|
|
params.Set("rename", "!!import:"+scriptServer.URL+"/rename.js")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for remote rename import, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := payload["content"].(string)
|
|
if !strings.Contains(content, "REM-") {
|
|
t.Fatalf("expected REM- prefix in content, got %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIConvertRemoteSubscriptionCaching(t *testing.T) {
|
|
var hitCount int32
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&hitCount, 1)
|
|
fmt.Fprint(w, "ss://aes-256-cfb:password@198.18.0.1:8388\nvmess://eyJhZGQiOiIxOTguMTguMC4yIiwgInBzIjoiVm1lc3MgRemoteIiwgInBvcnQiOjQ0MywgImlkIjoiZDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAifQ==\ntrojan://password@198.18.0.3:443")
|
|
}))
|
|
defer backend.Close()
|
|
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26708
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
backendURL, err := url.Parse(backend.URL)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse backend url: %v", err)
|
|
}
|
|
app.GetConversionEngine().SetHTTPClient(newHybridHTTPClient(backendURL.Host))
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", backend.URL)
|
|
params.Set("list", "true")
|
|
|
|
for i := 0; i < 2; i++ {
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("iteration %d: failed to read response: %v", i, err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("iteration %d: expected 200, got %d; body=%s", i, resp.StatusCode, string(bodyBytes))
|
|
}
|
|
if !strings.Contains(string(bodyBytes), "content") {
|
|
t.Fatalf("iteration %d: unexpected response payload %s", i, string(bodyBytes))
|
|
}
|
|
}
|
|
|
|
if atomic.LoadInt32(&hitCount) != 1 {
|
|
t.Fatalf("expected backend to be hit once, got %d", hitCount)
|
|
}
|
|
}
|
|
|
|
func TestAPIFilterScript(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26709
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("list", "true")
|
|
params.Set("filter_script", "function filter(node) { return node.Remark.indexOf('ss-') !== -1; }")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for filter script, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := payload["content"].(string)
|
|
if strings.Contains(content, "ss-192.168.1.1") {
|
|
t.Fatalf("expected filter script to remove ss node, got %s", content)
|
|
}
|
|
if !(strings.Contains(content, "Test VMess") || strings.Contains(content, "trojan-")) {
|
|
t.Fatalf("expected remaining nodes to include a non-ss entry, got %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIUploadToFile(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26710
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
tempDir := t.TempDir()
|
|
outputPath := filepath.Join(tempDir, "surge.conf")
|
|
params := url.Values{}
|
|
params.Set("target", "surge")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("upload", "true")
|
|
params.Set("interval", "7200")
|
|
params.Set("strict", "true")
|
|
params.Set("upload_path", "file://"+outputPath)
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for upload request, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
data, err := os.ReadFile(outputPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read uploaded file: %v", err)
|
|
}
|
|
if len(data) == 0 {
|
|
t.Fatalf("expected uploaded file to have content")
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
headers, _ := payload["headers"].(map[string]interface{})
|
|
if val, ok := headers["profile-update-interval"].(string); !ok || val != "2" {
|
|
t.Fatalf("expected profile-update-interval=2, got %v", headers["profile-update-interval"])
|
|
}
|
|
|
|
if strings.Contains(string(data), "#!MANAGED-CONFIG") {
|
|
t.Fatalf("managed header should not be included in uploaded artifact")
|
|
}
|
|
}
|
|
|
|
func TestAPIEmojiScript(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26704
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("list", "true")
|
|
params.Set("add_emoji", "true")
|
|
params.Set("emoji_rule", "!!script:function getEmoji(node) { return '🧪'; }")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for emoji script, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := payload["content"].(string)
|
|
if !strings.Contains(content, "🧪") {
|
|
t.Fatalf("expected emoji in content, got %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIRenameScriptError(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26705
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("rename", "!!script:function notrename(node) { return 'BAD'; }")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed to read response: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid rename script, got %d; body=%s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
var payload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if success, _ := payload["success"].(bool); success {
|
|
t.Fatalf("expected success=false for invalid script, got %v", payload)
|
|
}
|
|
}
|
|
|
|
func TestAPICustomGroupsAndRulesets(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26702
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
groupDef := base64.StdEncoding.EncodeToString([]byte("name=Manual&type=select&proxies=ss-192.168.1.1:8388,vmess-192.168.1.2:443"))
|
|
ruleDef := base64.StdEncoding.EncodeToString([]byte("DOMAIN-SUFFIX,example.com,Manual"))
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("groups", groupDef)
|
|
params.Set("ruleset", ruleDef)
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for custom groups/rulesets, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, ok := body["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("missing content in response: %v", body)
|
|
}
|
|
|
|
parsed := parseClashYAML(t, content)
|
|
|
|
groupsVal, ok := parsed["proxy-groups"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("missing proxy-groups in config: %v", parsed)
|
|
}
|
|
|
|
foundManual := false
|
|
for _, entry := range groupsVal {
|
|
group, ok := entry.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if group["name"] == "Manual" {
|
|
proxies, _ := group["proxies"].([]interface{})
|
|
if len(proxies) == 0 {
|
|
t.Fatalf("manual group missing proxies: %v", group)
|
|
}
|
|
foundNames := make([]string, 0, len(proxies))
|
|
for _, p := range proxies {
|
|
if s, ok := p.(string); ok {
|
|
foundNames = append(foundNames, s)
|
|
}
|
|
}
|
|
if !contains(foundNames, "ss-192.168.1.1:8388") || !contains(foundNames, "vmess-192.168.1.2:443") {
|
|
t.Fatalf("manual group missing expected proxies: %v", foundNames)
|
|
}
|
|
foundManual = true
|
|
break
|
|
}
|
|
}
|
|
if !foundManual {
|
|
t.Fatalf("custom group Manual not found: %v", groupsVal)
|
|
}
|
|
|
|
if !strings.Contains(content, "DOMAIN-SUFFIX,example.com,Manual") {
|
|
t.Fatalf("custom ruleset not applied: %s", content)
|
|
}
|
|
}
|
|
|
|
func TestAPIEEmojiToggle(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26705
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("add_emoji", "true")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for add emoji, got %d", resp.StatusCode)
|
|
}
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := body["content"].(string)
|
|
parsed := parseClashYAML(t, content)
|
|
proxies, ok := parsed["proxies"].([]interface{})
|
|
if !ok || len(proxies) == 0 {
|
|
t.Fatalf("expected proxies in generated content, got %#v", parsed)
|
|
}
|
|
foundEmoji := false
|
|
for _, entry := range proxies {
|
|
if proxyMap, ok := entry.(map[string]interface{}); ok {
|
|
if name, ok := proxyMap["name"].(string); ok && hasEmojiPrefix(name) {
|
|
foundEmoji = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !foundEmoji {
|
|
t.Fatalf("expected emoji-prefixed proxies, got %s", content)
|
|
}
|
|
|
|
params = url.Values{}
|
|
params.Set("target", "clash")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("rename", ".*@🇭🇰 $0")
|
|
params.Set("remove_emoji", "true")
|
|
|
|
resp = doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for remove emoji, got %d", resp.StatusCode)
|
|
}
|
|
body = map[string]interface{}{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ = body["content"].(string)
|
|
parsed = parseClashYAML(t, content)
|
|
proxies, ok = parsed["proxies"].([]interface{})
|
|
if !ok || len(proxies) == 0 {
|
|
t.Fatalf("expected proxies in generated content, got %#v", parsed)
|
|
}
|
|
for _, entry := range proxies {
|
|
if proxyMap, ok := entry.(map[string]interface{}); ok {
|
|
if name, ok := proxyMap["name"].(string); ok && hasEmojiPrefix(name) {
|
|
t.Fatalf("expected leading emoji removed, got %s", name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func contains(items []string, target string) bool {
|
|
for _, item := range items {
|
|
if item == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestAPISurgeManagedConfig(t *testing.T) {
|
|
config := `server:
|
|
host: "127.0.0.1"
|
|
port: 26710
|
|
logging:
|
|
level: "error"
|
|
format: "text"
|
|
`
|
|
|
|
app, _, cleanup := setupTestApplicationWithConfig(t, config)
|
|
t.Cleanup(cleanup)
|
|
server := app.GetHTTPServer()
|
|
|
|
params := url.Values{}
|
|
params.Set("target", "surge")
|
|
params.Set("url", "https://mock-subscribe.example.com")
|
|
params.Set("upload", "true")
|
|
params.Set("interval", "7200")
|
|
params.Set("strict", "true")
|
|
|
|
resp := doRequest(t, server, http.MethodGet, "/api/convert?"+params.Encode())
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for managed config, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
content, _ := body["content"].(string)
|
|
if !strings.Contains(content, "#!MANAGED-CONFIG ") {
|
|
t.Fatalf("managed config header missing: %s", content)
|
|
}
|
|
if !strings.Contains(content, "interval=7200") {
|
|
t.Fatalf("interval not encoded in managed config header: %s", content)
|
|
}
|
|
headers, _ := body["headers"].(map[string]interface{})
|
|
if headers == nil {
|
|
t.Fatalf("headers not returned: %v", body)
|
|
}
|
|
if val, ok := headers["profile-update-interval"].(string); !ok || val != "2" {
|
|
t.Fatalf("expected profile-update-interval=2, got %v", headers["profile-update-interval"])
|
|
}
|
|
}
|