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