mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-17 02:42:06 +08:00
- 更新检查前恢复持久化全局代理,避免重启后代理状态未进入运行时 - 将 DNS、EOF 和临时网络异常转为本地化错误,并对可重试网络错误做短重试 - 优先读取 GitHub asset digest,缺失时再回退 SHA256SUMS 校验文件 - 新增全局代理连接测试后端接口,支持草稿代理、已保存密码复用和清除密码意图 - 更新 Wails 绑定、浏览器 mock 与代理草稿序列化测试
386 lines
11 KiB
Go
386 lines
11 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"GoNavi-Wails/internal/connection"
|
|
"GoNavi-Wails/shared/i18n"
|
|
)
|
|
|
|
func TestSaveGlobalProxyStripsPasswordFromView(t *testing.T) {
|
|
store := newFakeAppSecretStore()
|
|
app := NewAppWithSecretStore(store)
|
|
app.configDir = t.TempDir()
|
|
|
|
view, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
Password: "proxy-secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("saveGlobalProxy returned error: %v", err)
|
|
}
|
|
if view.Password != "" {
|
|
t.Fatal("global proxy view must not expose plaintext password")
|
|
}
|
|
if !view.HasPassword {
|
|
t.Fatal("expected hasPassword=true")
|
|
}
|
|
|
|
snapshot := currentGlobalProxyConfig()
|
|
if snapshot.Proxy.Password != "proxy-secret" {
|
|
t.Fatalf("expected runtime proxy password to be preserved, got %q", snapshot.Proxy.Password)
|
|
}
|
|
}
|
|
|
|
func TestGetGlobalProxyConfigReturnsSecretlessView(t *testing.T) {
|
|
store := newFakeAppSecretStore()
|
|
app := NewAppWithSecretStore(store)
|
|
app.configDir = t.TempDir()
|
|
|
|
if _, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
Password: "proxy-secret",
|
|
}); err != nil {
|
|
t.Fatalf("saveGlobalProxy returned error: %v", err)
|
|
}
|
|
|
|
result := app.GetGlobalProxyConfig()
|
|
view, ok := result.Data.(connection.GlobalProxyView)
|
|
if !ok {
|
|
t.Fatalf("expected GlobalProxyView, got %T", result.Data)
|
|
}
|
|
if view.Password != "" {
|
|
t.Fatal("GetGlobalProxyConfig must not expose plaintext password")
|
|
}
|
|
if !view.HasPassword {
|
|
t.Fatal("expected hasPassword=true")
|
|
}
|
|
}
|
|
|
|
func TestSaveGlobalProxyClearPasswordRemovesStoredSecret(t *testing.T) {
|
|
store := newFakeAppSecretStore()
|
|
app := NewAppWithSecretStore(store)
|
|
app.configDir = t.TempDir()
|
|
|
|
if _, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
Password: "proxy-secret",
|
|
}); err != nil {
|
|
t.Fatalf("saveGlobalProxy with password returned error: %v", err)
|
|
}
|
|
|
|
view, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
ClearPassword: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("saveGlobalProxy clear password returned error: %v", err)
|
|
}
|
|
if view.HasPassword {
|
|
t.Fatal("expected global proxy password marker to be cleared")
|
|
}
|
|
if secret, ok, err := app.dailySecretStore().GetGlobalProxy(); err != nil {
|
|
t.Fatalf("GetGlobalProxy returned error: %v", err)
|
|
} else if ok || secret.Password != "" {
|
|
t.Fatalf("expected global proxy secret to be deleted, got %#v ok=%v", secret, ok)
|
|
}
|
|
snapshot := currentGlobalProxyConfig()
|
|
if snapshot.Proxy.Password != "" {
|
|
t.Fatalf("expected runtime proxy password to be cleared, got %q", snapshot.Proxy.Password)
|
|
}
|
|
}
|
|
|
|
func TestSaveGlobalProxyDisabledKeepsDraftAndStoredPassword(t *testing.T) {
|
|
store := newFakeAppSecretStore()
|
|
app := NewAppWithSecretStore(store)
|
|
app.configDir = t.TempDir()
|
|
|
|
if _, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
Password: "proxy-secret",
|
|
}); err != nil {
|
|
t.Fatalf("saveGlobalProxy with password returned error: %v", err)
|
|
}
|
|
|
|
view, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: false,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("saveGlobalProxy disabled draft returned error: %v", err)
|
|
}
|
|
if view.Enabled {
|
|
t.Fatal("expected saved proxy view to stay disabled")
|
|
}
|
|
if view.Host != "127.0.0.1" || view.Port != 8080 || view.User != "ops" {
|
|
t.Fatalf("expected disabled draft fields to be retained, got %#v", view)
|
|
}
|
|
if !view.HasPassword {
|
|
t.Fatal("expected disabled draft to keep saved password marker")
|
|
}
|
|
if snapshot := currentGlobalProxyConfig(); snapshot.Enabled {
|
|
t.Fatalf("expected runtime global proxy to be disabled, got %#v", snapshot)
|
|
}
|
|
if secret, ok, err := app.dailySecretStore().GetGlobalProxy(); err != nil {
|
|
t.Fatalf("GetGlobalProxy returned error: %v", err)
|
|
} else if !ok || secret.Password != "proxy-secret" {
|
|
t.Fatalf("expected saved proxy password to be retained, got %#v ok=%v", secret, ok)
|
|
}
|
|
|
|
result := app.GetGlobalProxyConfig()
|
|
stored, ok := result.Data.(connection.GlobalProxyView)
|
|
if !ok {
|
|
t.Fatalf("expected GlobalProxyView, got %T", result.Data)
|
|
}
|
|
if stored.Enabled || stored.Host != "127.0.0.1" || !stored.HasPassword {
|
|
t.Fatalf("expected GetGlobalProxyConfig to return disabled persisted draft, got %#v", stored)
|
|
}
|
|
}
|
|
|
|
func TestTestGlobalProxyConnectionUsesDraftHTTPProxy(t *testing.T) {
|
|
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
|
app.configDir = t.TempDir()
|
|
|
|
proxyCalled := false
|
|
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
proxyCalled = true
|
|
if !r.URL.IsAbs() {
|
|
t.Fatalf("expected proxy request URL to be absolute, got %q", r.URL.String())
|
|
}
|
|
if r.URL.String() != "http://example.com/probe" {
|
|
t.Fatalf("unexpected target URL through proxy: %s", r.URL.String())
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer proxyServer.Close()
|
|
|
|
host, port := parseTestServerHostPort(t, proxyServer.URL)
|
|
result := app.TestGlobalProxyConnection(connection.TestGlobalProxyInput{
|
|
Proxy: connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: host,
|
|
Port: port,
|
|
},
|
|
URL: "http://example.com/probe",
|
|
TimeoutSeconds: 2,
|
|
})
|
|
if !result.Success {
|
|
t.Fatalf("expected proxy test success, got %#v", result)
|
|
}
|
|
if !proxyCalled {
|
|
t.Fatal("expected draft proxy to receive the test request")
|
|
}
|
|
data, ok := result.Data.(connection.GlobalProxyTestResult)
|
|
if !ok {
|
|
t.Fatalf("expected GlobalProxyTestResult, got %T", result.Data)
|
|
}
|
|
if data.StatusCode != http.StatusNoContent || !data.ViaProxy {
|
|
t.Fatalf("unexpected proxy test data: %#v", data)
|
|
}
|
|
}
|
|
|
|
func TestTestGlobalProxyConnectionReusesSavedPasswordWhenDraftPasswordBlank(t *testing.T) {
|
|
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
|
app.configDir = t.TempDir()
|
|
|
|
if _, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
Password: "proxy-secret",
|
|
}); err != nil {
|
|
t.Fatalf("saveGlobalProxy returned error: %v", err)
|
|
}
|
|
|
|
wantAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("ops:proxy-secret"))
|
|
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("Proxy-Authorization"); got != wantAuth {
|
|
t.Fatalf("expected saved proxy password auth %q, got %q", wantAuth, got)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer proxyServer.Close()
|
|
|
|
host, port := parseTestServerHostPort(t, proxyServer.URL)
|
|
result := app.TestGlobalProxyConnection(connection.TestGlobalProxyInput{
|
|
Proxy: connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: host,
|
|
Port: port,
|
|
User: "ops",
|
|
},
|
|
URL: "http://example.com/probe",
|
|
TimeoutSeconds: 2,
|
|
})
|
|
if !result.Success {
|
|
t.Fatalf("expected proxy test to reuse saved password, got %#v", result)
|
|
}
|
|
}
|
|
|
|
func parseTestServerHostPort(t *testing.T, rawURL string) (string, int) {
|
|
t.Helper()
|
|
parsed, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
t.Fatalf("url.Parse returned error: %v", err)
|
|
}
|
|
host, portText, err := net.SplitHostPort(parsed.Host)
|
|
if err != nil {
|
|
t.Fatalf("SplitHostPort returned error: %v", err)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
t.Fatalf("Atoi returned error: %v", err)
|
|
}
|
|
return host, port
|
|
}
|
|
|
|
func TestLoadPersistedGlobalProxyOnDarwinUsesInlinePassword(t *testing.T) {
|
|
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
|
|
t.Fatalf("setGlobalProxyConfig returned error: %v", err)
|
|
}
|
|
|
|
app := NewAppWithSecretStore(failOnUseSecretStore{})
|
|
app.configDir = t.TempDir()
|
|
|
|
if _, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
User: "ops",
|
|
Password: "proxy-secret",
|
|
}); err != nil {
|
|
t.Fatalf("saveGlobalProxy returned error: %v", err)
|
|
}
|
|
|
|
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
|
|
t.Fatalf("setGlobalProxyConfig reset returned error: %v", err)
|
|
}
|
|
|
|
app.loadPersistedGlobalProxy()
|
|
snapshot := currentGlobalProxyConfig()
|
|
if !snapshot.Enabled {
|
|
t.Fatal("expected persisted global proxy to be restored")
|
|
}
|
|
if snapshot.Proxy.Password != "proxy-secret" {
|
|
t.Fatalf("expected daily-stored global proxy password to be restored, got %q", snapshot.Proxy.Password)
|
|
}
|
|
}
|
|
|
|
func TestSaveGlobalProxyUsesCurrentLanguageForValidationErrors(t *testing.T) {
|
|
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
|
|
t.Fatalf("setGlobalProxyConfig reset returned error: %v", err)
|
|
}
|
|
|
|
store := newFakeAppSecretStore()
|
|
app := NewAppWithSecretStore(store)
|
|
app.configDir = t.TempDir()
|
|
app.SetLanguage(string(i18n.LanguageEnUS))
|
|
t.Cleanup(func() {
|
|
app.SetLanguage(string(i18n.LanguageZhCN))
|
|
_, _ = setGlobalProxyConfig(false, connection.ProxyConfig{})
|
|
})
|
|
|
|
_, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
|
Enabled: true,
|
|
Type: "Shadowsocks",
|
|
Host: "127.0.0.1",
|
|
Port: 1080,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected saveGlobalProxy to reject unsupported proxy type")
|
|
}
|
|
|
|
want := app.appText("proxy.backend.error.unsupported_type", map[string]any{"type": "Shadowsocks"})
|
|
if err.Error() != want {
|
|
t.Fatalf("expected localized saveGlobalProxy error %q, got %q", want, err.Error())
|
|
}
|
|
if strings.Contains(err.Error(), "不支持的代理类型") {
|
|
t.Fatalf("expected no raw Chinese proxy validation error, got %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestConfigureGlobalProxyUsesCurrentLanguageForSuccessMessage(t *testing.T) {
|
|
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
|
|
t.Fatalf("setGlobalProxyConfig reset returned error: %v", err)
|
|
}
|
|
|
|
store := newFakeAppSecretStore()
|
|
app := NewAppWithSecretStore(store)
|
|
app.configDir = t.TempDir()
|
|
app.SetLanguage(string(i18n.LanguageEnUS))
|
|
t.Cleanup(func() {
|
|
app.SetLanguage(string(i18n.LanguageZhCN))
|
|
_, _ = setGlobalProxyConfig(false, connection.ProxyConfig{})
|
|
})
|
|
|
|
result := app.ConfigureGlobalProxy(true, connection.ProxyConfig{
|
|
Type: "http",
|
|
Host: "127.0.0.1",
|
|
Port: 8080,
|
|
})
|
|
if !result.Success {
|
|
t.Fatalf("expected ConfigureGlobalProxy success, got %+v", result)
|
|
}
|
|
|
|
want := app.appText("app.proxy.message.config_applied", nil)
|
|
if result.Message != want {
|
|
t.Fatalf("expected localized ConfigureGlobalProxy success message %q, got %q", want, result.Message)
|
|
}
|
|
if strings.Contains(result.Message, "全局代理配置已生效") {
|
|
t.Fatalf("expected no raw Chinese global proxy success message, got %q", result.Message)
|
|
}
|
|
}
|
|
|
|
func TestBuildProxyURLFromConfigDefensiveErrorsStayInternalEnglish(t *testing.T) {
|
|
sourceBytes, err := os.ReadFile("global_proxy.go")
|
|
if err != nil {
|
|
t.Fatalf("read global_proxy.go: %v", err)
|
|
}
|
|
source := string(sourceBytes)
|
|
for _, literal := range []string{
|
|
"不支持的代理类型",
|
|
"代理地址不能为空",
|
|
"代理端口无效",
|
|
} {
|
|
if strings.Contains(source, literal) {
|
|
t.Fatalf("buildProxyURLFromConfig should not keep raw Chinese defensive error %q", literal)
|
|
}
|
|
}
|
|
}
|