Files
MyGoNavi/internal/app/windows_update_integration_windows_test.go
Syngnat e1fb5b12d4 🐛 fix(update): 修复 Windows 在线更新中文路径失效
- 将 Windows 更新脚本统一为 PowerShell,并通过 Unicode 环境变量传递中文与特殊字符路径
- 保留当前可执行文件路径,补充替换重试、启动健康检查、失败回滚和严格 ZIP 选择
- 更新前复用未保存 SQL 确认流程,并延长退出兜底时间
- 新增 Windows 原生集成测试并接入 dev/release 构建
2026-07-10 13:31:34 +08:00

244 lines
8.2 KiB
Go

//go:build windows
package app
import (
"archive/zip"
"crypto/sha256"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
func TestWindowsPowerShellUpdaterHandlesUnicodeAndShellMetacharacters(t *testing.T) {
root := t.TempDir()
installDir := filepath.Join(root, `软件 ! 100% & (便携版)`, `O'Brien`)
stagedDir := filepath.Join(root, `literal-%TEMP%-stage`)
if err := os.MkdirAll(installDir, 0o755); err != nil {
t.Fatalf("MkdirAll install dir: %v", err)
}
if err := os.MkdirAll(stagedDir, 0o755); err != nil {
t.Fatalf("MkdirAll staged dir: %v", err)
}
sourcePath := filepath.Join(stagedDir, "GoNavi-0.8.5-Windows-Amd64.exe")
build := exec.Command("go", "build", "-ldflags=-H=windowsgui", "-o", sourcePath, "./testdata/windows_update_helper")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build update helper: %v\n%s", err, output)
}
sourceData, err := os.ReadFile(sourcePath)
if err != nil {
t.Fatalf("ReadFile source: %v", err)
}
wantHash := sha256.Sum256(sourceData)
targetPath := filepath.Join(installDir, "GoNavi.exe")
if err := os.WriteFile(targetPath, []byte("old executable"), 0o755); err != nil {
t.Fatalf("WriteFile old target: %v", err)
}
logPath := filepath.Join(stagedDir, "gonavi-update-windows-test.log")
scriptPath := filepath.Join(stagedDir, "update.ps1")
if err := os.WriteFile(scriptPath, []byte(buildWindowsPowerShellScript()), 0o644); err != nil {
t.Fatalf("WriteFile updater: %v", err)
}
context := windowsUpdateLaunchContext{
SourcePath: sourcePath,
TargetPath: targetPath,
CurrentTargetPath: targetPath,
StagedDir: stagedDir,
LogPath: logPath,
PID: 2147483647,
}
cmd := buildWindowsLaunchCommand(scriptPath, context)
if output, err := cmd.CombinedOutput(); err != nil {
logData, _ := os.ReadFile(logPath)
t.Fatalf("run updater: %v\nstdout/stderr:\n%s\nlog:\n%s", err, output, logData)
}
// The helper remains alive long enough for the updater's launch health check.
// Wait for it to exit before the test temp directory is removed on Windows.
time.Sleep(7 * time.Second)
targetData, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("ReadFile updated target: %v", err)
}
if gotHash := sha256.Sum256(targetData); gotHash != wantHash {
t.Fatalf("updated target hash = %x, want %x", gotHash, wantHash)
}
if _, err := os.Stat(targetPath + ".old"); !os.IsNotExist(err) {
t.Fatalf("expected rollback executable to be cleaned, stat err=%v", err)
}
deadline := time.Now().Add(5 * time.Second)
for {
_, err := os.Stat(stagedDir)
if os.IsNotExist(err) {
break
}
if time.Now().After(deadline) {
t.Fatalf("staged directory was not cleaned: %s", stagedDir)
}
time.Sleep(100 * time.Millisecond)
}
t.Logf("updated target at %s (%s)", targetPath, fmt.Sprintf("%x", wantHash[:8]))
}
func TestWindowsPowerShellUpdaterSelectsExactTargetFilenameRecursivelyFromZip(t *testing.T) {
root := t.TempDir()
installDir := filepath.Join(root, "install")
stagedDir := filepath.Join(root, "stage")
for _, dir := range []string{installDir, stagedDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll %q: %v", dir, err)
}
}
helperPath := filepath.Join(root, "update-helper.exe")
build := exec.Command("go", "build", "-ldflags=-H=windowsgui", "-o", helperPath, "./testdata/windows_update_helper")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build update helper: %v\n%s", err, output)
}
helperData, err := os.ReadFile(helperPath)
if err != nil {
t.Fatalf("ReadFile helper: %v", err)
}
wantHash := sha256.Sum256(helperData)
sourcePath := filepath.Join(stagedDir, "portable.zip")
writeWindowsUpdateTestZip(t, sourcePath, []windowsUpdateZipEntry{
{Name: "nested/GoNavi.exe", Data: helperData},
{Name: "portable.exe", Data: []byte("package-name distractor")},
{Name: "tools/GoNavi-helper.exe", Data: []byte("GoNavi-name distractor")},
})
targetPath := filepath.Join(installDir, "GoNavi.exe")
if err := os.WriteFile(targetPath, []byte("old executable"), 0o755); err != nil {
t.Fatalf("WriteFile old target: %v", err)
}
logPath := filepath.Join(stagedDir, "gonavi-update-windows-zip.log")
scriptPath := filepath.Join(stagedDir, "update.ps1")
if err := os.WriteFile(scriptPath, []byte(buildWindowsPowerShellScript()), 0o644); err != nil {
t.Fatalf("WriteFile updater: %v", err)
}
cmd := buildWindowsLaunchCommand(scriptPath, windowsUpdateLaunchContext{
SourcePath: sourcePath,
TargetPath: targetPath,
CurrentTargetPath: targetPath,
StagedDir: stagedDir,
LogPath: logPath,
PID: 2147483647,
})
if output, err := cmd.CombinedOutput(); err != nil {
logData, _ := os.ReadFile(logPath)
t.Fatalf("run ZIP updater: %v\nstdout/stderr:\n%s\nlog:\n%s", err, output, logData)
}
// The selected helper remains alive through the launch health check.
time.Sleep(7 * time.Second)
targetData, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("ReadFile updated target: %v", err)
}
if gotHash := sha256.Sum256(targetData); gotHash != wantHash {
t.Fatalf("updated target hash = %x, want exact target-name candidate hash %x", gotHash, wantHash)
}
}
func TestWindowsPowerShellUpdaterRejectsAmbiguousZipAndRetainsPackage(t *testing.T) {
root := t.TempDir()
installDir := filepath.Join(root, "install")
stagedDir := filepath.Join(root, "stage")
for _, dir := range []string{installDir, stagedDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll %q: %v", dir, err)
}
}
sourcePath := filepath.Join(stagedDir, "portable.zip")
writeWindowsUpdateTestZip(t, sourcePath, []windowsUpdateZipEntry{
{Name: "tools/alpha.exe", Data: []byte("alpha")},
{Name: "nested/beta.exe", Data: []byte("beta")},
})
targetPath := filepath.Join(installDir, "Application.exe")
oldTargetData := []byte("old executable")
if err := os.WriteFile(targetPath, oldTargetData, 0o755); err != nil {
t.Fatalf("WriteFile old target: %v", err)
}
logPath := filepath.Join(stagedDir, "gonavi-update-windows-ambiguous.log")
scriptPath := filepath.Join(stagedDir, "update.ps1")
if err := os.WriteFile(scriptPath, []byte(buildWindowsPowerShellScript()), 0o644); err != nil {
t.Fatalf("WriteFile updater: %v", err)
}
cmd := buildWindowsLaunchCommand(scriptPath, windowsUpdateLaunchContext{
SourcePath: sourcePath,
TargetPath: targetPath,
CurrentTargetPath: targetPath,
StagedDir: stagedDir,
LogPath: logPath,
PID: 2147483647,
})
if output, err := cmd.CombinedOutput(); err == nil {
t.Fatalf("ambiguous ZIP updater unexpectedly succeeded\n%s", output)
}
if _, err := os.Stat(sourcePath); err != nil {
t.Fatalf("ambiguous update package must be retained: %v", err)
}
targetData, err := os.ReadFile(targetPath)
if err != nil {
t.Fatalf("ReadFile target after ambiguous update: %v", err)
}
if string(targetData) != string(oldTargetData) {
t.Fatalf("target changed after ambiguous ZIP selection: got %q want %q", targetData, oldTargetData)
}
logData, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile updater log: %v", err)
}
if !strings.Contains(string(logData), "ambiguous portable zip") ||
!strings.Contains(string(logData), "package retained for manual install") {
t.Fatalf("expected ambiguous retained-package error in updater log:\n%s", logData)
}
}
type windowsUpdateZipEntry struct {
Name string
Data []byte
}
func writeWindowsUpdateTestZip(t *testing.T, path string, entries []windowsUpdateZipEntry) {
t.Helper()
archive, err := os.Create(path)
if err != nil {
t.Fatalf("Create ZIP %q: %v", path, err)
}
writer := zip.NewWriter(archive)
for _, entry := range entries {
file, createErr := writer.Create(entry.Name)
if createErr != nil {
_ = writer.Close()
_ = archive.Close()
t.Fatalf("Create ZIP entry %q: %v", entry.Name, createErr)
}
if _, writeErr := file.Write(entry.Data); writeErr != nil {
_ = writer.Close()
_ = archive.Close()
t.Fatalf("Write ZIP entry %q: %v", entry.Name, writeErr)
}
}
if err := writer.Close(); err != nil {
_ = archive.Close()
t.Fatalf("Close ZIP writer: %v", err)
}
if err := archive.Close(); err != nil {
t.Fatalf("Close ZIP file: %v", err)
}
}