返回 DeepSeek-Reasonix
updater_windows_test.go
根目录 / desktop / updater_windows_test.go
1 //go:build windows
2
3 package main
4
5 import (
6 "crypto/sha256"
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14
15 "golang.org/x/sys/windows"
16
17 "reasonix/internal/repair"
18 )
19
20 func TestInstallerCommandShowsUpdateProgressAndPassesUnquotedDFlagLast(t *testing.T) {
21 cmd := installerCommand(`C:\Temp\reasonix-update-1.exe`, `D:\Tools\Reasonix App`)
22 if cmd.SysProcAttr == nil {
23 t.Fatal("expected a raw command line forcing the install dir")
24 }
25 got := cmd.SysProcAttr.CmdLine
26 want := `"C:\Temp\reasonix-update-1.exe" /REASONIXUPDATE=1 /REASONIXSTAGE=1 /D=D:\Tools\Reasonix App`
27 if got != want {
28 t.Fatalf("CmdLine = %q, want %q", got, want)
29 }
30 if cmd.SysProcAttr.HideWindow {
31 t.Fatal("NSIS update progress window must remain visible")
32 }
33 }
34
35 func TestInstallerCommandWithoutDirSkipsDFlag(t *testing.T) {
36 cmd := installerCommand(`C:\Temp\reasonix-update-1.exe`, "")
37 if cmd.SysProcAttr == nil {
38 t.Fatal("expected a raw command line for visible updater installs")
39 }
40 got := cmd.SysProcAttr.CmdLine
41 want := `"C:\Temp\reasonix-update-1.exe" /REASONIXUPDATE=1 /REASONIXSTAGE=1`
42 if got != want {
43 t.Fatalf("CmdLine = %q, want %q", got, want)
44 }
45 }
46
47 func TestWindowsHelperStartRetriesTransientSecurityScan(t *testing.T) {
48 originalBackoff := windowsHelperStartBackoff
49 windowsHelperStartBackoff = func(int) time.Duration { return 0 }
50 t.Cleanup(func() { windowsHelperStartBackoff = originalBackoff })
51
52 calls := 0
53 err := retryWindowsUpdateHelperStart(func() error {
54 calls++
55 if calls < 3 {
56 return &os.PathError{Op: "fork/exec", Path: `C:\private\helper.exe`, Err: windows.ERROR_SHARING_VIOLATION}
57 }
58 return nil
59 })
60 if err != nil {
61 t.Fatalf("retryWindowsUpdateHelperStart: %v", err)
62 }
63 if calls != 3 {
64 t.Fatalf("start calls = %d, want 3", calls)
65 }
66 }
67
68 func TestWindowsHelperStartErrorIsActionableAndPathSafe(t *testing.T) {
69 raw := &os.PathError{Op: "fork/exec", Path: `C:\Users\Private\helper.exe`, Err: windows.ERROR_ACCESS_DENIED}
70 err := windowsUpdateHelperStartError(raw)
71 if !strings.Contains(err.Error(), "security software") {
72 t.Fatalf("error is not actionable: %v", err)
73 }
74 if strings.Contains(err.Error(), `C:\Users`) {
75 t.Fatalf("error leaks local path: %v", err)
76 }
77 if !errors.Is(raw, windows.ERROR_ACCESS_DENIED) {
78 t.Fatal("test setup does not expose the wrapped Windows error")
79 }
80 }
81
82 func TestWindowsPEMachineMatchesSupportedArchitectures(t *testing.T) {
83 for _, arch := range []string{"amd64", "arm64", "386"} {
84 if machine, ok := windowsPEMachine(arch); !ok || machine == 0 {
85 t.Fatalf("windowsPEMachine(%q) = (0x%x, %v)", arch, machine, ok)
86 }
87 }
88 if _, ok := windowsPEMachine("mips"); ok {
89 t.Fatal("unsupported architecture was accepted")
90 }
91 if err := validateWindowsUpdateHelper([]byte("not a PE image"), "amd64"); err == nil {
92 t.Fatal("malformed helper image was accepted")
93 }
94 }
95
96 func TestClaimVerifiedWindowsUpdateHelperExecutionFreezesPath(t *testing.T) {
97 path := filepath.Join(t.TempDir(), "reasonix-update-helper.exe")
98 content := []byte("verified-helper")
99 if err := os.WriteFile(path, content, 0o700); err != nil {
100 t.Fatal(err)
101 }
102 expected := sha256.Sum256(content)
103 release, err := claimVerifiedWindowsUpdateHelperExecution(path, expected)
104 if err != nil {
105 t.Fatal(err)
106 }
107 if err := os.WriteFile(path, []byte("tampered"), 0o700); err == nil {
108 release()
109 t.Fatal("copied helper remained writable while execution claim was held")
110 }
111 if err := os.Rename(path, path+".replaced"); err == nil {
112 release()
113 t.Fatal("copied helper remained renameable while execution claim was held")
114 }
115 release()
116 release()
117 if err := os.WriteFile(path, []byte("replacement"), 0o700); err != nil {
118 t.Fatalf("copied helper remained frozen after claim release: %v", err)
119 }
120 }
121
122 func TestClaimVerifiedWindowsUpdateHelperExecutionRejectsHashDrift(t *testing.T) {
123 path := filepath.Join(t.TempDir(), "reasonix-update-helper.exe")
124 if err := os.WriteFile(path, []byte("tampered"), 0o700); err != nil {
125 t.Fatal(err)
126 }
127 if release, err := claimVerifiedWindowsUpdateHelperExecution(path, sha256.Sum256([]byte("expected"))); err == nil {
128 release()
129 t.Fatal("copied helper with the wrong SHA-256 received an execution claim")
130 }
131 }
132
133 func TestStageWindowsUpdateHelperCopyAllocatesExclusiveNodes(t *testing.T) {
134 dir := t.TempDir()
135 content := []byte("verified-helper")
136 first, err := stageWindowsUpdateHelperCopy(dir, content)
137 if err != nil {
138 t.Fatal(err)
139 }
140 second, err := stageWindowsUpdateHelperCopy(dir, content)
141 if err != nil {
142 t.Fatal(err)
143 }
144 if first == second {
145 t.Fatalf("exclusive helper copies reused path %q", first)
146 }
147 for _, path := range []string{first, second} {
148 got, err := os.ReadFile(path)
149 if err != nil || string(got) != string(content) {
150 t.Fatalf("staged helper %q = %q, %v", path, got, err)
151 }
152 }
153 }
154
155 func TestPreparedWindowsUpdateHelperSHA256BindsReleaseUnitMember(t *testing.T) {
156 installDir := t.TempDir()
157 helper := filepath.Join(installDir, windowsUpdateHelperFileName)
158 expected := strings.Repeat("a", sha256.Size*2)
159 prepared := &repair.UpdateTransaction{
160 SchemaVersion: 1,
161 TargetKind: "file",
162 TargetPath: filepath.Join(installDir, "reasonix-desktop.exe"),
163 ToVersion: "v2",
164 Platform: "windows/amd64",
165 CreatedAt: "2026-07-29T00:00:00Z",
166 Files: []repair.UpdateTransactionFile{{
167 TargetPath: helper,
168 SHA256: expected,
169 }},
170 }
171 got, err := preparedWindowsUpdateHelperSHA256(prepared, installDir)
172 if err != nil || got != expected {
173 t.Fatalf("prepared helper SHA-256 = %q, %v", got, err)
174 }
175 prepared.Files[0].MissingBefore = true
176 if _, err := preparedWindowsUpdateHelperSHA256(prepared, installDir); err == nil {
177 t.Fatal("prepared transaction with a missing helper authorized execution")
178 }
179 }
180
181 func TestPrepareWindowsUpdateHelperRejectsPreparedHashDrift(t *testing.T) {
182 installDir := t.TempDir()
183 if err := os.WriteFile(
184 filepath.Join(installDir, windowsUpdateHelperFileName),
185 []byte("changed-helper"),
186 0o700,
187 ); err != nil {
188 t.Fatal(err)
189 }
190 prepared := sha256.Sum256([]byte("prepared-helper"))
191 _, _, err := prepareWindowsUpdateHelper(installDir, fmt.Sprintf("%x", prepared))
192 if err == nil || !strings.Contains(err.Error(), "changed after transaction prepare") {
193 t.Fatalf("changed packaged helper = %v", err)
194 }
195 }
196
196 lines GO