返回 DeepSeek-Reasonix
outbound_media_test.go
根目录 / internal / bot / feishu / outbound_media_test.go
1 package feishu
2
3 import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "testing"
8
9 "reasonix/internal/config"
10 )
11
12 func TestReadOutboundFileConfinement(t *testing.T) {
13 root := t.TempDir()
14 if err := os.WriteFile(filepath.Join(root, "ok.txt"), []byte("hello"), 0o600); err != nil {
15 t.Fatal(err)
16 }
17 outside := filepath.Join(t.TempDir(), "secret.txt")
18 if err := os.WriteFile(outside, []byte("secret"), 0o600); err != nil {
19 t.Fatal(err)
20 }
21
22 a := &adapter{cfg: config.FeishuBotConfig{OutboundMediaRoots: []string{root}}}
23
24 data, name, err := a.readOutboundFile("ok.txt")
25 if err != nil {
26 t.Fatalf("file inside root should be readable: %v", err)
27 }
28 if string(data) != "hello" || name != "ok.txt" {
29 t.Fatalf("got %q/%q, want hello/ok.txt", data, name)
30 }
31
32 // Paths are never aliases for a staged filename: callers must send a bare
33 // filename, even when an absolute path happens to point inside a root.
34 if _, _, err := a.readOutboundFile(filepath.Join(root, "ok.txt")); err == nil {
35 t.Fatal("absolute paths must be rejected")
36 }
37
38 // Outside every root: rejected before any lookup.
39 if _, _, err := a.readOutboundFile(outside); err == nil {
40 t.Fatal("file outside the roots must be rejected")
41 }
42
43 // Traversal that resolves outside the root: rejected.
44 if _, _, err := a.readOutboundFile(filepath.Join(root, "..", filepath.Base(outside))); err == nil {
45 t.Fatal("traversal out of the root must be rejected")
46 }
47
48 // Relative path: rejected.
49 if _, _, err := a.readOutboundFile("relative/path"); err == nil {
50 t.Fatal("relative path must be rejected")
51 }
52
53 // No roots configured: local sending disabled.
54 off := &adapter{cfg: config.FeishuBotConfig{}}
55 if _, _, err := off.readOutboundFile("ok.txt"); err == nil {
56 t.Fatal("local file sending must be disabled when no roots are set")
57 }
58 }
59
60 func TestReadOutboundFileRejectsAmbiguousName(t *testing.T) {
61 first := t.TempDir()
62 second := t.TempDir()
63 for _, root := range []string{first, second} {
64 if err := os.WriteFile(filepath.Join(root, "report.pdf"), []byte(root), 0o600); err != nil {
65 t.Fatal(err)
66 }
67 }
68 a := &adapter{cfg: config.FeishuBotConfig{OutboundMediaRoots: []string{first, second}}}
69 if _, _, err := a.readOutboundFile("report.pdf"); err == nil {
70 t.Fatal("the same filename in multiple roots must be rejected as ambiguous")
71 }
72 }
73
74 func TestReadOutboundFileRequiresAbsoluteRoots(t *testing.T) {
75 a := &adapter{cfg: config.FeishuBotConfig{OutboundMediaRoots: []string{"relative-root"}}}
76 if _, _, err := a.readOutboundFile("report.pdf"); err == nil {
77 t.Fatal("relative outbound media roots must be rejected")
78 }
79 }
80
81 func TestReadOutboundFileEnforcesActualReadLimit(t *testing.T) {
82 root := t.TempDir()
83 path := filepath.Join(root, "large.bin")
84 if err := os.WriteFile(path, []byte{1}, 0o600); err != nil {
85 t.Fatal(err)
86 }
87 if err := os.Truncate(path, maxOutboundMediaBytes+1); err != nil {
88 t.Fatal(err)
89 }
90 a := &adapter{cfg: config.FeishuBotConfig{OutboundMediaRoots: []string{root}}}
91 if _, _, err := a.readOutboundFile("large.bin"); err == nil {
92 t.Fatal("files larger than the actual read limit must be rejected")
93 }
94 }
95
96 func TestLoadOutboundMediaEnforcesAggregateLimit(t *testing.T) {
97 root := t.TempDir()
98 for _, name := range []string{"first.bin", "second.bin"} {
99 path := filepath.Join(root, name)
100 if err := os.WriteFile(path, []byte{1}, 0o600); err != nil {
101 t.Fatal(err)
102 }
103 if err := os.Truncate(path, maxOutboundMediaBytes/2+1); err != nil {
104 t.Fatal(err)
105 }
106 }
107 a := &adapter{cfg: config.FeishuBotConfig{OutboundMediaRoots: []string{root}}}
108 if _, err := a.loadOutboundMedia([]string{"first.bin", "second.bin"}); err == nil {
109 t.Fatal("aggregate outbound media larger than 25 MB must be rejected before sending")
110 }
111 }
112
113 func TestReadOutboundFileRejectsSymlinkEscape(t *testing.T) {
114 if runtime.GOOS == "windows" {
115 t.Skip("symlink creation is unreliable on Windows CI")
116 }
117 root := t.TempDir()
118 secret := filepath.Join(t.TempDir(), "secret.txt")
119 if err := os.WriteFile(secret, []byte("top secret"), 0o600); err != nil {
120 t.Fatal(err)
121 }
122 // A symlink living inside the allowed root but pointing outside it.
123 link := filepath.Join(root, "escape.txt")
124 if err := os.Symlink(secret, link); err != nil {
125 t.Skipf("symlink not supported: %v", err)
126 }
127 a := &adapter{cfg: config.FeishuBotConfig{OutboundMediaRoots: []string{root}}}
128 if _, _, err := a.readOutboundFile("escape.txt"); err == nil {
129 t.Fatal("a symlink escaping the root must be rejected (symlink resolution)")
130 }
131 }
132
133 func TestReadOutboundFileAcceptsSymlinkWithinRoot(t *testing.T) {
134 if runtime.GOOS == "windows" {
135 t.Skip("symlink creation is unreliable on Windows CI")
136 }
137 root := t.TempDir()
138 real := filepath.Join(root, "real.txt")
139 if err := os.WriteFile(real, []byte("ok"), 0o600); err != nil {
140 t.Fatal(err)
141 }
142 link := filepath.Join(root, "link.txt")
143 if err := os.Symlink(filepath.Base(real), link); err != nil {
144 t.Skipf("symlink not supported: %v", err)
145 }
146 a := &adapter{cfg: config.FeishuBotConfig{OutboundMediaRoots: []string{root}}}
147 if _, _, err := a.readOutboundFile("link.txt"); err != nil {
148 t.Fatalf("a symlink staying within the root should be allowed: %v", err)
149 }
150 }
151
151 lines GO