v0.32.0 0617278

scheme_handler_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close