v0.2.0 80c61e3

ssh_dialer_test.go RawHistoryFolder

1package dgram
2
3import (
4	"context"
5	"encoding/base64"
6	"errors"
7	"io"
8	"net"
9	"strings"
10	"testing"
11	"time"
12
13	pdgram "github.com/bnema/vev/pkg/dgram"
14)
15
16type fakeBootstrapProcess struct {
17	stdout   io.ReadCloser
18	startErr error
19	waitErr  error
20	killed   bool
21	waited   bool
22}
23
24type closeTrackReadCloser struct {
25	io.Reader
26	closed *bool
27}
28
29func (r closeTrackReadCloser) Close() error { *r.closed = true; return nil }
30
31func (p *fakeBootstrapProcess) StdoutPipe() (io.ReadCloser, error) { return p.stdout, nil }
32func (p *fakeBootstrapProcess) Start() error                       { return p.startErr }
33func (p *fakeBootstrapProcess) Kill() error                        { p.killed = true; return nil }
34func (p *fakeBootstrapProcess) Wait() error                        { p.waited = true; return p.waitErr }
35
36func withBootstrapStarter(t *testing.T, fn func(context.Context, string, io.Writer) bootstrapProcess) {
37	t.Helper()
38	old := startUDPBootstrap
39	startUDPBootstrap = fn
40	t.Cleanup(func() { startUDPBootstrap = old })
41}
42
43func withListenUDP(t *testing.T, fn func(context.Context) (net.PacketConn, error)) {
44	t.Helper()
45	old := listenUDPPacket
46	listenUDPPacket = fn
47	t.Cleanup(func() { listenUDPPacket = old })
48}
49
50func TestRemoteDialerUDPBootstrapFailures(t *testing.T) {
51	key := base64.StdEncoding.EncodeToString(make([]byte, pdgram.KeySize))
52	tests := []struct {
53		name             string
54		stdout           string
55		start            error
56		listen           error
57		want             string
58		killed           bool
59		waited           bool
60		wantStdoutClosed bool
61	}{
62		{name: "start failure", start: errors.New("ssh missing"), want: "remote UDP transport unavailable: start bootstrap: ssh missing", wantStdoutClosed: true},
63		{name: "malformed readiness", stdout: "hello\n", want: "malformed UDP readiness line", killed: true, waited: true, wantStdoutClosed: true},
64		{name: "bootstrap wait failure", stdout: "VEV-UDP 4444 " + key + "\n", want: "wait bootstrap: exit status 255", waited: true, wantStdoutClosed: true},
65		{name: "packet listen failure", stdout: "VEV-UDP 4444 " + key + "\n", listen: errors.New("bind denied"), want: "listen UDP: bind denied", waited: true, wantStdoutClosed: true},
66	}
67	for _, tt := range tests {
68		t.Run(tt.name, func(t *testing.T) {
69			stdoutClosed := false
70			proc := &fakeBootstrapProcess{stdout: closeTrackReadCloser{Reader: strings.NewReader(tt.stdout), closed: &stdoutClosed}, startErr: tt.start}
71			if tt.name == "bootstrap wait failure" {
72				proc.waitErr = errors.New("exit status 255")
73			}
74			withBootstrapStarter(t, func(context.Context, string, io.Writer) bootstrapProcess { return proc })
75			if tt.listen != nil {
76				withListenUDP(t, func(context.Context) (net.PacketConn, error) { return nil, tt.listen })
77			}
78			tr, err := NewRemoteDialer("127.0.0.1", "work").Dial(context.Background())
79			if err == nil || !strings.Contains(err.Error(), tt.want) {
80				t.Fatalf("Dial() error = %v, want containing %q", err, tt.want)
81			}
82			var dialErr *RemoteDialError
83			if !errors.As(err, &dialErr) {
84				t.Fatalf("Dial() error = %T, want *RemoteDialError", err)
85			}
86			if dialErr.Kind != RemoteDialBootstrapUnavailable {
87				t.Fatalf("Dial() error kind = %v, want bootstrap unavailable", dialErr.Kind)
88			}
89			if tr != nil {
90				t.Fatalf("Dial() transport = %T, want nil", tr)
91			}
92			if proc.killed != tt.killed {
93				t.Fatalf("killed=%v, want %v", proc.killed, tt.killed)
94			}
95			if proc.waited != tt.waited {
96				t.Fatalf("waited=%v, want %v", proc.waited, tt.waited)
97			}
98			if stdoutClosed != tt.wantStdoutClosed {
99				t.Fatalf("stdout closed=%v, want %v", stdoutClosed, tt.wantStdoutClosed)
100			}
101		})
102	}
103}
104
105func TestListenUDPPacketUsesDualStackWildcard(t *testing.T) {
106	pc, err := listenUDPPacket(context.Background())
107	if err != nil {
108		t.Fatal(err)
109	}
110	defer func() { _ = pc.Close() }()
111	addr, ok := pc.LocalAddr().(*net.UDPAddr)
112	if !ok {
113		t.Fatalf("local addr = %T, want *net.UDPAddr", pc.LocalAddr())
114	}
115	if !addr.IP.IsUnspecified() {
116		t.Fatalf("listen IP = %v, want unspecified wildcard", addr.IP)
117	}
118}
119
120func TestRemoteDialerProbeFailureCleansUpWithoutStdioFallback(t *testing.T) {
121	key := base64.StdEncoding.EncodeToString(make([]byte, pdgram.KeySize))
122	proc := &fakeBootstrapProcess{stdout: io.NopCloser(strings.NewReader("VEV-UDP 4444 " + key + "\n"))}
123	withBootstrapStarter(t, func(_ context.Context, target string, _ io.Writer) bootstrapProcess {
124		if target != "127.0.0.1" {
125			t.Fatalf("bootstrap target = %q", target)
126		}
127		return proc
128	})
129	var lc net.ListenConfig
130	pc, err := lc.ListenPacket(context.Background(), "udp", "127.0.0.1:0")
131	if err != nil {
132		t.Fatal(err)
133	}
134	closed := false
135	withListenUDP(t, func(context.Context) (net.PacketConn, error) {
136		return closeTrackPacketConn{PacketConn: pc, closed: &closed}, nil
137	})
138	d := NewRemoteDialer("127.0.0.1", "work")
139	d.ProbeTimeout = time.Nanosecond
140	tr, err := d.Dial(context.Background())
141	if err == nil || !strings.Contains(err.Error(), "remote UDP transport unavailable: probe UDP transport") {
142		t.Fatalf("Dial() error = %v, want probe unavailable", err)
143	}
144	var dialErr *RemoteDialError
145	if !errors.As(err, &dialErr) {
146		t.Fatalf("Dial() error = %T, want *RemoteDialError", err)
147	}
148	if dialErr.Kind != RemoteDialProbeUnreachable {
149		t.Fatalf("Dial() error kind = %v, want probe unreachable", dialErr.Kind)
150	}
151	for _, want := range []string{"firewall", "VEV_REMOTE_TRANSPORT=stdio vev attach 127.0.0.1"} {
152		if !strings.Contains(err.Error(), want) {
153			t.Fatalf("Dial() error = %q, want actionable hint %q", err, want)
154		}
155	}
156	if tr != nil {
157		t.Fatalf("transport=%T, want nil", tr)
158	}
159	if !closed {
160		t.Fatal("packet conn not closed after probe failure")
161	}
162	if proc.killed {
163		t.Fatal("bootstrap process killed after it was already waited")
164	}
165	if !proc.waited {
166		t.Fatal("bootstrap process not waited after probe failure")
167	}
168}
169
170type closeTrackPacketConn struct {
171	net.PacketConn
172	closed *bool
173}
174
175func (c closeTrackPacketConn) Close() error { *c.closed = true; return c.PacketConn.Close() }
176
177func TestDeliverUDPReadyErasesParsedKeyAfterCancellation(t *testing.T) {
178	ctx, cancel := context.WithCancel(context.Background())
179	cancel()
180	key := make([]byte, pdgram.KeySize)
181	for i := range key {
182		key[i] = byte(i + 1)
183	}
184	ready, err := readUDPReady(strings.NewReader("VEV-UDP 1234 " + base64.StdEncoding.EncodeToString(key) + "\n"))
185	if err != nil {
186		t.Fatal(err)
187	}
188
189	deliverUDPReady(ctx, make(chan udpReadyResult), udpReadyResult{ready: ready})
190
191	for i, b := range ready.key {
192		if b != 0 {
193			t.Fatalf("key[%d] = %d, want erased", i, b)
194		}
195	}
196}
197
198func TestReadUDPReady(t *testing.T) {
199	key := base64.StdEncoding.EncodeToString(make([]byte, pdgram.KeySize))
200	tests := []struct {
201		name, line string
202		wantPort   int
203		wantErr    string
204		exactErr   bool
205	}{
206		{name: "valid", line: "VEV-UDP 1234 " + key + "\n", wantPort: 1234},
207		{name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed UDP readiness line", exactErr: true},
208		{name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: `invalid UDP port "99999"`, exactErr: true},
209		{name: "bad key", line: "VEV-UDP 1234 !!!\n", wantErr: "invalid bootstrap key"},
210	}
211	for _, tt := range tests {
212		t.Run(tt.name, func(t *testing.T) {
213			got, err := readUDPReady(strings.NewReader(tt.line))
214			if tt.wantErr != "" {
215				if err == nil {
216					t.Fatalf("err=nil, want %q", tt.wantErr)
217				}
218				if tt.exactErr && err.Error() != tt.wantErr {
219					t.Fatalf("err=%q, want exactly %q", err, tt.wantErr)
220				}
221				if !tt.exactErr && !strings.Contains(err.Error(), tt.wantErr) {
222					t.Fatalf("err=%v, want containing %q", err, tt.wantErr)
223				}
224				return
225			}
226			if err != nil {
227				t.Fatal(err)
228			}
229			if got.port != tt.wantPort || len(got.key) != pdgram.KeySize {
230				t.Fatalf("ready=%+v keylen=%d", got, len(got.key))
231			}
232		})
233	}
234}
235
236func TestLimitedBufferCapsBootstrapStderr(t *testing.T) {
237	var b limitedBuffer
238	chunk := strings.Repeat("x", maxBootstrapStderr+1024)
239	n, err := b.Write([]byte(chunk))
240	if err != nil {
241		t.Fatal(err)
242	}
243	if n != len(chunk) {
244		t.Fatalf("n=%d, want %d", n, len(chunk))
245	}
246	if got := len(b.String()); got != maxBootstrapStderr {
247		t.Fatalf("captured=%d, want %d", got, maxBootstrapStderr)
248	}
249}
250
251func TestSSHTargetHost(t *testing.T) {
252	tests := []struct {
253		target string
254		want   string
255	}{
256		{target: "example.com", want: "example.com"},
257		{target: "user@example.com", want: "example.com"},
258		{target: "example.com:2222", want: "example.com"},
259		{target: "user@[2001:db8::1]:2222", want: "2001:db8::1"},
260	}
261	for _, tt := range tests {
262		t.Run(tt.target, func(t *testing.T) {
263			if got := sshTargetHost(tt.target); got != tt.want {
264				t.Fatalf("sshTargetHost(%q) = %q, want %q", tt.target, got, tt.want)
265			}
266		})
267	}
268}

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 12

Tags 11

↑↓ moveenter openesc close