1package cef
2
3import (
4 "context"
5 "encoding/base64"
6 "net/http"
7 "regexp"
8 "strings"
9 "testing"
10
11 purecef "github.com/bnema/purego-cef/cef"
12 cefmocks "github.com/bnema/purego-cef/cef/mocks"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"
16
17 "github.com/bnema/dumber/internal/application/dto"
18 "github.com/bnema/dumber/internal/application/port"
19)
20
21// expectVimPageScript expects one ExecuteJavaScript call and captures the
22// script that was actually sent to the page.
23func expectVimPageScript(t *testing.T) (*WebView, *string) {
24 t.Helper()
25 browser := cefmocks.NewMockBrowser(t)
26 frame := cefmocks.NewMockFrame(t)
27 var script string
28 browser.EXPECT().GetMainFrame().Return(frame).Once()
29 frame.EXPECT().ExecuteJavaScript(mock.Anything, "", int32(0)).Run(func(code, _ string, _ int32) { script = code }).Once()
30 return &WebView{browser: browser, bridgeNonce: "shared-bridge-nonce"}, &script
31}
32
33func TestWebViewStartVimPageInteractionArmsFreshTokenWithoutBridgeNonce(t *testing.T) {
34 wv, script := expectVimPageScript(t)
35
36 err := wv.StartVimPageInteraction(context.Background(), dto.VimPageInteractionRequest{
37 Kind: dto.VimPageYankObject, Object: dto.VimPageTextObjectCode, HighlightColor: "#123456",
38 })
39 require.NoError(t, err)
40 token := wv.currentVimPageToken()
41 require.NotEmpty(t, token)
42 assert.NotEqual(t, "shared-bridge-nonce", token)
43 assert.Contains(t, *script, "if (window.__dumberVimPage) return;", "start ships the runtime")
44 assert.Contains(t, *script, `dumb:///api/vim-page`)
45 assert.NotContains(t, *script, "shared-bridge-nonce")
46 assert.Contains(t, *script, `window.__dumberVimPage.start("`+token+`", {"kind":"yank","object":"code","color":"#123456"});`)
47}
48
49// Keys and cancels run on the already installed runtime: shipping the whole
50// runtime for every key press would cost tens of kilobytes per keystroke.
51func TestWebViewSendVimPageKeyDoesNotShipRuntime(t *testing.T) {
52 wv, script := expectVimPageScript(t)
53 wv.vimPageToken = "tok"
54
55 require.NoError(t, wv.SendVimPageKey(context.Background(), "\"\a"))
56 require.NoError(t, wv.SendVimPageKey(context.Background(), ""))
57
58 assert.True(t, strings.HasPrefix(*script, `window.__dumberVimPage ? window.__dumberVimPage.key("tok", "\"\u0007") : `), *script)
59 assert.NotContains(t, *script, "if (window.__dumberVimPage) return;")
60 assert.Less(t, len(*script), 1024)
61}
62
63// A document replaced between start and key leaves no runtime; the script must
64// then report the end of that token so the UI releases its key capture.
65func TestVimPageCallScriptReportsEndWhenRuntimeMissing(t *testing.T) {
66 script := vimPageCallScript("cancel", "tok")
67
68 assert.Contains(t, script, `window.__dumberVimPage ? window.__dumberVimPage.cancel("tok") : `)
69 assert.Contains(t, script, `dumb:///api/vim-page`)
70 match := regexp.MustCompile(`"X-Dumber-Body": "([^"]+)"`).FindStringSubmatch(script)
71 require.Len(t, match, 2)
72 body, err := base64.StdEncoding.DecodeString(match[1])
73 require.NoError(t, err)
74 assert.JSONEq(t, `{"token":"tok","type":"end"}`, string(body))
75}
76
77func TestWebViewSendVimPageKeyWithoutInteractionIsNoop(t *testing.T) {
78 wv := &WebView{browser: cefmocks.NewMockBrowser(t)}
79
80 require.NoError(t, wv.SendVimPageKey(context.Background(), "j"))
81}
82
83func TestWebViewCancelVimPageInteractionDisarmsToken(t *testing.T) {
84 wv, script := expectVimPageScript(t)
85 wv.vimPageToken = "tok"
86
87 require.NoError(t, wv.CancelVimPageInteraction(context.Background()))
88 assert.True(t, strings.HasPrefix(*script, `window.__dumberVimPage ? window.__dumberVimPage.cancel("tok") : `), *script)
89 assert.Empty(t, wv.currentVimPageToken())
90 require.NoError(t, wv.CancelVimPageInteraction(context.Background()))
91}
92
93func TestVimPageRequestJSONRejectsUnknownRequests(t *testing.T) {
94 _, err := vimPageRequestJSON(dto.VimPageInteractionRequest{})
95 require.Error(t, err)
96 _, err = vimPageRequestJSON(dto.VimPageInteractionRequest{Kind: dto.VimPageYankObject})
97 require.Error(t, err)
98
99 got, err := vimPageRequestJSON(dto.VimPageInteractionRequest{Kind: dto.VimPageHintFollowNew})
100 require.NoError(t, err)
101 assert.JSONEq(t, `{"kind":"hint-follow-new"}`, got)
102}
103
104// The accent reaches the page as a CSS color; only hex colors are sent.
105func TestVimPageRequestJSONValidatesAccentColor(t *testing.T) {
106 for color, want := range map[string]string{
107 "#fbbf24": `{"kind":"visual","color":"#fbbf24"}`,
108 "#FFF": `{"kind":"visual","color":"#FFF"}`,
109 "#aabbccdd": `{"kind":"visual","color":"#aabbccdd"}`,
110 "": `{"kind":"visual"}`,
111 "red": `{"kind":"visual"}`,
112 "#12": `{"kind":"visual"}`,
113 "#12345": `{"kind":"visual"}`,
114 "#1234567": `{"kind":"visual"}`,
115 "#123456789": `{"kind":"visual"}`,
116 "#abc;} body{display:none": `{"kind":"visual"}`,
117 "url(javascript:alert(1))": `{"kind":"visual"}`,
118 "#abc\"; alert(1); //": `{"kind":"visual"}`,
119 } {
120 got, err := vimPageRequestJSON(dto.VimPageInteractionRequest{Kind: dto.VimPageVisual, HighlightColor: color})
121 require.NoError(t, err)
122 assert.JSONEq(t, want, got, "color %q", color)
123 }
124}
125
126func TestDecodeVimPageBridgePayload(t *testing.T) {
127 tests := []struct {
128 name string
129 body string
130 wantErr bool
131 }{
132 {"copy", `{"type":"copy","token":"t","text":"hello"}`, false},
133 {"copy empty", `{"type":"copy","token":"t"}`, true},
134 {"missing token", `{"type":"end"}`, true},
135 {"open", `{"type":"open-new","token":"t","url":"https://example.com/a"}`, false},
136 {"open empty", `{"type":"open-new","token":"t"}`, true},
137 {"end", `{"type":"end","token":"t"}`, false},
138 {"mode caret", `{"type":"mode","token":"t","mode":"caret"}`, false},
139 {"mode visual line", `{"type":"mode","token":"t","mode":"visual-line"}`, false},
140 {"mode unknown", `{"type":"mode","token":"t","mode":"insert"}`, true},
141 {"mode missing", `{"type":"mode","token":"t"}`, true},
142 {"unknown", `{"type":"eval","token":"t"}`, true},
143 {"malformed", `{`, true},
144 }
145 for _, tt := range tests {
146 t.Run(tt.name, func(t *testing.T) {
147 _, err := decodeVimPageBridgePayload([]byte(tt.body))
148 if tt.wantErr {
149 require.Error(t, err)
150 return
151 }
152 require.NoError(t, err)
153 })
154 }
155}
156
157func TestIsNavigableVimHintURL(t *testing.T) {
158 assert.True(t, isNavigableVimHintURL("https://example.com/a", "https://site.test/"))
159 assert.False(t, isNavigableVimHintURL("https:///nohost", "https://site.test/"))
160 assert.False(t, isNavigableVimHintURL("javascript:alert(1)", "https://site.test/"))
161 assert.False(t, isNavigableVimHintURL("dumb://config", "https://site.test/"))
162 assert.False(t, isNavigableVimHintURL("file:///etc/passwd", "https://site.test/"))
163 assert.True(t, isNavigableVimHintURL("file:///tmp/b.html", "file:///tmp/a.html"))
164}
165
166func newVimResultWebView(opened *string, ended *int) *WebView {
167 return &WebView{ctx: context.Background(), uri: "https://site.test/", callbacks: &port.WebViewCallbacks{
168 OnLinkMiddleClick: func(uri string) bool { *opened = uri; return true },
169 OnVimPageInteractionEnded: func() { *ended++ },
170 }}
171}
172
173// Mode messages and copies of the caret/visual flow are not terminal: the
174// token stays armed so the user can keep moving and chain copies, and the UI
175// is told about each sub-mode without being asked to release key capture.
176func TestWebViewHandleVimPageResultVisualFlowStaysArmedAcrossModeAndCopy(t *testing.T) {
177 var opened string
178 ended := 0
179 var modes []dto.VimPageMode
180 wv := newVimResultWebView(&opened, &ended)
181 wv.callbacks.OnVimPageModeChanged = func(mode dto.VimPageMode) { modes = append(modes, mode) }
182 wv.vimPageToken, wv.vimPageKind = "tok", dto.VimPageVisual
183 wv.vimPageCopyAllowance = 1
184
185 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageMode, Token: "tok", Mode: "caret"})
186 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageMode, Token: "tok", Mode: "visual-line"})
187 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageCopy, Token: "tok", Text: "copied"})
188
189 assert.Equal(t, []dto.VimPageMode{dto.VimPageModeCaret, dto.VimPageModeVisualLine}, modes)
190 assert.Zero(t, ended, "mode and copy must not end the interaction")
191 assert.Equal(t, "tok", wv.currentVimPageToken())
192
193 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageEnd, Token: "tok"})
194 assert.Equal(t, 1, ended)
195 assert.Empty(t, wv.currentVimPageToken())
196}
197
198func TestWebViewHandleVimPageResultCopyEndsHintYankAndYankObject(t *testing.T) {
199 for _, kind := range []dto.VimPageInteractionKind{dto.VimPageHintYankURL, dto.VimPageYankObject} {
200 var opened string
201 ended := 0
202 wv := newVimResultWebView(&opened, &ended)
203 wv.vimPageToken, wv.vimPageKind = "tok", kind
204
205 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageCopy, Token: "tok", Text: "x"})
206
207 assert.Empty(t, wv.currentVimPageToken(), "kind %d: copy stays terminal", kind)
208 assert.Equal(t, map[bool]int{true: 1, false: 0}[kind.CapturesKeys()], ended, "kind %d", kind)
209 }
210}
211
212func TestWebViewHandleVimPageResultModeRejectedForLinkHints(t *testing.T) {
213 var opened string
214 ended := 0
215 modes := 0
216 wv := newVimResultWebView(&opened, &ended)
217 wv.callbacks.OnVimPageModeChanged = func(dto.VimPageMode) { modes++ }
218 wv.vimPageToken, wv.vimPageKind = "tok", dto.VimPageHintFollow
219
220 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageMode, Token: "tok", Mode: "caret"})
221
222 assert.Zero(t, modes, "hint interactions never report sub-modes")
223}
224
225func TestWebViewHandleVimPageResultAcceptsArmedTokenOnce(t *testing.T) {
226 var opened string
227 ended := 0
228 wv := newVimResultWebView(&opened, &ended)
229 wv.vimPageToken, wv.vimPageKind = "tok", dto.VimPageHintFollowNew
230
231 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageOpenNew, Token: "tok", URL: "https://example.com"})
232 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageOpenNew, Token: "tok", URL: "https://evil.test"})
233
234 assert.Equal(t, "https://example.com", opened)
235 assert.Equal(t, 1, ended)
236 assert.Empty(t, wv.currentVimPageToken())
237}
238
239func TestWebViewHandleVimPageResultRejectsStaleOrForgedTokens(t *testing.T) {
240 var opened string
241 ended := 0
242 wv := newVimResultWebView(&opened, &ended)
243 wv.vimPageToken, wv.vimPageKind = "current", dto.VimPageHintFollowNew
244
245 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageEnd, Token: "previous"})
246 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageOpenNew, Token: "forged", URL: "https://example.com"})
247
248 assert.Empty(t, opened)
249 assert.Zero(t, ended)
250 assert.Equal(t, "current", wv.currentVimPageToken())
251}
252
253func TestWebViewHandleVimPageResultRejectsDisallowedURL(t *testing.T) {
254 var opened string
255 ended := 0
256 wv := newVimResultWebView(&opened, &ended)
257 wv.vimPageToken, wv.vimPageKind = "tok", dto.VimPageHintFollowNew
258
259 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageOpenNew, Token: "tok", URL: "file:///etc/passwd"})
260
261 assert.Empty(t, opened)
262 assert.Equal(t, 1, ended, "the interaction still ends so key capture is released")
263}
264
265func TestWebViewEndVimPageInteractionOnNavigation(t *testing.T) {
266 var opened string
267 ended := 0
268 wv := newVimResultWebView(&opened, &ended)
269 wv.vimPageToken, wv.vimPageKind = "tok", dto.VimPageHintFollowNew
270
271 wv.endVimPageInteractionOnNavigation()
272 wv.endVimPageInteractionOnNavigation()
273
274 assert.Equal(t, 1, ended)
275 assert.Empty(t, wv.currentVimPageToken())
276}
277
278func TestSchemeHandler_APIVimPageForwardsValidPayloads(t *testing.T) {
279 oldNewResourceHandler := cefNewResourceHandler
280 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
281 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
282
283 tests := []struct {
284 name string
285 body string
286 wantStatus int
287 wantCalled bool
288 }{
289 {"valid", `{"type":"copy","token":"tok","text":"yanked"}`, http.StatusOK, true},
290 {"missing token", `{"type":"copy","text":"yanked"}`, http.StatusBadRequest, false},
291 }
292 for _, tt := range tests {
293 t.Run(tt.name, func(t *testing.T) {
294 h := newTestDumbSchemeHandler(t)
295 browser := cefmocks.NewMockBrowser(t)
296 var received vimPageBridgePayload
297 called := false
298 h.onVimPage = func(_ purecef.Browser, payload vimPageBridgePayload) {
299 called = true
300 received = payload
301 }
302
303 request := cefmocks.NewMockRequest(t)
304 encoded := base64.StdEncoding.EncodeToString([]byte(tt.body))
305 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
306 response := cefmocks.NewMockResponse(t)
307 response.EXPECT().SetStatus(int32(tt.wantStatus)).Once()
308 response.EXPECT().SetStatusText(http.StatusText(tt.wantStatus)).Once()
309 response.EXPECT().SetMimeType("application/json").Once()
310 expectAPIResponseHeaders(response)
311
312 handler := h.handleAPI(browser, http.MethodPost, "/api/vim-page", request)
313 require.NotNil(t, handler)
314 var length int64
315 handler.GetResponseHeaders(response, &length, 0)
316
317 assert.Equal(t, tt.wantCalled, called)
318 if tt.wantCalled {
319 assert.Equal(t, vimPageBridgePayload{Type: "copy", Token: "tok", Text: "yanked"}, received)
320 }
321 })
322 }
323}
324
325func TestWebViewHandleVimPageResultRejectsTypeNotMatchingInteraction(t *testing.T) {
326 var opened string
327 ended := 0
328 wv := newVimResultWebView(&opened, &ended)
329 wv.vimPageToken, wv.vimPageKind = "tok", dto.VimPageYankObject
330
331 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageOpenNew, Token: "tok", URL: "https://example.com"})
332
333 assert.Empty(t, opened)
334 assert.Empty(t, wv.currentVimPageToken(), "the token is still spent")
335}
336
337func TestWebViewVimPageEndedCallbackSkippedAfterRearm(t *testing.T) {
338 var opened string
339 ended := 0
340 wv := newVimResultWebView(&opened, &ended)
341 _, err := wv.armVimPageInteraction(dto.VimPageHintFollow)
342 require.NoError(t, err)
343 _, generation := wv.disarmVimPageInteraction()
344 _, err = wv.armVimPageInteraction(dto.VimPageHintFollow)
345 require.NoError(t, err)
346
347 wv.notifyVimPageInteractionEnded(generation)
348
349 assert.Zero(t, ended)
350}
351
352// handleVimPageResult runs on the CEF IO thread. The clipboard write must be
353// handed to GTK instead of running there.
354func newVimClipboardWebView(t *testing.T, kind dto.VimPageInteractionKind) (*WebView, *recordingClipboardTextOrchestrator, *[]func()) {
355 t.Helper()
356 orchestrator := &recordingClipboardTextOrchestrator{}
357 queued := &[]func(){}
358 wv := &WebView{
359 ctx: context.Background(), id: 7, uri: "https://site.test/",
360 engine: &Engine{clipboardTextOrchestrator: orchestrator},
361 gtkDispatch: func(fn func()) { *queued = append(*queued, fn) },
362 callbacks: &port.WebViewCallbacks{},
363 }
364 wv.vimPageToken, wv.vimPageKind = "tok", kind
365 return wv, orchestrator, queued
366}
367
368func TestWebViewHandleVimPageResultCopyRunsOnGTK(t *testing.T) {
369 wv, orchestrator, queued := newVimClipboardWebView(t, dto.VimPageYankObject)
370
371 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageCopy, Token: "tok", Text: "yanked"})
372
373 assert.Empty(t, orchestrator.explicit.Text, "the clipboard must not be written on the IO thread")
374 require.Len(t, *queued, 1)
375 (*queued)[0]()
376 assert.Equal(t, dto.ExplicitClipboardInput{
377 Text: "yanked", Action: "copy", SourceEngine: dto.ClipboardSourceCEF, ViewID: 7,
378 }, orchestrator.explicit)
379}
380
381// A hostile page that read the token must not be able to write the clipboard
382// again and again: visual copies are non-terminal, so each needs a y/Enter the
383// user pressed and Go forwarded.
384func TestWebViewHandleVimPageResultVisualCopyNeedsForwardedCopyKey(t *testing.T) {
385 wv, orchestrator, queued := newVimClipboardWebView(t, dto.VimPageVisual)
386 copyResult := vimPageBridgePayload{Type: vimPageMessageCopy, Token: "tok", Text: "stolen"}
387
388 wv.handleVimPageResult(copyResult)
389 assert.Empty(t, *queued, "no key was forwarded: the copy is refused")
390 assert.Equal(t, "tok", wv.currentVimPageToken(), "a refused copy leaves the interaction running")
391
392 wv.grantVimPageCopyAllowance("tok", "j")
393 wv.handleVimPageResult(copyResult)
394 assert.Empty(t, *queued, "motion keys grant no copy")
395
396 wv.grantVimPageCopyAllowance("tok", "y")
397 wv.handleVimPageResult(copyResult)
398 wv.handleVimPageResult(copyResult)
399 require.Len(t, *queued, 1, "one forwarded key allows exactly one copy")
400 (*queued)[0]()
401 assert.Equal(t, "stolen", orchestrator.explicit.Text)
402
403 wv.grantVimPageCopyAllowance("tok", "<Return>")
404 wv.grantVimPageCopyAllowance("other", "y")
405 wv.handleVimPageResult(copyResult)
406 assert.Len(t, *queued, 2)
407
408 // y presses the page ignored (caret phase, empty selection) do not bank
409 // copies for later.
410 wv.grantVimPageCopyAllowance("tok", "y")
411 wv.grantVimPageCopyAllowance("tok", "y")
412 wv.grantVimPageCopyAllowance("tok", "y")
413 wv.handleVimPageResult(copyResult)
414 wv.handleVimPageResult(copyResult)
415 assert.Len(t, *queued, 3, "ignored copy keys do not accumulate allowance")
416}
417
418func TestWebViewSendVimPageKeyGrantsCopyAllowanceOnlyForVisual(t *testing.T) {
419 for kind, want := range map[dto.VimPageInteractionKind]int{dto.VimPageVisual: 1, dto.VimPageHintYankURL: 0} {
420 browser := cefmocks.NewMockBrowser(t)
421 frame := cefmocks.NewMockFrame(t)
422 browser.EXPECT().GetMainFrame().Return(frame).Times(3)
423 frame.EXPECT().ExecuteJavaScript(mock.Anything, "", int32(0)).Times(3)
424 wv := &WebView{browser: browser, vimPageToken: "tok", vimPageKind: kind}
425
426 require.NoError(t, wv.SendVimPageKey(context.Background(), "y"))
427 require.NoError(t, wv.SendVimPageKey(context.Background(), "j"))
428 require.NoError(t, wv.SendVimPageKey(context.Background(), "<Return>"))
429
430 assert.Equal(t, want, wv.vimPageCopyAllowance, "kind %d", kind)
431 }
432}
433
434func TestWebViewArmVimPageInteractionResetsCopyAllowance(t *testing.T) {
435 wv := &WebView{vimPageToken: "old", vimPageKind: dto.VimPageVisual, vimPageCopyAllowance: 3}
436
437 _, err := wv.armVimPageInteraction(dto.VimPageVisual)
438 require.NoError(t, err)
439 assert.Zero(t, wv.vimPageCopyAllowance)
440 wv.vimPageCopyAllowance = 2
441 wv.disarmVimPageInteraction()
442 assert.Zero(t, wv.vimPageCopyAllowance)
443}
444
445func TestWebViewHandleVimPageResultIgnoredAfterDestroy(t *testing.T) {
446 wv, orchestrator, queued := newVimClipboardWebView(t, dto.VimPageYankObject)
447 ended := 0
448 wv.callbacks.OnVimPageInteractionEnded = func() { ended++ }
449 wv.destroyed.Store(true)
450
451 wv.handleVimPageResult(vimPageBridgePayload{Type: vimPageMessageCopy, Token: "tok", Text: "late"})
452
453 assert.Empty(t, *queued)
454 assert.Empty(t, orchestrator.explicit.Text)
455 assert.Zero(t, ended)
456}
457
458func TestWebViewDestroyClearsVimPageInteraction(t *testing.T) {
459 wv := &WebView{ctx: context.Background(), vimPageToken: "tok", vimPageKind: dto.VimPageVisual, vimPageCopyAllowance: 1}
460
461 wv.Destroy()
462
463 assert.Empty(t, wv.currentVimPageToken())
464 assert.Zero(t, wv.vimPageKind)
465 assert.Zero(t, wv.vimPageCopyAllowance)
466}
467
468// A crashed renderer takes the page runtime with it, so it can never report
469// the end; the render-process-terminated handler must release the capture.
470func TestHandlerSetOnRenderProcessTerminatedEndsVimPageInteraction(t *testing.T) {
471 var opened string
472 ended := 0
473 wv := newVimResultWebView(&opened, &ended)
474 wv.vimPageToken, wv.vimPageKind = "tok", dto.VimPageVisual
475 h := &handlerSet{wv: wv}
476
477 h.OnRenderProcessTerminated(nil, purecef.TerminationStatusTsProcessCrashed, 0, "")
478
479 assert.Equal(t, 1, ended)
480 assert.Empty(t, wv.currentVimPageToken())
481}
482
483func TestSchemeHandler_APIVimPageRejectsOversizedPayloadWith413(t *testing.T) {
484 oldNewResourceHandler := cefNewResourceHandler
485 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
486 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
487
488 h := newTestDumbSchemeHandler(t)
489 h.onVimPage = func(purecef.Browser, vimPageBridgePayload) { t.Fatal("oversized payload must not reach the handler") }
490 request := cefmocks.NewMockRequest(t)
491 encoded := base64.StdEncoding.EncodeToString([]byte(`{"type":"copy","token":"t","text":"` + strings.Repeat("a", maxClipboardBytes) + `"}`))
492 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
493 response := cefmocks.NewMockResponse(t)
494 response.EXPECT().SetStatus(int32(http.StatusRequestEntityTooLarge)).Once()
495 response.EXPECT().SetStatusText(http.StatusText(http.StatusRequestEntityTooLarge)).Once()
496 response.EXPECT().SetMimeType("application/json").Once()
497 expectAPIResponseHeaders(response)
498
499 handler := h.handleAPI(cefmocks.NewMockBrowser(t), http.MethodPost, "/api/vim-page", request)
500 require.NotNil(t, handler)
501 var length int64
502 handler.GetResponseHeaders(response, &length, 0)
503}
504
505func TestDecodeVimPageBridgePayloadOversizedIsSentinel(t *testing.T) {
506 _, err := decodeVimPageBridgePayload(make([]byte, maxClipboardBytes+1))
507 require.ErrorIs(t, err, errVimPagePayloadTooLarge)
508 _, err = decodeVimPageBridgePayload([]byte(`{`))
509 require.Error(t, err)
510 require.NotErrorIs(t, err, errVimPagePayloadTooLarge)
511}