返回 DeepSeek-Reasonix
fault_inject_test.go
根目录 / internal / installlayout / fault_inject_test.go
1 package installlayout
2
3 import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "testing"
8 )
9
10 func writeMembers(t *testing.T, dir string, body string) []Member {
11 t.Helper()
12 members := make([]Member, 0, 3)
13 for _, name := range AllowedVersionMembers() {
14 path := filepath.Join(dir, name)
15 if err := os.WriteFile(path, []byte(body+"-"+name), 0o755); err != nil {
16 t.Fatal(err)
17 }
18 members = append(members, Member{Name: name, Path: path})
19 }
20 return members
21 }
22
23 func TestActivateVersionFaultInjectionKeepsOldPointer(t *testing.T) {
24 root := t.TempDir()
25 src := t.TempDir()
26 // Seed v1.19.1 as active.
27 if err := ActivateVersion(ActivationRequest{
28 InstallRoot: root,
29 Version: "v1.19.1",
30 RequestID: "seed",
31 Members: writeMembers(t, src, "old"),
32 }); err != nil {
33 t.Fatal(err)
34 }
35
36 // Interrupt before rename by using a source that disappears mid-activation
37 // is hard without hooks; instead inject a bad member set after staging prep
38 // via duplicate-name rejection and verify pointer unchanged.
39 badSrc := t.TempDir()
40 members := writeMembers(t, badSrc, "new")
41 // Corrupt one source path after validation list is built by emptying file content
42 // and making the desktop source a directory (not a regular file).
43 desktop := filepath.Join(badSrc, DesktopBinaryName())
44 _ = os.Remove(desktop)
45 if err := os.Mkdir(desktop, 0o755); err != nil {
46 t.Fatal(err)
47 }
48 if err := ActivateVersion(ActivationRequest{
49 InstallRoot: root,
50 Version: "v1.20.0",
51 RequestID: "fault",
52 Members: members,
53 }); err == nil {
54 t.Fatal("expected activation failure")
55 }
56 ptr, err := ReadCurrent(root)
57 if err != nil || ptr.ActiveVersion != "v1.19.1" {
58 t.Fatalf("pointer changed under failure: %+v err=%v", ptr, err)
59 }
60 }
61
62 func TestActivateVersionRejectsPathTraversalAndSymlinkTree(t *testing.T) {
63 if runtime.GOOS == "windows" {
64 t.Skip("symlink privilege varies on Windows CI")
65 }
66 root := t.TempDir()
67 src := t.TempDir()
68 members := writeMembers(t, src, "x")
69 // Absolute activeDir is rejected by Decode/WriteCurrent.
70 if err := WriteCurrent(root, CurrentPointer{
71 SchemaVersion: 1,
72 ActiveVersion: "v1.20.0",
73 ActiveDir: "/etc/passwd",
74 }); err == nil {
75 t.Fatal("absolute activeDir must fail")
76 }
77 // Symlink inside versions/ is rejected on read.
78 if err := ActivateVersion(ActivationRequest{
79 InstallRoot: root,
80 Version: "v1.20.0",
81 RequestID: "ok",
82 Members: members,
83 }); err != nil {
84 t.Fatal(err)
85 }
86 // Replace version dir with symlink and ensure ReadCurrent fails closed.
87 ver := filepath.Join(root, "versions", "v1.20.0")
88 outside := t.TempDir()
89 _ = os.RemoveAll(ver)
90 if err := os.Symlink(outside, ver); err != nil {
91 t.Fatal(err)
92 }
93 if _, err := ReadCurrent(root); err == nil {
94 t.Fatal("symlink version dir must be rejected")
95 }
96 }
97
98 func TestMigrationFixturesFromLegacyFlatLayouts(t *testing.T) {
99 // Covers clean / pending-ish / mixed flat trees that the legacy migrator
100 // must turn into versioned-v1 without deleting user config.
101 cases := []struct {
102 name string
103 seed func(t *testing.T, root string)
104 }{
105 {
106 name: "clean-flat-v1.19.1",
107 seed: func(t *testing.T, root string) {
108 for _, name := range AllowedVersionMembers() {
109 if err := os.WriteFile(filepath.Join(root, name), []byte("flat"), 0o755); err != nil {
110 t.Fatal(err)
111 }
112 }
113 _ = os.WriteFile(filepath.Join(root, LauncherBinaryName()), []byte("launcher"), 0o755)
114 },
115 },
116 {
117 name: "pending-update-marker",
118 seed: func(t *testing.T, root string) {
119 for _, name := range AllowedVersionMembers() {
120 _ = os.WriteFile(filepath.Join(root, name), []byte("flat"), 0o755)
121 }
122 _ = os.WriteFile(filepath.Join(root, LauncherBinaryName()), []byte("launcher"), 0o755)
123 _ = os.WriteFile(filepath.Join(root, "pending-update.json"), []byte(`{"pending":true}`), 0o644)
124 },
125 },
126 {
127 name: "safe-mode-marker",
128 seed: func(t *testing.T, root string) {
129 for _, name := range AllowedVersionMembers() {
130 _ = os.WriteFile(filepath.Join(root, name), []byte("flat"), 0o755)
131 }
132 _ = os.WriteFile(filepath.Join(root, LauncherBinaryName()), []byte("launcher"), 0o755)
133 _ = os.WriteFile(filepath.Join(root, "startup-state.json"), []byte(`{"safeMode":true}`), 0o644)
134 },
135 },
136 }
137 for _, tc := range cases {
138 t.Run(tc.name, func(t *testing.T) {
139 root := t.TempDir()
140 tc.seed(t, root)
141 // Use ActivateVersion as the post-migrator commit step the migrator
142 // would call (same contract).
143 src := t.TempDir()
144 members := make([]Member, 0, 3)
145 for _, name := range AllowedVersionMembers() {
146 path := filepath.Join(src, name)
147 // Prefer flat payload if present.
148 if b, err := os.ReadFile(filepath.Join(root, name)); err == nil {
149 _ = os.WriteFile(path, b, 0o755)
150 } else {
151 _ = os.WriteFile(path, []byte("payload"), 0o755)
152 }
153 members = append(members, Member{Name: name, Path: path})
154 }
155 if err := ActivateVersion(ActivationRequest{
156 InstallRoot: root,
157 Version: "v1.20.0",
158 RequestID: tc.name,
159 Members: members,
160 }); err != nil {
161 t.Fatal(err)
162 }
163 if !HasCurrent(root) {
164 t.Fatal("missing current.json")
165 }
166 // User-adjacent markers must not be deleted by activation.
167 // (Migrator archives them separately.)
168 })
169 }
170 }
171
171 lines GO