返回 DeepSeek-Reasonix
present_preview_test.go
根目录 / desktop / present_preview_test.go
1 package main
2
3 import (
4 "io"
5 "net/http"
6 "net/http/httptest"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 )
12
13 func withPreviewWorkspace(t *testing.T) string {
14 t.Helper()
15 original, err := os.Getwd()
16 if err != nil {
17 t.Fatal(err)
18 }
19 dir := t.TempDir()
20 if err := os.Chdir(dir); err != nil {
21 t.Fatal(err)
22 }
23 t.Cleanup(func() { _ = os.Chdir(original) })
24 return dir
25 }
26
27 func TestHTMLPreviewBindsLocalDependencies(t *testing.T) {
28 withPreviewWorkspace(t)
29 if err := os.MkdirAll("assets", 0o755); err != nil {
30 t.Fatal(err)
31 }
32 files := map[string]string{
33 "index.html": `<!doctype html><link rel="stylesheet" href="assets/app.css"><script src="assets/app.js"></script><img src="assets/icon.png">`,
34 "assets/app.css": "body { color: green }",
35 "assets/app.js": "document.body.dataset.ready = 'yes'",
36 "assets/icon.png": "png-bytes",
37 }
38 for name, body := range files {
39 if err := os.WriteFile(filepath.FromSlash(name), []byte(body), 0o644); err != nil {
40 t.Fatal(err)
41 }
42 }
43
44 app := NewApp()
45 preview := app.ReadFile("index.html")
46 if preview.Err != "" || preview.Kind != "html" || preview.URL == "" {
47 t.Fatalf("HTML preview = %+v", preview)
48 }
49 tokenRoot := strings.TrimSuffix(preview.URL, "/index.html")
50 handler := app.workspaceMediaMiddleware()(http.NotFoundHandler())
51 for name, want := range files {
52 requestPath := tokenRoot + "/" + name
53 recorder := httptest.NewRecorder()
54 handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, requestPath, nil))
55 if recorder.Code != http.StatusOK || recorder.Body.String() != want {
56 t.Fatalf("GET %s = %d %q, want 200 %q", requestPath, recorder.Code, recorder.Body.String(), want)
57 }
58 }
59 app.RevokeWorkspaceMediaPreview(preview.URL)
60 recorder := httptest.NewRecorder()
61 handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, preview.URL, nil))
62 if recorder.Code != http.StatusNotFound {
63 t.Fatalf("revoked document preview response = %d, want 404", recorder.Code)
64 }
65 }
66
67 func TestHTMLPreviewDecodesDependencyPathsExactlyOnce(t *testing.T) {
68 withPreviewWorkspace(t)
69 files := map[string]string{
70 "raw%20name.js": "literal-percent",
71 "raw name.js": "space",
72 "100%.css": "literal-percent-sign",
73 "中文#.png": "unicode-hash",
74 }
75 for name, body := range files {
76 if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
77 t.Fatal(err)
78 }
79 }
80 htmlBody := `<script src="raw%2520name.js"></script><script src="raw%20name.js"></script>` +
81 `<link href="100%25.css"><img src="%E4%B8%AD%E6%96%87%23.png">`
82 if err := os.WriteFile("index.html", []byte(htmlBody), 0o644); err != nil {
83 t.Fatal(err)
84 }
85 app := NewApp()
86 preview := app.ReadFile("index.html")
87 if preview.Err != "" || preview.URL == "" {
88 t.Fatalf("HTML preview = %+v", preview)
89 }
90 tokenRoot := strings.TrimSuffix(preview.URL, "/index.html")
91 handler := app.workspaceMediaMiddleware()(http.NotFoundHandler())
92 requests := map[string]string{
93 "raw%2520name.js": "literal-percent",
94 "raw%20name.js": "space",
95 "100%25.css": "literal-percent-sign",
96 "%E4%B8%AD%E6%96%87%23.png": "unicode-hash",
97 }
98 for encoded, want := range requests {
99 recorder := httptest.NewRecorder()
100 handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, tokenRoot+"/"+encoded, nil))
101 if recorder.Code != http.StatusOK || recorder.Body.String() != want {
102 t.Fatalf("GET %q = %d %q, want 200 %q", encoded, recorder.Code, recorder.Body.String(), want)
103 }
104 }
105 }
106
107 func TestHTMLPreviewRejectsEncodedTraversalAndNUL(t *testing.T) {
108 withPreviewWorkspace(t)
109 for _, source := range []string{"%2e%2e/outside.js", "asset%00.js"} {
110 if err := os.WriteFile("index.html", []byte(`<script src="`+source+`"></script>`), 0o644); err != nil {
111 t.Fatal(err)
112 }
113 if preview := NewApp().ReadFile("index.html"); preview.URL != "" || preview.Err == "" {
114 t.Fatalf("unsafe dependency %q = %+v", source, preview)
115 }
116 }
117 }
118
119 func TestHTMLPreviewFailsBeforePublishingIncompleteBundle(t *testing.T) {
120 withPreviewWorkspace(t)
121 if err := os.WriteFile("index.html", []byte(`<script src="missing.js"></script>`), 0o644); err != nil {
122 t.Fatal(err)
123 }
124 preview := NewApp().ReadFile("index.html")
125 if preview.URL != "" || !strings.Contains(preview.Err, "missing.js") {
126 t.Fatalf("incomplete HTML preview = %+v", preview)
127 }
128 }
129
130 func TestWorkspaceBrowserPreviewUsesRevocableLoopbackOrigin(t *testing.T) {
131 withPreviewWorkspace(t)
132 if err := os.WriteFile("index.html", []byte("<!doctype html><title>preview</title>"), 0o644); err != nil {
133 t.Fatal(err)
134 }
135 app := NewApp()
136 previewURL, err := app.CreateWorkspaceBrowserPreviewForTab("", "index.html")
137 if err != nil {
138 t.Fatal(err)
139 }
140 if !strings.HasPrefix(previewURL, "http://127.0.0.1:") || !strings.Contains(previewURL, "/__reasonix_workspace_media/") {
141 t.Fatalf("browser preview URL = %q", previewURL)
142 }
143 response, err := http.Get(previewURL) //nolint:gosec -- loopback URL created by the test app
144 if err != nil {
145 t.Fatal(err)
146 }
147 body, readErr := io.ReadAll(response.Body)
148 _ = response.Body.Close()
149 if readErr != nil || response.StatusCode != http.StatusOK || !strings.Contains(string(body), "preview") {
150 t.Fatalf("browser preview response = %d %q, err=%v", response.StatusCode, body, readErr)
151 }
152 app.RevokeWorkspaceBrowserPreview(previewURL)
153 revoked, err := http.Get(previewURL) //nolint:gosec -- loopback URL created by the test app
154 if err != nil {
155 t.Fatal(err)
156 }
157 _ = revoked.Body.Close()
158 if revoked.StatusCode != http.StatusNotFound {
159 t.Fatalf("revoked browser preview response = %d, want 404", revoked.StatusCode)
160 }
161 app.stopWorkspacePreviewOrigin()
162 }
163
164 func TestPresentedTextPagesAreUTF8AlignedAndVersionFenced(t *testing.T) {
165 dir := t.TempDir()
166 path := filepath.Join(dir, "large.txt")
167 prefix := strings.Repeat("a", presentedTextPageLimit-1)
168 body := prefix + "界" + strings.Repeat("b", 64)
169 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
170 t.Fatal(err)
171 }
172 info, err := os.Stat(path)
173 if err != nil {
174 t.Fatal(err)
175 }
176 version := workspaceFileVersion(info)
177 first, err := readPresentedTextPage(path, "large.txt", 0, version)
178 if err != nil {
179 t.Fatal(err)
180 }
181 if !first.HasMore || first.NextOffset != int64(len(prefix)) || first.Body != prefix {
182 t.Fatalf("first page split = offset %d more=%v tail=%q", first.NextOffset, first.HasMore, first.Body[len(first.Body)-4:])
183 }
184 second, err := readPresentedTextPage(path, "large.txt", first.NextOffset, version)
185 if err != nil {
186 t.Fatal(err)
187 }
188 if first.Body+second.Body != body || second.HasMore {
189 t.Fatalf("joined pages do not equal source (joined=%d source=%d more=%v)", len(first.Body+second.Body), len(body), second.HasMore)
190 }
191 if err := os.WriteFile(path, []byte(body+"changed"), 0o644); err != nil {
192 t.Fatal(err)
193 }
194 if _, err := readPresentedTextPage(path, "large.txt", first.NextOffset, version); err == nil || !strings.Contains(err.Error(), "file changed") {
195 t.Fatalf("stale version error = %v", err)
196 }
197 }
198
198 lines GO