v0.4.0 76a5504

rpc_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package host
4
5import (
6	"context"
7	"net/http"
8	"net/http/httptest"
9	"strings"
10	"sync/atomic"
11	"testing"
12
13	"connectrpc.com/connect"
14
15	ermite "bnema.dev/ermite-api/gen/ermite"
16)
17
18func TestCheckURL(t *testing.T) {
19	for _, tc := range []struct {
20		url      string
21		insecure bool
22		want     string // "" = accepted
23	}{
24		{"https://ermite.example.org", false, ""},
25		{"https://ermite.example.org:8443/base", false, ""},
26		{"http://localhost:8080", false, ""},
27		{"http://LOCALHOST:8080", false, ""},
28		{"http://127.0.0.1:8080", false, ""},
29		{"http://[::1]:8080", false, ""},
30		{"http://ermite.example.org", false, "must be https"},
31		{"http://10.0.0.5:8080", false, "must be https"},
32		{"http://localhost.evil.example", false, "must be https"},
33		{"http://127.0.0.1.evil.example", false, "must be https"},
34		{"http://ermite.example.org", true, ""},
35		{"ftp://ermite.example.org", false, "scheme must be https"},
36		{"ermite.example.org", false, "not a valid URL"},
37		{"https://user:pw@ermite.example.org", false, "credentials"},
38		{"", false, "not a valid URL"},
39	} {
40		err := CheckURL(tc.url, tc.insecure)
41		if tc.want == "" {
42			if err != nil {
43				t.Errorf("%q: %v", tc.url, err)
44			}
45			continue
46		}
47		if err == nil || !strings.Contains(err.Error(), tc.want) {
48			t.Errorf("%q (insecure=%v): %v, want %q", tc.url, tc.insecure, err, tc.want)
49		}
50	}
51}
52
53// The client does not follow a redirect: the token must not reach the target,
54// whoever it is.
55func TestClientDoesNotFollowRedirects(t *testing.T) {
56	var hits atomic.Int32
57	var leaked atomic.Value
58	target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59		hits.Add(1)
60		leaked.Store(r.Header.Get("Authorization"))
61		w.WriteHeader(200)
62	}))
63	defer target.Close()
64	redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65		http.Redirect(w, r, target.URL+"/stolen", http.StatusTemporaryRedirect)
66	}))
67	defer redirector.Close()
68
69	c := NewClient(redirector.URL, "ermiterun_secret", nil)
70	_, err := c.FetchJob(context.Background(), connect.NewRequest(&ermite.FetchJobRequest{}))
71	if err == nil {
72		t.Fatal("a redirect answered a call")
73	}
74	if hits.Load() != 0 {
75		t.Fatalf("the redirect was followed (%d hits), the token was sent to the target: %v", hits.Load(), leaked.Load())
76	}
77}
78
79// The token is not sent over plain http to a host that is not loopback.
80func TestClientRefusesToSendTheTokenInClear(t *testing.T) {
81	var hits atomic.Int32
82	rt := roundTripFunc(func(*http.Request) (*http.Response, error) { hits.Add(1); return nil, nil })
83	c := NewClient("http://ermite.example.org", "ermiterun_secret", rt)
84	_, err := c.FetchJob(context.Background(), connect.NewRequest(&ermite.FetchJobRequest{}))
85	if err == nil || !strings.Contains(err.Error(), "must be https") || hits.Load() != 0 {
86		t.Fatalf("hits=%d err=%v", hits.Load(), err)
87	}
88	// The explicit flag allows it.
89	c = NewInsecureClient("http://ermite.example.org", "ermiterun_secret", rt)
90	_, _ = c.FetchJob(context.Background(), connect.NewRequest(&ermite.FetchJobRequest{}))
91	if hits.Load() != 1 {
92		t.Fatal("the insecure client did not send the request")
93	}
94}
95
96// The API lives under /-/api of the base URL, which may carry a path prefix
97// and a trailing slash.
98func TestClientCallsTheAPIPath(t *testing.T) {
99	for base, want := range map[string]string{
100		"https://h":         "/-/api/ermite.RunnerService/FetchJob",
101		"https://h/":        "/-/api/ermite.RunnerService/FetchJob",
102		"https://h/ermite":  "/ermite/-/api/ermite.RunnerService/FetchJob",
103		"https://h/ermite/": "/ermite/-/api/ermite.RunnerService/FetchJob",
104	} {
105		var got string
106		rt := roundTripFunc(func(r *http.Request) (*http.Response, error) {
107			got = r.URL.Path
108			return nil, context.Canceled
109		})
110		_, _ = NewClient(base, "ermiterun_secret", rt).FetchJob(context.Background(), connect.NewRequest(&ermite.FetchJobRequest{}))
111		if got != want {
112			t.Errorf("%s: called %q, want %q", base, got, want)
113		}
114	}
115}
116
117type roundTripFunc func(*http.Request) (*http.Response, error)
118
119func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
120
121func TestLoadConfigChecksTheURL(t *testing.T) {
122	env := map[string]string{EnvURL: "http://ermite.example.org"}
123	get := func(k string) string { return env[k] }
124	if _, err := LoadConfig(get, "h", "/s", 1, ""); err == nil || !strings.Contains(err.Error(), EnvURL) {
125		t.Fatalf("%v", err)
126	}
127	env[EnvInsecureHTTP] = "1"
128	c, err := LoadConfig(get, "h", "/s", 1, "")
129	if err != nil || !c.InsecureHTTP {
130		t.Fatalf("%+v %v", c, err)
131	}
132	env[EnvInsecureHTTP] = "maybe"
133	if _, err := LoadConfig(get, "h", "/s", 1, ""); err == nil {
134		t.Fatal("a bad value was accepted")
135	}
136}
137
138func TestResolveRegisterToken(t *testing.T) {
139	env := map[string]string{}
140	get := func(k string) string { return env[k] }
141	if _, err := ResolveRegisterToken("", false, strings.NewReader(""), get); err == nil || !strings.Contains(err.Error(), EnvRegisterToken) {
142		t.Fatalf("%v", err)
143	}
144	if tok, _ := ResolveRegisterToken("from-flag", false, nil, get); tok != "from-flag" {
145		t.Fatal(tok)
146	}
147	env[EnvRegisterToken] = " from-env \n"
148	if tok, _ := ResolveRegisterToken("from-flag", false, nil, get); tok != "from-env" {
149		t.Fatalf("the environment must win over the flag: %q", tok)
150	}
151	if tok, _ := ResolveRegisterToken("from-flag", true, strings.NewReader("from-stdin\n"), get); tok != "from-stdin" {
152		t.Fatalf("standard input must win: %q", tok)
153	}
154	if _, err := ResolveRegisterToken("x", true, strings.NewReader("  \n"), get); err == nil {
155		t.Fatal("an empty standard input was accepted")
156	}
157}

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 4

Tags 11

↑↓ moveenter openesc close