返回 DeepSeek-Reasonix
crash_app_test.go
根目录 / desktop / crash_app_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "reflect"
10 "strings"
11 "testing"
12 )
13
14 func TestScrubUserPaths(t *testing.T) {
15 cases := map[string]string{
16 `at C:\Users\yuhua\proj\app.ts:12:3`: `at C:\Users\_\proj\app.ts:12:3`,
17 `at c:\users\someone\x.go`: `at c:\users\_\x.go`,
18 `/home/bob/.reasonix/config.toml`: `/home/_/.reasonix/config.toml`,
19 `/Users/alice/Library/Logs`: `/Users/_/Library/Logs`,
20 `Error: ENOENT open '/home/bob/secret'`: `Error: ENOENT open '/home/_/secret'`,
21 `no user path here: /usr/lib/node`: `no user path here: /usr/lib/node`,
22 "first /home/a/x\nsecond C:\\Users\\b\\y": "first /home/_/x\nsecond C:\\Users\\_\\y",
23 }
24 for in, want := range cases {
25 if got := scrubUserPaths(in); got != want {
26 t.Errorf("scrubUserPaths(%q) = %q, want %q", in, got, want)
27 }
28 }
29 }
30
31 func TestScrubSensitiveText(t *testing.T) {
32 apiKey := "sk-proj-" + "abcdefghijklmnopqrstuvwxyz1234567890"
33 bearer := "abcdefghijklmnopqrstuvwxyz1234567890ABCDE"
34 longHex := "0123456789abcdef0123456789abcdef"
35 jwt := "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.signature"
36 got := scrubSensitiveText("user dev@example.com Authorization: Bearer " + bearer + " api_key=" + apiKey + " jwt " + jwt + " hash " + longHex + " env FEISHU_BOT_APP_SECRET WEIXIN_BOT_TOKEN short abc1234 path /Users/alice/x")
37
38 for _, leaked := range []string{"dev@example.com", bearer, apiKey, jwt, longHex, "FEISHU_BOT_APP_SECRET", "WEIXIN_BOT_TOKEN", "alice"} {
39 if strings.Contains(got, leaked) {
40 t.Fatalf("sensitive text leaked %q in %q", leaked, got)
41 }
42 }
43 for _, want := range []string{"[redacted-email]", "Authorization=[redacted]", "api_key=[redacted]", "[redacted-jwt]", "[redacted-hex]", "[redacted-env]", "short abc1234", "/Users/_/x"} {
44 if !strings.Contains(got, want) {
45 t.Fatalf("scrubSensitiveText() = %q, want it to contain %q", got, want)
46 }
47 }
48 }
49
50 func TestPostCrashReport(t *testing.T) {
51 var got crashReport
52 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53 if r.Method != http.MethodPost {
54 t.Errorf("method = %s, want POST", r.Method)
55 }
56 if ct := r.Header.Get("Content-Type"); ct != "application/json" {
57 t.Errorf("content-type = %q", ct)
58 }
59 body, _ := io.ReadAll(r.Body)
60 if err := json.Unmarshal(body, &got); err != nil {
61 t.Errorf("body not JSON: %v", err)
62 }
63 w.WriteHeader(http.StatusAccepted)
64 }))
65 defer srv.Close()
66
67 r := crashReport{Kind: "crash", Version: "v9.9.9", OS: "windows", Arch: "amd64", Message: "[react]\nboom"}
68 if err := postCrashReport(context.Background(), srv.Client(), srv.URL, r); err != nil {
69 t.Fatal(err)
70 }
71 if !installIDPattern.MatchString(got.InstallID) {
72 t.Fatalf("send-time install id = %q", got.InstallID)
73 }
74 got.InstallID = ""
75 if !reflect.DeepEqual(got, r) {
76 t.Errorf("server received %+v, want %+v", got, r)
77 }
78 }
79
80 func TestPostCrashReportRejectedStatus(t *testing.T) {
81 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
82 w.WriteHeader(http.StatusTooManyRequests)
83 }))
84 defer srv.Close()
85
86 err := postCrashReport(context.Background(), srv.Client(), srv.URL, crashReport{Kind: "crash"})
87 if err == nil || !strings.Contains(err.Error(), "429") {
88 t.Fatalf("want 429 error, got %v", err)
89 }
90 }
91
92 func TestReportCrashRejectsBadInput(t *testing.T) {
93 app := NewApp()
94 if err := app.ReportCrash("telemetry", "x"); err == nil {
95 t.Error("unknown kind should be rejected")
96 }
97 if err := app.ReportCrash("crash", ""); err == nil {
98 t.Error("empty detail should be rejected")
99 }
100 }
101
102 func TestCrashReportFromStructuredDetail(t *testing.T) {
103 apiKey := "sk-proj-" + "abcdefghijklmnopqrstuvwxyz1234567890"
104 secretHex := "abcdefabcdefabcdefabcdefabcdef12"
105 buildCommit := "0123456789abcdef0123456789abcdef01234567"
106 payload := frontendCrashPayload{
107 SchemaVersion: 2,
108 Kind: "exception",
109 Source: "frontend",
110 Label: "unhandledrejection",
111 Message: "[unhandledrejection]\n\ninvalid argument at C:\\Users\\alice\\app.ts:1 from alice@example.com",
112 ErrorType: "TypeError",
113 ErrorMessage: "invalid argument at /Users/alice/project/app.ts api_key=" + apiKey,
114 Stack: "TypeError: invalid argument\n at run (/Users/alice/project/app.ts:12:3)\nsecret=" + secretHex,
115 TopFrame: "at run (/Users/alice/project/app.ts:12:3)",
116 FingerprintHint: "build:01234567|view:app://reasonix/|cats:startup>tabs",
117 BuildCommit: buildCommit,
118 Channel: "canary",
119 Language: "zh-CN",
120 View: "wails://wails.localhost/index.html?token=" + secretHex,
121 Breadcrumbs: []crashBreadcrumb{{T: 1, Cat: "bridge", Msg: "turn SubmitToTab token=" + apiKey}},
122 }
123 detail, err := json.Marshal(payload)
124 if err != nil {
125 t.Fatal(err)
126 }
127 r, err := crashReportFromDetail("crash", string(detail))
128 if err != nil {
129 t.Fatal(err)
130 }
131 if r.Kind != "exception" || r.Source != "frontend" || r.Label != "unhandledrejection" {
132 t.Fatalf("structured fields not preserved: %+v", r)
133 }
134 if strings.Contains(r.Message, "alice") || strings.Contains(r.ErrorMessage, "alice") || strings.Contains(r.Stack, "alice") {
135 t.Fatalf("user path was not scrubbed: %+v", r)
136 }
137 if r.TopFrame == "" || r.FingerprintHint != payload.FingerprintHint || r.BuildCommit != buildCommit || r.Channel != "canary" || len(r.Breadcrumbs) != 1 {
138 t.Fatalf("metadata missing: %+v", r)
139 }
140 freeText := strings.Join([]string{
141 r.Message,
142 r.ErrorMessage,
143 r.Stack,
144 r.ComponentStack,
145 r.TopFrame,
146 r.View,
147 r.Breadcrumbs[0].Msg,
148 }, "\n")
149 for _, leaked := range []string{apiKey, secretHex, "alice@example.com"} {
150 if strings.Contains(freeText, leaked) {
151 t.Fatalf("sensitive value leaked %q in %+v", leaked, r)
152 }
153 }
154 }
155
156 func TestCrashReportFromPerformanceDetail(t *testing.T) {
157 payload := frontendCrashPayload{
158 SchemaVersion: 2,
159 Kind: "performance",
160 Source: "frontend.performance",
161 Label: "performance.pressure",
162 Message: "[performance.pressure]\n\n--- performance context ---\nreason: event loop lag 1300ms",
163 ErrorType: "PerformancePressure",
164 ErrorMessage: "UI responsiveness degraded because the app observed long tasks, event-loop lag, or high JS heap pressure.",
165 TopFrame: "frontend.performance",
166 }
167 detail, err := json.Marshal(payload)
168 if err != nil {
169 t.Fatal(err)
170 }
171 r, err := crashReportFromDetail("performance", string(detail))
172 if err != nil {
173 t.Fatal(err)
174 }
175 if r.Kind != "performance" || r.Source != "frontend.performance" || r.Label != "performance.pressure" {
176 t.Fatalf("performance fields not preserved: %+v", r)
177 }
178 if !strings.Contains(r.Message, "--- native runtime context ---") || !strings.Contains(r.Message, "goroutines:") {
179 t.Fatalf("native runtime context missing from performance report: %q", r.Message)
180 }
181 }
182
183 func TestCrashReportFromBotDetail(t *testing.T) {
184 token := "abcdefghijklmnopqrstuvwxyz1234567890ABCDE"
185 payload := frontendCrashPayload{
186 SchemaVersion: 2,
187 Kind: "bot",
188 Source: "bot.runtime",
189 Label: "bot.feishu.lark.send",
190 Message: "[bot]\n\nfailed at /Users/alice/project with token=" + token,
191 ErrorType: "BotConnectionDiagnostic",
192 ErrorMessage: "send failed with Bearer " + token,
193 TopFrame: "bot.send",
194 }
195 detail, err := json.Marshal(payload)
196 if err != nil {
197 t.Fatal(err)
198 }
199 r, err := crashReportFromDetail("bot", string(detail))
200 if err != nil {
201 t.Fatal(err)
202 }
203 if r.Kind != "bot" || r.Source != "bot.runtime" || r.Label != "bot.feishu.lark.send" {
204 t.Fatalf("bot fields not preserved: %+v", r)
205 }
206 if strings.Contains(r.Message, "alice") || strings.Contains(r.Message, token) || strings.Contains(r.ErrorMessage, token) {
207 t.Fatalf("bot report was not scrubbed: %+v", r)
208 }
209 if strings.Contains(r.Message, "--- native runtime context ---") {
210 t.Fatalf("bot report should not include performance runtime context: %q", r.Message)
211 }
212 }
213
213 lines GO