返回 DeepSeek-Reasonix
attachments_test.go
根目录 / internal / control / attachments_test.go
1 package control
2
3 import (
4 "encoding/base64"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 "time"
10 )
11
12 const tinyPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
13
14 func TestSaveImageDataURL(t *testing.T) {
15 t.Chdir(t.TempDir())
16 got, err := SaveImageDataURL("data:image/png;base64," + tinyPNG)
17 if err != nil {
18 t.Fatalf("SaveImageDataURL: %v", err)
19 }
20 if !strings.HasPrefix(got, ".reasonix/attachments/clipboard-") || !strings.HasSuffix(got, ".png") {
21 t.Fatalf("path = %q, want attachment png path", got)
22 }
23 }
24
25 func TestSaveImageDataURLRejectsSpoofedMime(t *testing.T) {
26 t.Chdir(t.TempDir())
27 if _, err := SaveImageDataURL("data:image/png;base64,aGk="); err == nil {
28 t.Fatal("spoofed image mime should fail")
29 }
30 }
31
32 func TestCreateAttachmentFileSkipsExistingPath(t *testing.T) {
33 t.Chdir(t.TempDir())
34 if err := ensureAttachmentRoot(); err != nil {
35 t.Fatal(err)
36 }
37
38 first := attachmentPath(".png")
39 if err := os.WriteFile(first, []byte("keep"), 0o644); err != nil {
40 t.Fatal(err)
41 }
42
43 rel, f, err := createAttachmentFile(".png")
44 if err != nil {
45 t.Fatalf("createAttachmentFile: %v", err)
46 }
47 if err := f.Close(); err != nil {
48 t.Fatal(err)
49 }
50 if rel == first {
51 t.Fatalf("createAttachmentFile reused existing path %q", rel)
52 }
53 if got, err := os.ReadFile(first); err != nil {
54 t.Fatal(err)
55 } else if string(got) != "keep" {
56 t.Fatalf("existing attachment was overwritten: %q", got)
57 }
58 }
59
60 func TestSaveImageBytesUsesUniquePathsWithinSameTimestamp(t *testing.T) {
61 t.Chdir(t.TempDir())
62 oldNow := attachmentNow
63 attachmentNow = func() time.Time {
64 return time.Date(2026, 6, 1, 10, 20, 30, 123456000, time.UTC)
65 }
66 defer func() {
67 attachmentNow = oldNow
68 }()
69
70 raw := mustBase64(t, tinyPNG)
71 first, err := SaveImageBytes("image/png", raw)
72 if err != nil {
73 t.Fatalf("first SaveImageBytes: %v", err)
74 }
75 second, err := SaveImageBytes("image/png", raw)
76 if err != nil {
77 t.Fatalf("second SaveImageBytes: %v", err)
78 }
79 if first == second {
80 t.Fatalf("paths collided: %q", first)
81 }
82 for _, path := range []string{first, second} {
83 if got, err := os.ReadFile(path); err != nil {
84 t.Fatalf("read %s: %v", path, err)
85 } else if string(got) != string(raw) {
86 t.Fatalf("content for %s changed", path)
87 }
88 }
89 }
90
91 func TestSaveImageFile(t *testing.T) {
92 t.Chdir(t.TempDir())
93 if err := os.WriteFile("source.png", mustBase64(t, tinyPNG), 0o644); err != nil {
94 t.Fatal(err)
95 }
96 got, err := SaveImageFile("source.png")
97 if err != nil {
98 t.Fatalf("SaveImageFile: %v", err)
99 }
100 if !strings.HasPrefix(got, ".reasonix/attachments/clipboard-") || !strings.HasSuffix(got, ".png") {
101 t.Fatalf("path = %q, want attachment png path", got)
102 }
103 }
104
105 func TestSaveAttachmentFile(t *testing.T) {
106 t.Chdir(t.TempDir())
107 if err := os.WriteFile("notes.pdf", []byte("%PDF-1.4 body"), 0o644); err != nil {
108 t.Fatal(err)
109 }
110 got, err := SaveAttachmentFile("notes.pdf")
111 if err != nil {
112 t.Fatalf("SaveAttachmentFile: %v", err)
113 }
114 if !strings.HasPrefix(got, ".reasonix/attachments/clipboard-") || !strings.HasSuffix(got, ".pdf") {
115 t.Fatalf("path = %q, want attachment pdf path", got)
116 }
117 if data, err := os.ReadFile(got); err != nil || string(data) != "%PDF-1.4 body" {
118 t.Fatalf("stored bytes = %q (err %v), want original", data, err)
119 }
120 }
121
122 func TestSaveAttachmentFileRejectsEmptyAndDir(t *testing.T) {
123 t.Chdir(t.TempDir())
124 if err := os.WriteFile("empty.txt", nil, 0o644); err != nil {
125 t.Fatal(err)
126 }
127 if _, err := SaveAttachmentFile("empty.txt"); err == nil {
128 t.Fatal("empty file should fail")
129 }
130 if err := os.Mkdir("adir", 0o755); err != nil {
131 t.Fatal(err)
132 }
133 if _, err := SaveAttachmentFile("adir"); err == nil {
134 t.Fatal("directory should fail")
135 }
136 }
137
138 func TestSaveAttachmentFileSanitizesExtension(t *testing.T) {
139 t.Chdir(t.TempDir())
140 if err := os.WriteFile("payload.weird-ext-here", []byte("x"), 0o644); err != nil {
141 t.Fatal(err)
142 }
143 got, err := SaveAttachmentFile("payload.weird-ext-here")
144 if err != nil {
145 t.Fatalf("SaveAttachmentFile: %v", err)
146 }
147 if !strings.HasSuffix(got, ".bin") {
148 t.Fatalf("path = %q, want .bin fallback for unsafe extension", got)
149 }
150 }
151
152 func TestSaveAttachmentFileRejectsSymlink(t *testing.T) {
153 t.Chdir(t.TempDir())
154 if err := os.WriteFile("source.bin", []byte("payload"), 0o644); err != nil {
155 t.Fatal(err)
156 }
157 if err := os.Symlink("source.bin", "link.bin"); err != nil {
158 t.Skipf("symlink unsupported: %v", err)
159 }
160 if _, err := SaveAttachmentFile("link.bin"); err == nil {
161 t.Fatal("symlink attachment path should fail")
162 }
163 }
164
165 func TestSaveImageFileRejectsSymlink(t *testing.T) {
166 t.Chdir(t.TempDir())
167 if err := os.WriteFile("source.png", mustBase64(t, tinyPNG), 0o644); err != nil {
168 t.Fatal(err)
169 }
170 if err := os.Symlink("source.png", "link.png"); err != nil {
171 t.Skipf("symlink unsupported: %v", err)
172 }
173 if _, err := SaveImageFile("link.png"); err == nil {
174 t.Fatal("symlink image path should fail")
175 }
176 }
177
178 func TestImageDataURLRejectsOutsideAttachmentDir(t *testing.T) {
179 t.Chdir(t.TempDir())
180 if err := os.WriteFile("x.png", []byte("x"), 0o644); err != nil {
181 t.Fatal(err)
182 }
183 if _, err := ImageDataURL("x.png"); err == nil {
184 t.Fatal("outside attachment dir should fail")
185 }
186 if _, err := ImageDataURL("../.reasonix/attachments/x.png"); err == nil {
187 t.Fatal("traversal path should fail")
188 }
189 }
190
191 func TestImageDataURLRejectsSymlinkFile(t *testing.T) {
192 t.Chdir(t.TempDir())
193 if err := ensureAttachmentRoot(); err != nil {
194 t.Fatal(err)
195 }
196 if err := os.WriteFile("secret.png", []byte("secret"), 0o644); err != nil {
197 t.Fatal(err)
198 }
199 link := filepath.Join(".reasonix", "attachments", "link.png")
200 if err := os.Symlink(filepath.Join("..", "..", "secret.png"), link); err != nil {
201 t.Skipf("symlink unsupported: %v", err)
202 }
203 if _, err := ImageDataURL(link); err == nil {
204 t.Fatal("symlink attachment file should fail")
205 }
206 }
207
208 func TestImageDataURLRejectsSymlinkAttachmentDir(t *testing.T) {
209 t.Chdir(t.TempDir())
210 if err := os.Mkdir(".reasonix", 0o755); err != nil {
211 t.Fatal(err)
212 }
213 if err := os.Mkdir("elsewhere", 0o755); err != nil {
214 t.Fatal(err)
215 }
216 if err := os.Symlink("../elsewhere", filepath.Join(".reasonix", "attachments")); err != nil {
217 t.Skipf("symlink unsupported: %v", err)
218 }
219 if _, err := ImageDataURL(".reasonix/attachments/x.png"); err == nil {
220 t.Fatal("symlink attachment directory should fail")
221 }
222 }
223
224 func TestImageDataURLRejectsSymlinkSubdirectory(t *testing.T) {
225 t.Chdir(t.TempDir())
226 if err := ensureAttachmentRoot(); err != nil {
227 t.Fatal(err)
228 }
229 if err := os.Mkdir("outside", 0o755); err != nil {
230 t.Fatal(err)
231 }
232 if err := os.WriteFile(filepath.Join("outside", "x.png"), mustBase64(t, tinyPNG), 0o644); err != nil {
233 t.Fatal(err)
234 }
235 link := filepath.Join(".reasonix", "attachments", "link")
236 if err := os.Symlink(filepath.Join("..", "..", "outside"), link); err != nil {
237 t.Skipf("symlink unsupported: %v", err)
238 }
239 if _, err := ImageDataURL(filepath.Join(link, "x.png")); err == nil {
240 t.Fatal("symlink attachment subdirectory should fail")
241 }
242 }
243
244 func mustBase64(t *testing.T, s string) []byte {
245 t.Helper()
246 raw, err := base64.StdEncoding.DecodeString(s)
247 if err != nil {
248 t.Fatal(err)
249 }
250 return raw
251 }
252
252 lines GO