mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-15 09:23:08 +08:00
🐛 fix(update/proxy): 稳定更新检查与代理运行时
- 更新检查前恢复持久化全局代理,避免重启后代理状态未进入运行时 - 将 DNS、EOF 和临时网络异常转为本地化错误,并对可重试网络错误做短重试 - 优先读取 GitHub asset digest,缺失时再回退 SHA256SUMS 校验文件 - 新增全局代理连接测试后端接口,支持草稿代理、已保存密码复用和清除密码意图 - 更新 Wails 绑定、浏览器 mock 与代理草稿序列化测试
This commit is contained in:
@@ -212,11 +212,13 @@ if (
|
||||
|
||||
const saveMockGlobalProxy = (input: any) => {
|
||||
const nextPassword = String(input?.password ?? '');
|
||||
const clearPassword = input?.clearPassword === true;
|
||||
mockGlobalProxy = {
|
||||
...mockGlobalProxy,
|
||||
...input,
|
||||
password: '',
|
||||
hasPassword: nextPassword !== '' ? true : !!mockGlobalProxy.hasPassword,
|
||||
hasPassword: clearPassword ? false : (nextPassword !== '' ? true : !!mockGlobalProxy.hasPassword),
|
||||
clearPassword: undefined,
|
||||
};
|
||||
return cloneBrowserMockValue(mockGlobalProxy);
|
||||
};
|
||||
@@ -432,6 +434,21 @@ if (
|
||||
},
|
||||
SaveGlobalProxy: async (input: any) => saveMockGlobalProxy(input),
|
||||
ImportLegacyGlobalProxy: async (input: any) => saveMockGlobalProxy(input),
|
||||
TestGlobalProxyConnection: async (input: any) => {
|
||||
const url = String(input?.url || 'https://api.github.com/').trim();
|
||||
return {
|
||||
success: true,
|
||||
message: t('app.proxy.backend.message.test_success', { status: 200, duration: 18, url }),
|
||||
data: {
|
||||
url,
|
||||
finalUrl: url,
|
||||
statusCode: 200,
|
||||
status: '200 OK',
|
||||
durationMs: 18,
|
||||
viaProxy: input?.proxy?.enabled === true,
|
||||
},
|
||||
};
|
||||
},
|
||||
SelectDataRootDirectory: async (currentPath: string) => ({ success: true, data: { ...mockDataRootInfo, path: currentPath || mockDataRootInfo.path } }),
|
||||
ApplyDataRootDirectory: async (path: string) => {
|
||||
const nextPath = String(path || mockDataRootInfo.defaultPath);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createGlobalProxyDraft, toPersistedGlobalProxy } from './globalProxyDraft';
|
||||
import { createGlobalProxyDraft, toPersistedGlobalProxy, toSaveGlobalProxyInput } from './globalProxyDraft';
|
||||
|
||||
describe('global proxy draft', () => {
|
||||
it('hydrates a secretless draft from backend metadata while keeping password input blank', () => {
|
||||
@@ -32,4 +32,18 @@ describe('global proxy draft', () => {
|
||||
expect('password' in persisted).toBe(false);
|
||||
expect(persisted.hasPassword).toBe(true);
|
||||
});
|
||||
|
||||
it('passes explicit saved password clearing intent to the backend save payload', () => {
|
||||
const input = toSaveGlobalProxyInput({
|
||||
enabled: true,
|
||||
type: 'socks5',
|
||||
host: '127.0.0.1',
|
||||
port: 7890,
|
||||
hasPassword: true,
|
||||
clearPassword: true,
|
||||
});
|
||||
|
||||
expect(input.password).toBe('');
|
||||
expect(input.clearPassword).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { GlobalProxyConfig } from '../types';
|
||||
|
||||
type SaveGlobalProxyDraft = Partial<GlobalProxyConfig> & {
|
||||
clearPassword?: boolean;
|
||||
};
|
||||
|
||||
const toTrimmedString = (value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim();
|
||||
@@ -53,10 +57,11 @@ export function toPersistedGlobalProxy(value: Partial<GlobalProxyConfig> = {}):
|
||||
};
|
||||
}
|
||||
|
||||
export function toSaveGlobalProxyInput(value: Partial<GlobalProxyConfig> = {}): GlobalProxyConfig {
|
||||
export function toSaveGlobalProxyInput(value: SaveGlobalProxyDraft = {}): GlobalProxyConfig & { clearPassword?: boolean } {
|
||||
const draft = createGlobalProxyDraft(value);
|
||||
return {
|
||||
...draft,
|
||||
password: typeof value.password === 'string' ? value.password : '',
|
||||
clearPassword: value.clearPassword === true || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
2
frontend/wailsjs/go/app/App.d.ts
vendored
2
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -372,6 +372,8 @@ export function StartSecurityUpdate(arg1:app.StartSecurityUpdateRequest):Promise
|
||||
|
||||
export function TestConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
export function TestGlobalProxyConnection(arg1:connection.TestGlobalProxyInput):Promise<connection.QueryResult>;
|
||||
|
||||
export function TestJVMConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||
|
||||
export function TruncateTables(arg1:connection.ConnectionConfig,arg2:string,arg3:Array<string>):Promise<connection.QueryResult>;
|
||||
|
||||
@@ -734,6 +734,10 @@ export function TestConnection(arg1) {
|
||||
return window['go']['app']['App']['TestConnection'](arg1);
|
||||
}
|
||||
|
||||
export function TestGlobalProxyConnection(arg1) {
|
||||
return window['go']['app']['App']['TestGlobalProxyConnection'](arg1);
|
||||
}
|
||||
|
||||
export function TestJVMConnection(arg1) {
|
||||
return window['go']['app']['App']['TestJVMConnection'](arg1);
|
||||
}
|
||||
|
||||
@@ -1161,6 +1161,7 @@ export namespace connection {
|
||||
port: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
clearPassword?: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SaveGlobalProxyInput(source);
|
||||
@@ -1174,6 +1175,7 @@ export namespace connection {
|
||||
this.port = source["port"];
|
||||
this.user = source["user"];
|
||||
this.password = source["password"];
|
||||
this.clearPassword = source["clearPassword"];
|
||||
}
|
||||
}
|
||||
export class SavedConnectionInput {
|
||||
@@ -1360,6 +1362,40 @@ export namespace connection {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class TestGlobalProxyInput {
|
||||
proxy: SaveGlobalProxyInput;
|
||||
url: string;
|
||||
timeoutSeconds?: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new TestGlobalProxyInput(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.proxy = this.convertValues(source["proxy"], SaveGlobalProxyInput);
|
||||
this.url = source["url"];
|
||||
this.timeoutSeconds = source["timeoutSeconds"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1555,4 +1591,3 @@ export namespace sync {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -139,6 +141,18 @@ func currentGlobalProxyView() connection.GlobalProxyView {
|
||||
}
|
||||
|
||||
func (a *App) GetGlobalProxyConfig() connection.QueryResult {
|
||||
if strings.TrimSpace(a.configDir) == "" {
|
||||
a.configDir = resolveAppConfigDir()
|
||||
}
|
||||
if view, err := a.loadStoredGlobalProxyView(); err == nil {
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Message: "OK",
|
||||
Data: sanitizeGlobalProxyView(view),
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
logger.Error(err, "加载全局代理元数据失败")
|
||||
}
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Message: "OK",
|
||||
@@ -186,6 +200,33 @@ func newHTTPClientWithGlobalProxy(timeout time.Duration) *http.Client {
|
||||
return client
|
||||
}
|
||||
|
||||
func (a *App) ensurePersistedGlobalProxyRuntime() {
|
||||
if currentGlobalProxyConfig().Enabled {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(a.configDir) == "" {
|
||||
a.configDir = resolveAppConfigDir()
|
||||
}
|
||||
view, err := a.loadStoredGlobalProxyView()
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Error(err, "加载全局代理元数据失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
if !view.Enabled {
|
||||
return
|
||||
}
|
||||
proxyConfig, err := a.resolveStoredGlobalProxyRuntimeConfig(view)
|
||||
if err != nil {
|
||||
logger.Error(err, "恢复全局代理运行时配置失败")
|
||||
return
|
||||
}
|
||||
if _, err := setGlobalProxyConfig(true, proxyConfig); err != nil {
|
||||
logger.Error(err, "恢复全局代理运行时配置失败")
|
||||
}
|
||||
}
|
||||
|
||||
func buildHTTPTransportWithGlobalProxy() http.RoundTripper {
|
||||
baseTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok || baseTransport == nil {
|
||||
@@ -199,16 +240,29 @@ func buildHTTPTransportWithGlobalProxy() http.RoundTripper {
|
||||
return transport
|
||||
}
|
||||
|
||||
proxyURL, err := buildProxyURLFromConfig(snapshot.Proxy)
|
||||
transportWithProxy, err := buildHTTPTransportForProxyConfig(snapshot.Proxy)
|
||||
if err != nil {
|
||||
logger.Warnf("全局代理配置无效,回退系统代理:%v", err)
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
return transport
|
||||
}
|
||||
return transportWithProxy
|
||||
}
|
||||
|
||||
func buildHTTPTransportForProxyConfig(proxyConfig connection.ProxyConfig) (http.RoundTripper, error) {
|
||||
baseTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok || baseTransport == nil {
|
||||
return nil, fmt.Errorf("default HTTP transport unavailable")
|
||||
}
|
||||
|
||||
transport := baseTransport.Clone()
|
||||
proxyURL, err := buildProxyURLFromConfig(proxyConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
if !isLoopbackProxyHost(snapshot.Proxy.Host) {
|
||||
return transport
|
||||
if !isLoopbackProxyHost(proxyConfig.Host) {
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
fallbackTransport := transport.Clone()
|
||||
@@ -217,7 +271,7 @@ func buildHTTPTransportWithGlobalProxy() http.RoundTripper {
|
||||
primary: transport,
|
||||
fallback: fallbackTransport,
|
||||
proxyEndpoint: proxyURL.Redacted(),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *localProxyTLSFallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
@@ -328,3 +382,156 @@ func buildProxyURLFromConfig(proxyConfig connection.ProxyConfig) (*url.URL, erro
|
||||
return proxyURL, nil
|
||||
}
|
||||
|
||||
func (a *App) TestGlobalProxyConnection(input connection.TestGlobalProxyInput) connection.QueryResult {
|
||||
started := time.Now()
|
||||
targetURL, err := normalizeGlobalProxyTestURL(input.URL)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: a.localizedGlobalProxyTestError(err)}
|
||||
}
|
||||
|
||||
timeoutSeconds := input.TimeoutSeconds
|
||||
if timeoutSeconds <= 0 {
|
||||
timeoutSeconds = 8
|
||||
}
|
||||
if timeoutSeconds > 30 {
|
||||
timeoutSeconds = 30
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: time.Duration(timeoutSeconds) * time.Second}
|
||||
if input.Proxy.Enabled {
|
||||
proxyPassword, err := a.resolveGlobalProxyPasswordForInput(input.Proxy)
|
||||
if err != nil {
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("app.proxy.backend.message.test_failed", map[string]any{
|
||||
"url": targetURL,
|
||||
"detail": err.Error(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
transport, err := buildHTTPTransportForProxyConfig(connection.ProxyConfig{
|
||||
Type: input.Proxy.Type,
|
||||
Host: input.Proxy.Host,
|
||||
Port: input.Proxy.Port,
|
||||
User: input.Proxy.User,
|
||||
Password: proxyPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
client.Transport = transport
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, targetURL, nil)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
req.Header.Set("User-Agent", "GoNavi-Proxy-Test")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Range", "bytes=0-0")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
durationMs := time.Since(started).Milliseconds()
|
||||
if err != nil {
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: a.appText("app.proxy.backend.message.test_failed", map[string]any{
|
||||
"url": targetURL,
|
||||
"detail": err.Error(),
|
||||
}),
|
||||
Data: connection.GlobalProxyTestResult{
|
||||
URL: targetURL,
|
||||
DurationMs: durationMs,
|
||||
ViaProxy: input.Proxy.Enabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, _ = io.CopyN(io.Discard, resp.Body, 512)
|
||||
|
||||
result := connection.GlobalProxyTestResult{
|
||||
URL: targetURL,
|
||||
FinalURL: resp.Request.URL.String(),
|
||||
StatusCode: resp.StatusCode,
|
||||
Status: resp.Status,
|
||||
DurationMs: durationMs,
|
||||
ViaProxy: input.Proxy.Enabled,
|
||||
}
|
||||
messageKey := "app.proxy.backend.message.test_success"
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
messageKey = "app.proxy.backend.message.test_http_status"
|
||||
}
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Message: a.appText(messageKey, map[string]any{
|
||||
"status": resp.StatusCode,
|
||||
"duration": durationMs,
|
||||
"url": targetURL,
|
||||
}),
|
||||
Data: result,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) resolveGlobalProxyPasswordForInput(input connection.SaveGlobalProxyInput) (string, error) {
|
||||
if strings.TrimSpace(input.Password) != "" {
|
||||
return input.Password, nil
|
||||
}
|
||||
if input.ClearPassword {
|
||||
return "", nil
|
||||
}
|
||||
if strings.TrimSpace(a.configDir) == "" {
|
||||
a.configDir = resolveAppConfigDir()
|
||||
}
|
||||
existing, err := a.loadStoredGlobalProxyView()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if !existing.HasPassword {
|
||||
return "", nil
|
||||
}
|
||||
bundle, err := a.loadGlobalProxySecretBundle(existing)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return bundle.Password, nil
|
||||
}
|
||||
|
||||
func normalizeGlobalProxyTestURL(rawURL string) (string, error) {
|
||||
trimmed := strings.TrimSpace(rawURL)
|
||||
if trimmed == "" {
|
||||
return "", localizedUpdateError{key: "app.proxy.backend.error.test_url_empty"}
|
||||
}
|
||||
if !strings.Contains(trimmed, "://") {
|
||||
trimmed = "https://" + trimmed
|
||||
}
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil {
|
||||
return "", localizedUpdateError{
|
||||
key: "app.proxy.backend.error.test_url_invalid",
|
||||
params: map[string]any{"detail": err.Error()},
|
||||
}
|
||||
}
|
||||
switch strings.ToLower(parsed.Scheme) {
|
||||
case "http", "https":
|
||||
default:
|
||||
return "", localizedUpdateError{
|
||||
key: "app.proxy.backend.error.test_scheme_unsupported",
|
||||
params: map[string]any{"scheme": parsed.Scheme},
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(parsed.Host) == "" {
|
||||
return "", localizedUpdateError{key: "app.proxy.backend.error.test_host_missing"}
|
||||
}
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
func (a *App) localizedGlobalProxyTestError(err error) string {
|
||||
var localized localizedUpdateError
|
||||
if errors.As(err, &localized) {
|
||||
return a.appText(localized.key, localized.params)
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (a *App) saveGlobalProxy(input connection.SaveGlobalProxyInput) (connection
|
||||
bundle := globalProxySecretBundle{}
|
||||
if strings.TrimSpace(input.Password) != "" {
|
||||
bundle.Password = input.Password
|
||||
} else if existing.HasPassword {
|
||||
} else if existing.HasPassword && !input.ClearPassword {
|
||||
existingBundle, loadErr := a.loadGlobalProxySecretBundle(existing)
|
||||
if loadErr != nil {
|
||||
return connection.GlobalProxyView{}, loadErr
|
||||
@@ -55,20 +55,6 @@ func (a *App) saveGlobalProxy(input connection.SaveGlobalProxyInput) (connection
|
||||
bundle = existingBundle
|
||||
}
|
||||
|
||||
if !view.Enabled {
|
||||
if deleteErr := a.dailySecretStore().DeleteGlobalProxy(); deleteErr != nil {
|
||||
return connection.GlobalProxyView{}, deleteErr
|
||||
}
|
||||
view = connection.GlobalProxyView{Enabled: false}
|
||||
if err := a.persistGlobalProxyView(view); err != nil {
|
||||
return connection.GlobalProxyView{}, err
|
||||
}
|
||||
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
|
||||
return connection.GlobalProxyView{}, err
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(bundle.Password) != "" {
|
||||
if storeErr := a.dailySecretStore().PutGlobalProxy(toDailyGlobalProxyBundle(bundle)); storeErr != nil {
|
||||
return connection.GlobalProxyView{}, storeErr
|
||||
@@ -86,6 +72,12 @@ func (a *App) saveGlobalProxy(input connection.SaveGlobalProxyInput) (connection
|
||||
if err := a.persistGlobalProxyView(view); err != nil {
|
||||
return connection.GlobalProxyView{}, err
|
||||
}
|
||||
if !view.Enabled {
|
||||
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
|
||||
return connection.GlobalProxyView{}, err
|
||||
}
|
||||
return sanitizeGlobalProxyView(view), nil
|
||||
}
|
||||
if _, err := setGlobalProxyConfig(true, connection.ProxyConfig{
|
||||
Type: view.Type,
|
||||
Host: view.Host,
|
||||
@@ -139,6 +131,28 @@ func (a *App) loadGlobalProxySecretBundle(view connection.GlobalProxyView) (glob
|
||||
return globalProxySecretBundle{}, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (a *App) resolveStoredGlobalProxyRuntimeConfig(view connection.GlobalProxyView) (connection.ProxyConfig, error) {
|
||||
proxyConfig := connection.ProxyConfig{
|
||||
Type: view.Type,
|
||||
Host: view.Host,
|
||||
Port: view.Port,
|
||||
User: view.User,
|
||||
}
|
||||
if !view.HasPassword {
|
||||
return proxyConfig, nil
|
||||
}
|
||||
bundle, err := a.loadGlobalProxySecretBundle(view)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logger.Warnf("全局代理标记了已保存密码,但密文不存在,将尝试按无认证代理恢复")
|
||||
return proxyConfig, nil
|
||||
}
|
||||
return connection.ProxyConfig{}, err
|
||||
}
|
||||
proxyConfig.Password = bundle.Password
|
||||
return proxyConfig, nil
|
||||
}
|
||||
|
||||
func (a *App) loadGlobalProxySecretBundleFromStore(view connection.GlobalProxyView) (globalProxySecretBundle, error) {
|
||||
if a.secretStore == nil {
|
||||
return globalProxySecretBundle{}, fmt.Errorf("secret store unavailable")
|
||||
@@ -195,20 +209,17 @@ func (a *App) loadPersistedGlobalProxy() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
proxyConfig := connection.ProxyConfig{
|
||||
Type: view.Type,
|
||||
Host: view.Host,
|
||||
Port: view.Port,
|
||||
User: view.User,
|
||||
}
|
||||
if view.HasPassword {
|
||||
bundle, loadErr := a.loadGlobalProxySecretBundle(view)
|
||||
if loadErr != nil {
|
||||
logger.Error(loadErr, "加载全局代理密码失败")
|
||||
return
|
||||
if !view.Enabled {
|
||||
if _, err := setGlobalProxyConfig(false, connection.ProxyConfig{}); err != nil {
|
||||
logger.Error(err, "恢复全局代理关闭状态失败")
|
||||
}
|
||||
proxyConfig.Password = bundle.Password
|
||||
return
|
||||
}
|
||||
|
||||
proxyConfig, err := a.resolveStoredGlobalProxyRuntimeConfig(view)
|
||||
if err != nil {
|
||||
logger.Error(err, "加载全局代理密码失败")
|
||||
return
|
||||
}
|
||||
if _, err := setGlobalProxyConfig(view.Enabled, proxyConfig); err != nil {
|
||||
logger.Error(err, "恢复全局代理配置失败")
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -67,6 +73,202 @@ func TestGetGlobalProxyConfigReturnsSecretlessView(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
urlpkg "net/url"
|
||||
"os"
|
||||
@@ -31,6 +32,7 @@ const (
|
||||
updateDevAPIURL = "https://api.github.com/repos/" + updateRepo + "/releases/tags/" + updateDevReleaseTag
|
||||
updateChecksumAsset = "SHA256SUMS"
|
||||
updateDownloadProgressEvent = "update:download-progress"
|
||||
updateNetworkRetryDelay = 250 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -150,6 +152,7 @@ func (a *App) CheckForUpdatesSilently() connection.QueryResult {
|
||||
}
|
||||
|
||||
func (a *App) checkForUpdates(logFailure bool) connection.QueryResult {
|
||||
a.ensurePersistedGlobalProxyRuntime()
|
||||
channel := a.currentUpdateChannel()
|
||||
info, err := fetchLatestUpdateInfo(channel)
|
||||
if err != nil {
|
||||
@@ -477,11 +480,14 @@ func fetchLatestUpdateInfo(channel updateChannel) (UpdateInfo, error) {
|
||||
return UpdateInfo{}, err
|
||||
}
|
||||
|
||||
hashMap, err := updateFetchReleaseSHA256(release.Assets)
|
||||
if err != nil {
|
||||
return UpdateInfo{}, err
|
||||
sha256Value := normalizeGitHubAssetSHA256(asset.Digest)
|
||||
if sha256Value == "" {
|
||||
hashMap, err := updateFetchReleaseSHA256(release.Assets)
|
||||
if err != nil {
|
||||
return UpdateInfo{}, err
|
||||
}
|
||||
sha256Value = strings.TrimSpace(hashMap[assetName])
|
||||
}
|
||||
sha256Value := strings.TrimSpace(hashMap[assetName])
|
||||
if sha256Value == "" {
|
||||
return UpdateInfo{}, localizedUpdateError{key: "app.update.backend.error.sha256_missing_current_package"}
|
||||
}
|
||||
@@ -566,7 +572,7 @@ func fetchReleaseByURL(apiURL string) (*githubRelease, error) {
|
||||
req.Header.Set("User-Agent", "GoNavi-Updater")
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := doUpdateRequest(client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -581,7 +587,7 @@ func fetchReleaseByURL(apiURL string) (*githubRelease, error) {
|
||||
|
||||
var release githubRelease
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return nil, err
|
||||
return nil, wrapUpdateNetworkError(err)
|
||||
}
|
||||
return &release, nil
|
||||
}
|
||||
@@ -662,6 +668,20 @@ func findReleaseAsset(assets []githubAsset, name string) (*githubAsset, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeGitHubAssetSHA256(digest string) string {
|
||||
digest = strings.TrimSpace(digest)
|
||||
if digest == "" {
|
||||
return ""
|
||||
}
|
||||
if algorithm, value, ok := strings.Cut(digest, ":"); ok {
|
||||
if !strings.EqualFold(strings.TrimSpace(algorithm), "sha256") {
|
||||
return ""
|
||||
}
|
||||
digest = strings.TrimSpace(value)
|
||||
}
|
||||
return strings.ToLower(digest)
|
||||
}
|
||||
|
||||
func fetchReleaseSHA256(assets []githubAsset) (map[string]string, error) {
|
||||
var checksumURL string
|
||||
for _, asset := range assets {
|
||||
@@ -680,7 +700,7 @@ func fetchReleaseSHA256(assets []githubAsset) (map[string]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "GoNavi-Updater")
|
||||
resp, err := client.Do(req)
|
||||
resp, err := doUpdateRequest(client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -695,7 +715,7 @@ func fetchReleaseSHA256(assets []githubAsset) (map[string]string, error) {
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, wrapUpdateNetworkError(err)
|
||||
}
|
||||
|
||||
return parseSHA256Sums(string(body)), nil
|
||||
@@ -765,7 +785,7 @@ func downloadFileWithHashWithTimeout(url, filePath string, onProgress func(downl
|
||||
req.Header.Set("Accept", "application/octet-stream")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := doUpdateRequest(client, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -810,7 +830,7 @@ func downloadFileWithHashWithTimeout(url, filePath string, onProgress func(downl
|
||||
}
|
||||
if _, err := io.Copy(io.MultiWriter(writers...), resp.Body); err != nil {
|
||||
out.Close()
|
||||
return "", err
|
||||
return "", wrapUpdateNetworkError(err)
|
||||
}
|
||||
if onProgress != nil {
|
||||
onProgress(progressWriter.written, total)
|
||||
@@ -828,6 +848,76 @@ func downloadFileWithHashWithTimeout(url, filePath string, onProgress func(downl
|
||||
return hex.EncodeToString(hasher.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func doUpdateRequest(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
if !shouldRetryUpdateNetworkError(err) {
|
||||
return nil, wrapUpdateNetworkError(err)
|
||||
}
|
||||
time.Sleep(updateNetworkRetryDelay)
|
||||
retryReq := req.Clone(req.Context())
|
||||
resp, err = client.Do(retryReq)
|
||||
if err != nil {
|
||||
return nil, wrapUpdateNetworkError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func shouldRetryUpdateNetworkError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if isUpdateEOFError(err) {
|
||||
return true
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
lower := strings.ToLower(err.Error())
|
||||
return strings.Contains(lower, "connection reset by peer") ||
|
||||
strings.Contains(lower, "connection refused") ||
|
||||
strings.Contains(lower, "server closed idle connection")
|
||||
}
|
||||
|
||||
func wrapUpdateNetworkError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var dnsErr *net.DNSError
|
||||
if errors.As(err, &dnsErr) {
|
||||
host := strings.TrimSpace(dnsErr.Name)
|
||||
if host == "" {
|
||||
host = "api.github.com"
|
||||
}
|
||||
return localizedUpdateError{
|
||||
key: "app.update.backend.error.network_dns",
|
||||
params: map[string]any{
|
||||
"host": host,
|
||||
"detail": err.Error(),
|
||||
},
|
||||
}
|
||||
}
|
||||
if isUpdateEOFError(err) {
|
||||
return localizedUpdateError{
|
||||
key: "app.update.backend.error.network_eof",
|
||||
params: map[string]any{"detail": err.Error()},
|
||||
}
|
||||
}
|
||||
return localizedUpdateError{
|
||||
key: "app.update.backend.error.network_failed",
|
||||
params: map[string]any{"detail": err.Error()},
|
||||
}
|
||||
}
|
||||
|
||||
func isUpdateEOFError(err error) bool {
|
||||
return errors.Is(err, io.EOF) ||
|
||||
errors.Is(err, io.ErrUnexpectedEOF) ||
|
||||
strings.Contains(strings.ToLower(err.Error()), "eof")
|
||||
}
|
||||
|
||||
func isGitHubReleaseAssetAPIURL(urlText string) bool {
|
||||
parsed, err := urlpkg.Parse(strings.TrimSpace(urlText))
|
||||
if err != nil {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
stdRuntime "runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
func TestFetchLatestUpdateInfoSkipsChecksumWhenCurrentVersionIsAlreadyLatest(t *testing.T) {
|
||||
@@ -64,7 +69,59 @@ func TestFetchLatestUpdateInfoSkipsChecksumWhenCurrentVersionIsAlreadyLatest(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestUpdateInfoFetchesChecksumWhenUpdateIsAvailable(t *testing.T) {
|
||||
func TestFetchLatestUpdateInfoUsesAssetDigestWhenUpdateIsAvailable(t *testing.T) {
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, "v0.6.5")
|
||||
if err != nil {
|
||||
t.Fatalf("expectedAssetName returned error: %v", err)
|
||||
}
|
||||
digest := strings.Repeat("A", 64)
|
||||
|
||||
originalVersion := AppVersion
|
||||
AppVersion = "0.6.4"
|
||||
defer func() {
|
||||
AppVersion = originalVersion
|
||||
}()
|
||||
|
||||
restoreRelease := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
|
||||
return &githubRelease{
|
||||
TagName: "v0.6.5",
|
||||
Name: "v0.6.5",
|
||||
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/v0.6.5",
|
||||
Assets: []githubAsset{
|
||||
{
|
||||
Name: assetName,
|
||||
BrowserDownloadURL: "https://example.com/" + assetName,
|
||||
Digest: "sha256:" + digest,
|
||||
Size: 4096,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restoreRelease()
|
||||
|
||||
checksumCalled := false
|
||||
restoreChecksum := swapUpdateFetchReleaseSHA256(func([]githubAsset) (map[string]string, error) {
|
||||
checksumCalled = true
|
||||
return nil, errors.New("checksum should not be fetched when asset digest is available")
|
||||
})
|
||||
defer restoreChecksum()
|
||||
|
||||
info, err := fetchLatestUpdateInfo(updateChannelLatest)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchLatestUpdateInfo returned error: %v", err)
|
||||
}
|
||||
if checksumCalled {
|
||||
t.Fatal("expected SHA256SUMS fetch to be skipped when asset digest is available")
|
||||
}
|
||||
if !info.HasUpdate {
|
||||
t.Fatalf("expected HasUpdate=true, got %#v", info)
|
||||
}
|
||||
if info.SHA256 != strings.ToLower(digest) || info.AssetName != assetName {
|
||||
t.Fatalf("unexpected update info: %#v", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestUpdateInfoFallsBackToChecksumFileWhenAssetDigestMissing(t *testing.T) {
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, "v0.6.5")
|
||||
if err != nil {
|
||||
t.Fatalf("expectedAssetName returned error: %v", err)
|
||||
@@ -106,7 +163,7 @@ func TestFetchLatestUpdateInfoFetchesChecksumWhenUpdateIsAvailable(t *testing.T)
|
||||
t.Fatalf("fetchLatestUpdateInfo returned error: %v", err)
|
||||
}
|
||||
if !checksumCalled {
|
||||
t.Fatal("expected SHA256SUMS fetch when update is available")
|
||||
t.Fatal("expected SHA256SUMS fetch when asset digest is missing")
|
||||
}
|
||||
if !info.HasUpdate {
|
||||
t.Fatalf("expected HasUpdate=true, got %#v", info)
|
||||
@@ -117,7 +174,7 @@ func TestFetchLatestUpdateInfoFetchesChecksumWhenUpdateIsAvailable(t *testing.T)
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesLogsFailuresForManualChecks(t *testing.T) {
|
||||
app := &App{}
|
||||
app := &App{configDir: t.TempDir()}
|
||||
|
||||
restoreRelease := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
|
||||
return nil, errors.New("request timed out")
|
||||
@@ -140,7 +197,7 @@ func TestCheckForUpdatesLogsFailuresForManualChecks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesSilentlySkipsFailureLogs(t *testing.T) {
|
||||
app := &App{}
|
||||
app := &App{configDir: t.TempDir()}
|
||||
|
||||
restoreRelease := swapUpdateFetchLatestRelease(func() (*githubRelease, error) {
|
||||
return nil, errors.New("request timed out")
|
||||
@@ -162,6 +219,73 @@ func TestCheckForUpdatesSilentlySkipsFailureLogs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesRestoresPersistedGlobalProxyRuntime(t *testing.T) {
|
||||
previousProxy := currentGlobalProxyConfig()
|
||||
t.Cleanup(func() {
|
||||
_, _ = setGlobalProxyConfig(previousProxy.Enabled, previousProxy.Proxy)
|
||||
})
|
||||
|
||||
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 update request through HTTP proxy to use absolute URL, got %q", r.URL.String())
|
||||
}
|
||||
if r.URL.Host != "api.github.invalid" {
|
||||
t.Fatalf("expected proxied GitHub API host api.github.invalid, got %q", r.URL.Host)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(githubRelease{
|
||||
TagName: updateDevReleaseTag,
|
||||
Name: "Dev Build (dev-proxy123)",
|
||||
HTMLURL: "https://github.com/Syngnat/GoNavi/releases/tag/dev-latest",
|
||||
}); err != nil {
|
||||
t.Fatalf("Encode returned error: %v", err)
|
||||
}
|
||||
}))
|
||||
defer proxyServer.Close()
|
||||
|
||||
host, port := parseTestServerHostPort(t, proxyServer.URL)
|
||||
if _, err := app.saveGlobalProxy(connection.SaveGlobalProxyInput{
|
||||
Enabled: true,
|
||||
Type: "http",
|
||||
Host: host,
|
||||
Port: port,
|
||||
}); 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)
|
||||
}
|
||||
|
||||
originalVersion := AppVersion
|
||||
AppVersion = "dev-proxy123"
|
||||
defer func() {
|
||||
AppVersion = originalVersion
|
||||
}()
|
||||
|
||||
restoreRelease := swapUpdateFetchDevRelease(func() (*githubRelease, error) {
|
||||
return fetchReleaseByURL("http://api.github.invalid/repos/Syngnat/GoNavi/releases/tags/dev-latest")
|
||||
})
|
||||
defer restoreRelease()
|
||||
|
||||
setChannelResult := app.SetUpdateChannel(string(updateChannelDev))
|
||||
if !setChannelResult.Success {
|
||||
t.Fatalf("SetUpdateChannel returned failure: %#v", setChannelResult)
|
||||
}
|
||||
|
||||
result := app.CheckForUpdates()
|
||||
if !result.Success {
|
||||
t.Fatalf("expected update check through restored proxy to succeed, got %#v", result)
|
||||
}
|
||||
if !proxyCalled {
|
||||
t.Fatal("expected persisted global proxy to receive the update check request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchLatestUpdateInfoForDevChannelUsesReleaseBuildVersion(t *testing.T) {
|
||||
assetName, err := expectedAssetName(stdRuntime.GOOS, stdRuntime.GOARCH, "dev-a1b2c3d")
|
||||
if err != nil {
|
||||
|
||||
@@ -42,12 +42,28 @@ type SavedConnectionView struct {
|
||||
type LegacySavedConnection = SavedConnectionInput
|
||||
|
||||
type SaveGlobalProxyInput struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
ClearPassword bool `json:"clearPassword,omitempty"`
|
||||
}
|
||||
|
||||
type TestGlobalProxyInput struct {
|
||||
Proxy SaveGlobalProxyInput `json:"proxy"`
|
||||
URL string `json:"url"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
}
|
||||
|
||||
type GlobalProxyTestResult struct {
|
||||
URL string `json:"url"`
|
||||
FinalURL string `json:"finalUrl,omitempty"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
DurationMs int64 `json:"durationMs"`
|
||||
ViaProxy bool `json:"viaProxy"`
|
||||
}
|
||||
|
||||
type GlobalProxyView struct {
|
||||
|
||||
Reference in New Issue
Block a user