1package cef
2
3import (
4 "context"
5 "encoding/base64"
6 "errors"
7 "net/http"
8 "testing"
9
10 purecef "github.com/bnema/purego-cef/cef"
11 cefmocks "github.com/bnema/purego-cef/cef/mocks"
12 "github.com/stretchr/testify/require"
13
14 "github.com/bnema/dumber/internal/application/port"
15)
16
17func expectAPIResponseHeaders(response *cefmocks.MockResponse) {
18 response.EXPECT().SetHeaderByName("Access-Control-Allow-Origin", "*", int32(1)).Once()
19 response.EXPECT().SetHeaderByName("Access-Control-Allow-Methods", "GET, POST, OPTIONS", int32(1)).Once()
20 response.EXPECT().SetHeaderByName(
21 "Access-Control-Allow-Headers",
22 "Content-Type, X-Dumber-Body, X-Dumber-Bridge-Action, X-Dumber-Bridge-Nonce",
23 int32(1),
24 ).Once()
25 response.EXPECT().SetHeaderByName("Access-Control-Max-Age", "86400", int32(1)).Once()
26 response.EXPECT().SetHeaderByName("Cache-Control", "no-store", int32(1)).Once()
27}
28
29func expectPrivateAPIResponseHeaders(response *cefmocks.MockResponse) {
30 response.EXPECT().SetHeaderByName("Cache-Control", "no-store", int32(1)).Once()
31}
32
33type panicSystemviewFaviconResolver struct{}
34
35func (panicSystemviewFaviconResolver) ResolveSystemviewIcon(context.Context, string, int) (*port.ResolvedFavicon, error) {
36 panic("unexpected favicon lookup")
37}
38
39// noopConfigPayload returns a builder that always returns an empty JSON object.
40func noopConfigPayload() func() ([]byte, error) {
41 return func() ([]byte, error) { return []byte(`{}`), nil }
42}
43
44// newTestDumbSchemeHandler creates a dumbSchemeHandler with noop config payloads.
45// It calls t.Helper() and fails the test if construction errors.
46func newTestDumbSchemeHandler(t *testing.T) *dumbSchemeHandler {
47 t.Helper()
48 h, err := newDumbSchemeHandler(context.Background(), nil, noopConfigPayload(), noopConfigPayload())
49 require.NoError(t, err)
50 return h
51}
52
53func TestResolveConfigPayload_UsesInjectedBuilder(t *testing.T) {
54 data, err := resolveConfigPayload(func() ([]byte, error) {
55 return []byte(`{"engine_type":"cef"}`), nil
56 })
57 require.NoError(t, err)
58 require.JSONEq(t, `{"engine_type":"cef"}`, string(data))
59}
60
61func TestResolveConfigPayload_PropagatesBuilderError(t *testing.T) {
62 want := errors.New("boom")
63 _, err := resolveConfigPayload(func() ([]byte, error) {
64 return nil, want
65 })
66 require.ErrorIs(t, err, want)
67}
68
69func TestResolveConfigPayload_NilBuilderFails(t *testing.T) {
70 _, err := resolveConfigPayload(nil)
71 require.Error(t, err)
72 require.Contains(t, err.Error(), "config payload builder")
73}
74
75func TestNewDumbSchemeHandler_NilCurrentConfigPayloadFails(t *testing.T) {
76 h, err := newDumbSchemeHandler(context.Background(), nil, nil, func() ([]byte, error) { return nil, nil })
77 require.Error(t, err)
78 require.Nil(t, h)
79 require.Contains(t, err.Error(), "current config payload builder")
80}
81
82func TestNewDumbSchemeHandler_NilDefaultConfigPayloadFails(t *testing.T) {
83 h, err := newDumbSchemeHandler(context.Background(), nil, func() ([]byte, error) { return nil, nil }, nil)
84 require.Error(t, err)
85 require.Nil(t, h)
86 require.Contains(t, err.Error(), "default config payload builder")
87}
88
89func TestRejectForbiddenAPIOrigin_AllowsInternalOrigin(t *testing.T) {
90 h := newTestDumbSchemeHandler(t)
91
92 request := cefmocks.NewMockRequest(t)
93 request.EXPECT().GetHeaderByName("Origin").Return(actualInternalOrigin).Once()
94
95 require.Nil(t, h.rejectForbiddenAPIOrigin(request))
96}
97
98func TestRejectForbiddenAPIOrigin_RejectsExternalOrigin(t *testing.T) {
99 oldNewResourceHandler := cefNewResourceHandler
100 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
101 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
102
103 h := newTestDumbSchemeHandler(t)
104
105 request := cefmocks.NewMockRequest(t)
106 request.EXPECT().GetHeaderByName("Origin").Return("https://evil.example").Once()
107
108 response := cefmocks.NewMockResponse(t)
109 response.EXPECT().SetStatus(int32(http.StatusForbidden)).Once()
110 response.EXPECT().SetStatusText(http.StatusText(http.StatusForbidden)).Once()
111 response.EXPECT().SetMimeType("application/json").Once()
112 expectAPIResponseHeaders(response)
113
114 handler := h.rejectForbiddenAPIOrigin(request)
115 require.NotNil(t, handler)
116 var responseLength int64
117 handler.GetResponseHeaders(response, &responseLength, 0)
118 require.Positive(t, responseLength)
119}
120
121func TestRejectUntrustedConfigRequesterRequiresTrustedOriginOrReferrer(t *testing.T) {
122 oldNewResourceHandler := cefNewResourceHandler
123 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
124 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
125
126 h := newTestDumbSchemeHandler(t)
127
128 trustedOrigin := cefmocks.NewMockRequest(t)
129 trustedOrigin.EXPECT().GetHeaderByName("Origin").Return(actualInternalOrigin).Once()
130 require.Nil(t, h.rejectUntrustedConfigRequester(trustedOrigin))
131
132 trustedReferrer := cefmocks.NewMockRequest(t)
133 trustedReferrer.EXPECT().GetHeaderByName("Origin").Return("").Once()
134 trustedReferrer.EXPECT().GetReferrerURL().Return("dumb://history").Once()
135 require.Nil(t, h.rejectUntrustedConfigRequester(trustedReferrer))
136
137 emptyContext := cefmocks.NewMockRequest(t)
138 emptyContext.EXPECT().GetHeaderByName("Origin").Return("").Once()
139 emptyContext.EXPECT().GetReferrerURL().Return("").Once()
140 emptyContext.EXPECT().GetHeaderByName("Referer").Return("").Once()
141 denied := h.rejectUntrustedConfigRequester(emptyContext)
142 require.NotNil(t, denied)
143
144 response := cefmocks.NewMockResponse(t)
145 response.EXPECT().SetStatus(int32(http.StatusForbidden)).Once()
146 response.EXPECT().SetStatusText(http.StatusText(http.StatusForbidden)).Once()
147 response.EXPECT().SetMimeType("application/json").Once()
148 expectPrivateAPIResponseHeaders(response)
149 var responseLength int64
150 denied.GetResponseHeaders(response, &responseLength, 0)
151 require.Positive(t, responseLength)
152
153 untrustedOrigin := cefmocks.NewMockRequest(t)
154 untrustedOrigin.EXPECT().GetHeaderByName("Origin").Return("https://evil.example").Once()
155 require.NotNil(t, h.rejectUntrustedConfigRequester(untrustedOrigin))
156}
157
158func TestHandleMessageAPIRequiresTrustedOriginOrReferrer(t *testing.T) {
159 oldNewResourceHandler := cefNewResourceHandler
160 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
161 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
162
163 h, err := newDumbSchemeHandler(context.Background(), NewMessageRouter(context.Background()), noopConfigPayload(), noopConfigPayload())
164 require.NoError(t, err)
165
166 trusted := cefmocks.NewMockRequest(t)
167 trusted.EXPECT().GetHeaderByName("Origin").Return("").Once()
168 trusted.EXPECT().GetReferrerURL().Return("dumb://history").Once()
169 trusted.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(base64.StdEncoding.EncodeToString([]byte(`{"type":"missing"}`))).Once()
170 trustedHandler := h.handleMessageAPI(trusted)
171 require.NotNil(t, trustedHandler)
172
173 untrusted := cefmocks.NewMockRequest(t)
174 untrusted.EXPECT().GetHeaderByName("Origin").Return("").Once()
175 untrusted.EXPECT().GetReferrerURL().Return("").Once()
176 untrusted.EXPECT().GetHeaderByName("Referer").Return("").Once()
177 denied := h.handleMessageAPI(untrusted)
178 require.NotNil(t, denied)
179
180 response := cefmocks.NewMockResponse(t)
181 response.EXPECT().SetStatus(int32(http.StatusForbidden)).Once()
182 response.EXPECT().SetStatusText(http.StatusText(http.StatusForbidden)).Once()
183 response.EXPECT().SetMimeType("application/json").Once()
184 expectPrivateAPIResponseHeaders(response)
185 var responseLength int64
186 denied.GetResponseHeaders(response, &responseLength, 0)
187 require.Positive(t, responseLength)
188}
189
190func TestHandleConfigAPIUsesPrivateNoCORSHeaders(t *testing.T) {
191 oldNewResourceHandler := cefNewResourceHandler
192 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
193 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
194
195 h, err := newDumbSchemeHandler(context.Background(), nil, func() ([]byte, error) { return []byte(`{"ok":true}`), nil }, noopConfigPayload())
196 require.NoError(t, err)
197
198 response := cefmocks.NewMockResponse(t)
199 response.EXPECT().SetStatus(int32(http.StatusOK)).Once()
200 response.EXPECT().SetStatusText(http.StatusText(http.StatusOK)).Once()
201 response.EXPECT().SetMimeType("application/json").Once()
202 expectPrivateAPIResponseHeaders(response)
203
204 handler := h.handleConfigAPI(h.currentConfigPayload)
205 var responseLength int64
206 handler.GetResponseHeaders(response, &responseLength, 0)
207 require.Positive(t, responseLength)
208}
209
210func TestHandleConfigAPIOptionsUsesPrivateNoCORSHeaders(t *testing.T) {
211 oldNewResourceHandler := cefNewResourceHandler
212 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
213 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
214
215 h := newTestDumbSchemeHandler(t)
216
217 request := cefmocks.NewMockRequest(t)
218 request.EXPECT().GetHeaderByName("Origin").Return(actualInternalOrigin).Once()
219
220 response := cefmocks.NewMockResponse(t)
221 response.EXPECT().SetStatus(int32(http.StatusNoContent)).Once()
222 response.EXPECT().SetStatusText(http.StatusText(http.StatusNoContent)).Once()
223 response.EXPECT().SetMimeType("text/plain").Once()
224 response.EXPECT().SetCharset("utf-8").Once()
225 expectPrivateAPIResponseHeaders(response)
226
227 handler := h.handleAPI(nil, http.MethodOptions, "/api/config", request)
228 var responseLength int64
229 handler.GetResponseHeaders(response, &responseLength, 0)
230 require.Zero(t, responseLength)
231}
232
233func TestIsTrustedSystemviewURL(t *testing.T) {
234 t.Parallel()
235
236 for _, raw := range []string{
237 "https://dumber.invalid/history",
238 "dumb://history",
239 "dumb:history",
240 } {
241 require.True(t, isTrustedSystemviewURL(raw), raw)
242 }
243 for _, raw := range []string{
244 "",
245 "https://evil.example/history",
246 "dumb://api/favicon",
247 "dumb://evil/history",
248 } {
249 require.False(t, isTrustedSystemviewURL(raw), raw)
250 }
251}
252
253func TestRejectUntrustedFaviconRequesterAllowsTrustedOriginWithoutReferrer(t *testing.T) {
254 h := newTestDumbSchemeHandler(t)
255
256 request := cefmocks.NewMockRequest(t)
257 request.EXPECT().GetHeaderByName("Origin").Return(actualInternalOrigin).Once()
258
259 require.Nil(t, h.rejectUntrustedFaviconRequester(request))
260}
261
262func TestRejectUntrustedFaviconRequesterRejectsUntrustedOriginEvenWithTrustedReferrer(t *testing.T) {
263 oldNewResourceHandler := cefNewResourceHandler
264 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
265 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
266
267 h := newTestDumbSchemeHandler(t)
268
269 request := cefmocks.NewMockRequest(t)
270 request.EXPECT().GetHeaderByName("Origin").Return("https://evil.example").Once()
271
272 require.NotNil(t, h.rejectUntrustedFaviconRequester(request))
273}
274
275func TestRejectUntrustedFaviconRequesterRequiresTrustedReferrerWhenOriginAbsent(t *testing.T) {
276 oldNewResourceHandler := cefNewResourceHandler
277 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
278 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
279
280 h := newTestDumbSchemeHandler(t)
281
282 trusted := cefmocks.NewMockRequest(t)
283 trusted.EXPECT().GetHeaderByName("Origin").Return("").Once()
284 trusted.EXPECT().GetReferrerURL().Return("dumb://history").Once()
285 require.Nil(t, h.rejectUntrustedFaviconRequester(trusted))
286
287 untrusted := cefmocks.NewMockRequest(t)
288 untrusted.EXPECT().GetHeaderByName("Origin").Return("").Once()
289 untrusted.EXPECT().GetReferrerURL().Return("").Once()
290 untrusted.EXPECT().GetHeaderByName("Referer").Return("").Once()
291 require.NotNil(t, h.rejectUntrustedFaviconRequester(untrusted))
292}
293
294func TestHandleFaviconAPIDefersFaviconDiskChecksUntilResourceOpen(t *testing.T) {
295 oldNewResourceHandler := cefNewResourceHandler
296 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
297 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
298
299 h := newTestDumbSchemeHandler(t)
300 h.setFaviconResolver(panicSystemviewFaviconResolver{})
301
302 request := cefmocks.NewMockRequest(t)
303 request.EXPECT().GetHeaderByName("Origin").Return(actualInternalOrigin).Once()
304 request.EXPECT().GetURL().Return(actualInternalOrigin + "/api/favicon?domain=example.com&size=32").Once()
305
306 require.NotNil(t, h.handleFaviconAPI(request))
307}
308
309func TestReadBodyFromHeader_DecodesBase64Payload(t *testing.T) {
310 request := cefmocks.NewMockRequest(t)
311 encoded := base64.StdEncoding.EncodeToString([]byte(`{"text":"copied from js"}`))
312 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
313
314 body := readBodyFromHeader(request)
315
316 require.JSONEq(t, `{"text":"copied from js"}`, string(body))
317}
318
319func TestDecodePopupOpenerPostMessagePayload_RejectsMissingTargetOrigin(t *testing.T) {
320 _, err := decodePopupOpenerPostMessagePayload([]byte(`{"data":"{}","data_kind":"json","target_origin":" "}`))
321 require.Error(t, err)
322 require.Contains(t, err.Error(), "target origin")
323}
324
325func TestDecodePopupOpenerPostMessagePayload_TrimsAndAcceptsTargetOrigin(t *testing.T) {
326 payload, err := decodePopupOpenerPostMessagePayload([]byte(`{"data":"{}","data_kind":"json","target_origin":" https://example.com ","source_origin":" https://popup.example.com ","source_href":" https://popup.example.com/callback "}`))
327 require.NoError(t, err)
328 require.Equal(t, "https://example.com", payload.TargetOrigin)
329 require.Equal(t, "https://popup.example.com", payload.SourceOrigin)
330 require.Equal(t, "https://popup.example.com/callback", payload.SourceHref)
331}
332
333func TestSchemeHandler_APIClipboardSetPathWritesClipboardPayload(t *testing.T) {
334 oldNewResourceHandler := cefNewResourceHandler
335 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
336 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
337
338 h := newTestDumbSchemeHandler(t)
339
340 const bridgeNonce = "bridge-nonce"
341 browser := cefmocks.NewMockBrowser(t)
342 h.bridgeNonceValidator = func(got purecef.Browser, gotNonce string) bool {
343 return got == browser && gotNonce == bridgeNonce
344 }
345
346 var copied string
347 h.onClipboardSet = func(text string) { copied = text }
348
349 request := cefmocks.NewMockRequest(t)
350 encoded := base64.StdEncoding.EncodeToString([]byte(`{"text":"copied from js"}`))
351 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return(bridgeNonce).Once()
352 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
353
354 response := cefmocks.NewMockResponse(t)
355 response.EXPECT().SetStatus(int32(http.StatusOK)).Once()
356 response.EXPECT().SetStatusText(http.StatusText(http.StatusOK)).Once()
357 response.EXPECT().SetMimeType("application/json").Once()
358 expectAPIResponseHeaders(response)
359
360 handler := h.handleAPI(browser, http.MethodPost, "/api/clipboard-set", request)
361 require.NotNil(t, handler)
362
363 var responseLength int64
364 handler.GetResponseHeaders(response, &responseLength, 0)
365 require.Positive(t, responseLength)
366 require.Equal(t, "copied from js", copied)
367}
368
369func TestSchemeHandler_APIFocusSyncRejectsRequestsWithoutTrustedBridgeHeader(t *testing.T) {
370 oldNewResourceHandler := cefNewResourceHandler
371 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
372 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
373
374 h := newTestDumbSchemeHandler(t)
375
376 request := cefmocks.NewMockRequest(t)
377 request.EXPECT().GetHeaderByName(dumberBridgeActionHeaderName).Return("").Once()
378
379 response := cefmocks.NewMockResponse(t)
380 response.EXPECT().SetStatus(int32(http.StatusForbidden)).Once()
381 response.EXPECT().SetStatusText(http.StatusText(http.StatusForbidden)).Once()
382 response.EXPECT().SetMimeType("application/json").Once()
383 expectAPIResponseHeaders(response)
384
385 handler := h.handleAPI(nil, http.MethodPost, "/api/focus-sync", request)
386 require.NotNil(t, handler)
387
388 var responseLength int64
389 handler.GetResponseHeaders(response, &responseLength, 0)
390 require.Positive(t, responseLength)
391}
392
393func TestSchemeHandler_APIFocusSyncInvokesEditableFocusCallback(t *testing.T) {
394 oldNewResourceHandler := cefNewResourceHandler
395 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
396 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
397
398 h := newTestDumbSchemeHandler(t)
399
400 const bridgeNonce = "bridge-nonce"
401 browser := cefmocks.NewMockBrowser(t)
402 h.bridgeNonceValidator = func(got purecef.Browser, gotNonce string) bool {
403 return got == browser && gotNonce == bridgeNonce
404 }
405
406 var focused purecef.Browser
407 h.onEditableFocus = func(got purecef.Browser) { focused = got }
408
409 request := cefmocks.NewMockRequest(t)
410 request.EXPECT().GetHeaderByName(dumberBridgeActionHeaderName).Return(dumberBridgeActionFocusSync).Once()
411 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return(bridgeNonce).Once()
412
413 response := cefmocks.NewMockResponse(t)
414 response.EXPECT().SetStatus(int32(http.StatusOK)).Once()
415 response.EXPECT().SetStatusText(http.StatusText(http.StatusOK)).Once()
416 response.EXPECT().SetMimeType("application/json").Once()
417 expectAPIResponseHeaders(response)
418
419 handler := h.handleAPI(browser, http.MethodPost, "/api/focus-sync", request)
420 require.NotNil(t, handler)
421
422 var responseLength int64
423 handler.GetResponseHeaders(response, &responseLength, 0)
424 require.Positive(t, responseLength)
425 require.Same(t, browser, focused)
426}
427
428func TestSchemeHandler_Create_ConceptualAPIRequestBypassesRedirect(t *testing.T) {
429 oldNewResourceHandler := cefNewResourceHandler
430 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
431 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
432
433 h := newTestDumbSchemeHandler(t)
434
435 const bridgeNonce = "bridge-nonce"
436 browser := cefmocks.NewMockBrowser(t)
437 h.bridgeNonceValidator = func(got purecef.Browser, gotNonce string) bool {
438 return got == browser && gotNonce == bridgeNonce
439 }
440
441 var copied string
442 h.onClipboardSet = func(text string) { copied = text }
443
444 request := cefmocks.NewMockRequest(t)
445 request.EXPECT().GetURL().Return("dumb://api/clipboard-set").Once()
446 request.EXPECT().GetMethod().Return(http.MethodPost).Once()
447 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return(bridgeNonce).Once()
448 encoded := base64.StdEncoding.EncodeToString([]byte(`{"text":"copied from create"}`))
449 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
450
451 response := cefmocks.NewMockResponse(t)
452 response.EXPECT().SetStatus(int32(http.StatusOK)).Once()
453 response.EXPECT().SetStatusText(http.StatusText(http.StatusOK)).Once()
454 response.EXPECT().SetMimeType("application/json").Once()
455 expectAPIResponseHeaders(response)
456
457 handler := h.Create(browser, nil, "", request)
458 require.NotNil(t, handler)
459
460 var responseLength int64
461 handler.GetResponseHeaders(response, &responseLength, 0)
462 require.Positive(t, responseLength)
463 require.Equal(t, "copied from create", copied)
464}
465
466func TestSchemeHandler_APIClipboardSetRejectsInvalidBridgeNonce(t *testing.T) {
467 oldNewResourceHandler := cefNewResourceHandler
468 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
469 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
470
471 h := newTestDumbSchemeHandler(t)
472
473 browser := cefmocks.NewMockBrowser(t)
474 h.bridgeNonceValidator = func(_ purecef.Browser, _ string) bool { return false }
475
476 request := cefmocks.NewMockRequest(t)
477 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return("invalid").Once()
478
479 response := cefmocks.NewMockResponse(t)
480 response.EXPECT().SetStatus(int32(http.StatusForbidden)).Once()
481 response.EXPECT().SetStatusText(http.StatusText(http.StatusForbidden)).Once()
482 response.EXPECT().SetMimeType("application/json").Once()
483 expectAPIResponseHeaders(response)
484
485 handler := h.handleAPI(browser, http.MethodPost, "/api/clipboard-set", request)
486 require.NotNil(t, handler)
487
488 var responseLength int64
489 handler.GetResponseHeaders(response, &responseLength, 0)
490 require.Positive(t, responseLength)
491}
492
493func TestSchemeHandler_APIPopupOpenInvokesPopupCallback(t *testing.T) {
494 oldNewResourceHandler := cefNewResourceHandler
495 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
496 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
497
498 h := newTestDumbSchemeHandler(t)
499
500 const bridgeNonce = "bridge-nonce"
501 browser := cefmocks.NewMockBrowser(t)
502 h.bridgeNonceValidator = func(got purecef.Browser, gotNonce string) bool {
503 return got == browser && gotNonce == bridgeNonce
504 }
505
506 var got rendererBridgePopupOpenPayload
507 h.onPopupOpen = func(gotBrowser purecef.Browser, payload rendererBridgePopupOpenPayload) {
508 require.Same(t, browser, gotBrowser)
509 got = payload
510 }
511
512 request := cefmocks.NewMockRequest(t)
513 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return(bridgeNonce).Once()
514 encoded := base64.StdEncoding.EncodeToString([]byte(`{"proxy_id":"popup-1","url":"https://example.com/login","frame_name":"Google login","user_gesture":true,"no_javascript_access":true}`))
515 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
516
517 response := cefmocks.NewMockResponse(t)
518 response.EXPECT().SetStatus(int32(http.StatusOK)).Once()
519 response.EXPECT().SetStatusText(http.StatusText(http.StatusOK)).Once()
520 response.EXPECT().SetMimeType("application/json").Once()
521 expectAPIResponseHeaders(response)
522
523 handler := h.handleAPI(browser, http.MethodPost, "/api/popup-open", request)
524 require.NotNil(t, handler)
525
526 var responseLength int64
527 handler.GetResponseHeaders(response, &responseLength, 0)
528 require.Positive(t, responseLength)
529 require.Equal(t, "popup-1", got.ProxyID)
530 require.Equal(t, "https://example.com/login", got.URL)
531 require.Equal(t, "Google login", got.FrameName)
532 require.True(t, got.UserGesture)
533 require.True(t, got.NoJavaScriptAccess)
534}
535
536func TestSchemeHandler_APIPopupNavigateInvokesPopupCallback(t *testing.T) {
537 oldNewResourceHandler := cefNewResourceHandler
538 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
539 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
540
541 h := newTestDumbSchemeHandler(t)
542
543 const bridgeNonce = "bridge-nonce"
544 browser := cefmocks.NewMockBrowser(t)
545 h.bridgeNonceValidator = func(got purecef.Browser, gotNonce string) bool {
546 return got == browser && gotNonce == bridgeNonce
547 }
548
549 var got rendererBridgePopupNavigatePayload
550 h.onPopupNavigate = func(gotBrowser purecef.Browser, payload rendererBridgePopupNavigatePayload) {
551 require.Same(t, browser, gotBrowser)
552 got = payload
553 }
554
555 request := cefmocks.NewMockRequest(t)
556 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return(bridgeNonce).Once()
557 encoded := base64.StdEncoding.EncodeToString([]byte(`{"proxy_id":"popup-1","url":"https://example.com/callback"}`))
558 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
559
560 response := cefmocks.NewMockResponse(t)
561 response.EXPECT().SetStatus(int32(http.StatusOK)).Once()
562 response.EXPECT().SetStatusText(http.StatusText(http.StatusOK)).Once()
563 response.EXPECT().SetMimeType("application/json").Once()
564 expectAPIResponseHeaders(response)
565
566 handler := h.handleAPI(browser, http.MethodPost, "/api/popup-navigate", request)
567 require.NotNil(t, handler)
568
569 var responseLength int64
570 handler.GetResponseHeaders(response, &responseLength, 0)
571 require.Positive(t, responseLength)
572 require.Equal(t, "popup-1", got.ProxyID)
573 require.Equal(t, "https://example.com/callback", got.URL)
574}
575
576func TestSchemeHandler_APIPopupCloseInvokesPopupCallback(t *testing.T) {
577 oldNewResourceHandler := cefNewResourceHandler
578 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
579 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
580
581 h := newTestDumbSchemeHandler(t)
582
583 const bridgeNonce = "bridge-nonce"
584 browser := cefmocks.NewMockBrowser(t)
585 h.bridgeNonceValidator = func(got purecef.Browser, gotNonce string) bool {
586 return got == browser && gotNonce == bridgeNonce
587 }
588
589 var got rendererBridgePopupClosePayload
590 h.onPopupClose = func(gotBrowser purecef.Browser, payload rendererBridgePopupClosePayload) {
591 require.Same(t, browser, gotBrowser)
592 got = payload
593 }
594
595 request := cefmocks.NewMockRequest(t)
596 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return(bridgeNonce).Once()
597 encoded := base64.StdEncoding.EncodeToString([]byte(`{"proxy_id":"popup-1"}`))
598 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
599
600 response := cefmocks.NewMockResponse(t)
601 response.EXPECT().SetStatus(int32(http.StatusOK)).Once()
602 response.EXPECT().SetStatusText(http.StatusText(http.StatusOK)).Once()
603 response.EXPECT().SetMimeType("application/json").Once()
604 expectAPIResponseHeaders(response)
605
606 handler := h.handleAPI(browser, http.MethodPost, "/api/popup-close", request)
607 require.NotNil(t, handler)
608
609 var responseLength int64
610 handler.GetResponseHeaders(response, &responseLength, 0)
611 require.Positive(t, responseLength)
612 require.Equal(t, "popup-1", got.ProxyID)
613}
614
615func TestSchemeHandler_APIPopupBridgeRejectsInvalidPayloads(t *testing.T) {
616 oldNewResourceHandler := cefNewResourceHandler
617 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
618 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
619
620 tests := []struct {
621 name string
622 path string
623 body string
624 wire func(*dumbSchemeHandler, *bool)
625 }{
626 {
627 name: "open malformed json",
628 path: "/api/popup-open",
629 body: `{invalid`,
630 wire: func(h *dumbSchemeHandler, called *bool) {
631 h.onPopupOpen = func(purecef.Browser, rendererBridgePopupOpenPayload) { *called = true }
632 },
633 },
634 {
635 name: "open missing proxy id",
636 path: "/api/popup-open",
637 body: `{"url":"https://example.com/login"}`,
638 wire: func(h *dumbSchemeHandler, called *bool) {
639 h.onPopupOpen = func(purecef.Browser, rendererBridgePopupOpenPayload) { *called = true }
640 },
641 },
642 {
643 name: "navigate malformed json",
644 path: "/api/popup-navigate",
645 body: `{invalid`,
646 wire: func(h *dumbSchemeHandler, called *bool) {
647 h.onPopupNavigate = func(purecef.Browser, rendererBridgePopupNavigatePayload) { *called = true }
648 },
649 },
650 {
651 name: "navigate missing proxy id",
652 path: "/api/popup-navigate",
653 body: `{"url":"https://example.com/callback"}`,
654 wire: func(h *dumbSchemeHandler, called *bool) {
655 h.onPopupNavigate = func(purecef.Browser, rendererBridgePopupNavigatePayload) { *called = true }
656 },
657 },
658 {
659 name: "close malformed json",
660 path: "/api/popup-close",
661 body: `{invalid`,
662 wire: func(h *dumbSchemeHandler, called *bool) {
663 h.onPopupClose = func(purecef.Browser, rendererBridgePopupClosePayload) { *called = true }
664 },
665 },
666 {
667 name: "close missing proxy id",
668 path: "/api/popup-close",
669 body: `{"url":"https://example.com/callback"}`,
670 wire: func(h *dumbSchemeHandler, called *bool) {
671 h.onPopupClose = func(purecef.Browser, rendererBridgePopupClosePayload) { *called = true }
672 },
673 },
674 }
675
676 for _, test := range tests {
677 t.Run(test.name, func(t *testing.T) {
678 h := newTestDumbSchemeHandler(t)
679
680 const bridgeNonce = "bridge-nonce"
681 browser := cefmocks.NewMockBrowser(t)
682 h.bridgeNonceValidator = func(got purecef.Browser, gotNonce string) bool {
683 return got == browser && gotNonce == bridgeNonce
684 }
685
686 called := false
687 test.wire(h, &called)
688
689 request := cefmocks.NewMockRequest(t)
690 request.EXPECT().GetHeaderByName(dumberBridgeNonceHeaderName).Return(bridgeNonce).Once()
691 encoded := base64.StdEncoding.EncodeToString([]byte(test.body))
692 request.EXPECT().GetHeaderByName(dumberBodyHeaderName).Return(encoded).Once()
693
694 response := cefmocks.NewMockResponse(t)
695 response.EXPECT().SetStatus(int32(http.StatusBadRequest)).Once()
696 response.EXPECT().SetStatusText(http.StatusText(http.StatusBadRequest)).Once()
697 response.EXPECT().SetMimeType("application/json").Once()
698 expectAPIResponseHeaders(response)
699
700 handler := h.handleAPI(browser, http.MethodPost, test.path, request)
701 require.NotNil(t, handler)
702
703 var responseLength int64
704 handler.GetResponseHeaders(response, &responseLength, 0)
705 require.Positive(t, responseLength)
706 require.False(t, called)
707 })
708 }
709}
710
711func TestSchemeHandler_APIOptionsReturnsCORSPreflightHeaders(t *testing.T) {
712 oldNewResourceHandler := cefNewResourceHandler
713 cefNewResourceHandler = func(impl purecef.ResourceHandler) purecef.ResourceHandler { return impl }
714 defer func() { cefNewResourceHandler = oldNewResourceHandler }()
715
716 h := newTestDumbSchemeHandler(t)
717
718 request := cefmocks.NewMockRequest(t)
719 response := cefmocks.NewMockResponse(t)
720 response.EXPECT().SetStatus(int32(http.StatusNoContent)).Once()
721 response.EXPECT().SetStatusText(http.StatusText(http.StatusNoContent)).Once()
722 response.EXPECT().SetMimeType("text/plain").Once()
723 response.EXPECT().SetCharset("utf-8").Once()
724 expectAPIResponseHeaders(response)
725
726 handler := h.handleAPI(nil, http.MethodOptions, "/api/clipboard-set", request)
727 require.NotNil(t, handler)
728
729 var responseLength int64
730 handler.GetResponseHeaders(response, &responseLength, 0)
731 require.Zero(t, responseLength)
732}