返回 DeepSeek-Reasonix
identity_windows_test.go
根目录 / internal / appidentity / identity_windows_test.go
1 //go:build windows
2
3 package appidentity
4
5 import (
6 "os"
7 "os/exec"
8 "path/filepath"
9 "runtime"
10 "syscall"
11 "testing"
12 "unsafe"
13
14 "golang.org/x/sys/windows"
15 )
16
17 func TestOwnedShortcutTargetCoversStableAndVersionedEntries(t *testing.T) {
18 root := t.TempDir()
19 tests := []struct {
20 target string
21 want bool
22 }{
23 {filepath.Join(root, "reasonix-launcher.exe"), true},
24 {filepath.Join(root, "Reasonix.exe"), true},
25 {filepath.Join(root, "reasonix-desktop.exe"), true},
26 {filepath.Join(root, "versions", "v1.20.0", "reasonix-desktop.exe"), true},
27 {filepath.Join(root, "versions", "v1.20.0", "reasonix-cli.exe"), false},
28 {filepath.Join(`D:\Apps`, "Reasonix", "reasonix-launcher.exe"), false},
29 }
30 for _, test := range tests {
31 if got := ownedShortcutTarget(test.target, root); got != test.want {
32 t.Errorf("ownedShortcutTarget(%q, %q) = %v, want %v", test.target, root, got, test.want)
33 }
34 }
35 }
36
37 func TestReasonixShortcutName(t *testing.T) {
38 tests := []struct {
39 name string
40 want bool
41 }{
42 {"Reasonix.lnk", true},
43 {"reasonix launcher.LNK", true},
44 {"Reasonix (2).lnk", true},
45 {"Other.lnk", false},
46 {"Reasonix.exe", false},
47 }
48 for _, test := range tests {
49 if got := reasonixShortcutName(test.name); got != test.want {
50 t.Errorf("reasonixShortcutName(%q) = %v, want %v", test.name, got, test.want)
51 }
52 }
53 }
54
55 func TestOwnedShortcutTargetAcceptsVersionedDesktopThroughJunction(t *testing.T) {
56 root := t.TempDir()
57 versionDir := filepath.Join(root, "versions", "v1.20.0")
58 if err := os.MkdirAll(versionDir, 0o755); err != nil {
59 t.Fatal(err)
60 }
61 if err := os.WriteFile(filepath.Join(versionDir, "reasonix-desktop.exe"), []byte("desktop"), 0o600); err != nil {
62 t.Fatal(err)
63 }
64 junction := filepath.Join(t.TempDir(), "current")
65 if output, err := exec.Command("cmd", "/c", "mklink", "/J", junction, root).CombinedOutput(); err != nil {
66 t.Fatalf("create directory junction: %v: %s", err, output)
67 }
68 target := filepath.Join(junction, "versions", "v1.20.0", "reasonix-desktop.exe")
69 if !ownedShortcutTarget(target, root) {
70 resolvedRoot, rootErr := existingShortcutPath(root)
71 resolvedTarget, targetErr := resolveShortcutTarget(target)
72 linkTarget, linkErr := os.Readlink(junction)
73 _, statErr := os.Stat(target)
74 t.Logf("junction value=%q (%v), target stat=%v", linkTarget, linkErr, statErr)
75 t.Fatalf("junction target %q was not recognised under %q; resolved root=%q (%v), target=%q (%v)", target, root, resolvedRoot, rootErr, resolvedTarget, targetErr)
76 }
77 }
78
79 func TestRepairOwnedShortcutPersistsAppUserModelID(t *testing.T) {
80 root := t.TempDir()
81 target := filepath.Join(root, "Reasonix.exe")
82 if err := os.WriteFile(target, []byte("launcher"), 0o600); err != nil {
83 t.Fatal(err)
84 }
85 shortcutPath := filepath.Join(root, "Reasonix.lnk")
86
87 runtime.LockOSThread()
88 defer runtime.UnlockOSThread()
89 uninitialize, err := initializeCOM()
90 if err != nil {
91 t.Fatal(err)
92 }
93 defer uninitialize()
94 createTestShortcut(t, shortcutPath, target)
95
96 changed, err := repairOwnedShortcut(shortcutPath, root)
97 if err != nil {
98 t.Fatal(err)
99 }
100 if !changed {
101 t.Fatal("first repair did not report a change")
102 }
103 shortcut, err := loadShortcut(shortcutPath, stgmReadWrite)
104 if err != nil {
105 t.Fatal(err)
106 }
107 got, err := shortcut.appUserModelID()
108 shortcut.release()
109 if err != nil {
110 t.Fatal(err)
111 }
112 if got != AppUserModelID {
113 t.Fatalf("shortcut AppUserModelID = %q, want %q", got, AppUserModelID)
114 }
115 changed, err = repairOwnedShortcut(shortcutPath, root)
116 if err != nil {
117 t.Fatal(err)
118 }
119 if changed {
120 t.Fatal("second repair rewrote an already healthy shortcut")
121 }
122 }
123
124 func TestRepairOwnedShortcutLeavesSeparateReasonix053InstallUntouched(t *testing.T) {
125 currentRoot := t.TempDir()
126 legacyRoot := t.TempDir()
127 legacyTarget := filepath.Join(legacyRoot, "reasonix-desktop.exe")
128 if err := os.WriteFile(legacyTarget, []byte("legacy tauri desktop"), 0o600); err != nil {
129 t.Fatal(err)
130 }
131 shortcutPath := filepath.Join(t.TempDir(), "Reasonix.lnk")
132
133 runtime.LockOSThread()
134 defer runtime.UnlockOSThread()
135 uninitialize, err := initializeCOM()
136 if err != nil {
137 t.Fatal(err)
138 }
139 defer uninitialize()
140 createTestShortcut(t, shortcutPath, legacyTarget)
141
142 shortcut, err := loadShortcut(shortcutPath, stgmReadWrite)
143 if err != nil {
144 t.Fatal(err)
145 }
146 if err := shortcut.setAppUserModelID(legacyTauriAppUserModelID); err != nil {
147 shortcut.release()
148 t.Fatal(err)
149 }
150 shortcut.release()
151
152 changed, err := repairOwnedShortcut(shortcutPath, currentRoot)
153 if err != nil {
154 t.Fatal(err)
155 }
156 if changed {
157 t.Fatal("current install rewrote a shortcut owned by a separate Reasonix 0.53 installation")
158 }
159
160 shortcut, err = loadShortcut(shortcutPath, stgmReadWrite)
161 if err != nil {
162 t.Fatal(err)
163 }
164 gotTarget, targetErr := shortcut.targetPath()
165 gotID, idErr := shortcut.appUserModelID()
166 shortcut.release()
167 if targetErr != nil {
168 t.Fatal(targetErr)
169 }
170 if idErr != nil {
171 t.Fatal(idErr)
172 }
173 if !migrationSamePath(gotTarget, legacyTarget) {
174 t.Fatalf("legacy shortcut target = %q, want %q", gotTarget, legacyTarget)
175 }
176 if gotID != legacyTauriAppUserModelID {
177 t.Fatalf("legacy shortcut AppUserModelID = %q, want %q", gotID, legacyTauriAppUserModelID)
178 }
179 }
180
181 func createTestShortcut(t *testing.T, shortcutPath, target string) {
182 t.Helper()
183 var link *shellLinkW
184 hr, _, _ := procCoCreateInstance.Call(
185 uintptr(unsafe.Pointer(&clsidShellLink)),
186 0,
187 clsctxInprocServer,
188 uintptr(unsafe.Pointer(&iidIShellLinkW)),
189 uintptr(unsafe.Pointer(&link)),
190 )
191 if err := checkHRESULT("CoCreateInstance(CLSID_ShellLink)", hr); err != nil {
192 t.Fatal(err)
193 }
194 defer releaseInterface(unsafe.Pointer(link))
195 targetPtr, err := windows.UTF16PtrFromString(target)
196 if err != nil {
197 t.Fatal(err)
198 }
199 hr, _, _ = syscall.SyscallN(
200 link.VTable.SetPath,
201 uintptr(unsafe.Pointer(link)),
202 uintptr(unsafe.Pointer(targetPtr)),
203 )
204 if err := checkHRESULT("IShellLinkW.SetPath", hr); err != nil {
205 t.Fatal(err)
206 }
207 var persist *persistFile
208 if err := queryInterface(unsafe.Pointer(link), &iidIPersistFile, unsafe.Pointer(&persist)); err != nil {
209 t.Fatal(err)
210 }
211 defer releaseInterface(unsafe.Pointer(persist))
212 shortcutPtr, err := windows.UTF16PtrFromString(shortcutPath)
213 if err != nil {
214 t.Fatal(err)
215 }
216 hr, _, _ = syscall.SyscallN(
217 persist.VTable.Save,
218 uintptr(unsafe.Pointer(persist)),
219 uintptr(unsafe.Pointer(shortcutPtr)),
220 1,
221 )
222 if err := checkHRESULT("IPersistFile.Save", hr); err != nil {
223 t.Fatal(err)
224 }
225 }
226
226 lines GO