v0.29.0 21b3150

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
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}

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