internal/sshd/sshd_test.go

v1.35.1
gitbay/internal/sshd/sshd_test.go history · blame · raw

199 lines · 5069 bytes

  1package sshd
  2
  3import (
  4	"bufio"
  5	"bytes"
  6	"crypto/ed25519"
  7	"crypto/rand"
  8	"errors"
  9	"net"
 10	"path/filepath"
 11	"strings"
 12	"testing"
 13	"time"
 14
 15	"golang.org/x/crypto/ssh"
 16
 17	"gitbay.org/gitbay/internal/config"
 18	"gitbay.org/gitbay/internal/store"
 19)
 20
 21// followServer starts an embedded server holding alice, her public repo
 22// alice/app and a queued build 1 whose log has one line, and returns it
 23// with a client connected as alice.
 24func followServer(t *testing.T) (*Server, *ssh.Client) {
 25	t.Helper()
 26	root := t.TempDir()
 27	st, err := store.Open(filepath.Join(root, "gitbay.db"))
 28	if err != nil {
 29		t.Fatal(err)
 30	}
 31	t.Cleanup(func() { st.Close() })
 32	if err := st.MigrateUp(); err != nil {
 33		t.Fatal(err)
 34	}
 35	uid, err := st.CreateUser("alice", false)
 36	if err != nil {
 37		t.Fatal(err)
 38	}
 39	repoID, err := st.CreateRepo("user", uid, "app", "public")
 40	if err != nil {
 41		t.Fatal(err)
 42	}
 43	id, err := st.CreateBuild(repoID, "unit", "abc", "main", `["true"]`, "", "", true)
 44	if err != nil {
 45		t.Fatal(err)
 46	}
 47	if err := st.AppendBuildLog(id, []byte("queued\n")); err != nil {
 48		t.Fatal(err)
 49	}
 50	_, priv, err := ed25519.GenerateKey(rand.Reader)
 51	if err != nil {
 52		t.Fatal(err)
 53	}
 54	signer, err := ssh.NewSignerFromKey(priv)
 55	if err != nil {
 56		t.Fatal(err)
 57	}
 58	pub := signer.PublicKey()
 59	if err := st.AddSSHKey(uid, ssh.FingerprintSHA256(pub), pub.Type(), pub.Marshal(), "full", "test"); err != nil {
 60		t.Fatal(err)
 61	}
 62
 63	cfg := config.Default()
 64	cfg.Server.Root = root
 65	srv, err := New(cfg, st)
 66	if err != nil {
 67		t.Fatal(err)
 68	}
 69	ln, err := net.Listen("tcp", "127.0.0.1:0")
 70	if err != nil {
 71		t.Fatal(err)
 72	}
 73	go srv.Serve(ln)
 74	t.Cleanup(func() { ln.Close() })
 75
 76	client, err := ssh.Dial("tcp", ln.Addr().String(), &ssh.ClientConfig{
 77		User:            "git",
 78		Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
 79		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
 80		Timeout:         5 * time.Second,
 81	})
 82	if err != nil {
 83		t.Fatal(err)
 84	}
 85	t.Cleanup(func() { client.Close() })
 86	return srv, client
 87}
 88
 89// startFollow runs build log --follow on a new session and returns once
 90// the stored line has arrived, so the follow is past its first read.
 91func startFollow(t *testing.T, client *ssh.Client, stderr *bytes.Buffer) *ssh.Session {
 92	t.Helper()
 93	sess, err := client.NewSession()
 94	if err != nil {
 95		t.Fatal(err)
 96	}
 97	t.Cleanup(func() { sess.Close() })
 98	sess.Stderr = stderr
 99	out, err := sess.StdoutPipe()
100	if err != nil {
101		t.Fatal(err)
102	}
103	if err := sess.Start("build log alice/app 1 --follow"); err != nil {
104		t.Fatal(err)
105	}
106	line := make(chan string, 1)
107	go func() {
108		l, _ := bufio.NewReader(out).ReadString('\n')
109		line <- l
110	}()
111	select {
112	case l := <-line:
113		if l != "queued\n" {
114			t.Fatalf("first line %q", l)
115		}
116	case <-time.After(5 * time.Second):
117		t.Fatal("the stored log never arrived")
118	}
119	return sess
120}
121
122func activeSessions(s *Server) int32 {
123	s.mu.Lock()
124	defer s.mu.Unlock()
125	var n int32
126	for c := range s.conns {
127		n += c.active.Load()
128	}
129	return n
130}
131
132// Closing the session channel ends a follow of a queued build while the
133// connection stays up, as a Ctrl-C does over the CLI's shared connection.
134// Nothing else would end it for ten minutes.
135func TestFollowEndsWhenChannelCloses(t *testing.T) {
136	srv, client := followServer(t)
137	var stderr bytes.Buffer
138	sess := startFollow(t, client, &stderr)
139	if n := activeSessions(srv); n != 1 {
140		t.Fatalf("%d sessions active while following, want 1", n)
141	}
142	sess.Close()
143
144	deadline := time.Now().Add(5 * time.Second)
145	for activeSessions(srv) != 0 {
146		if time.Now().After(deadline) {
147			t.Fatal("the follow outlived its channel")
148		}
149		time.Sleep(20 * time.Millisecond)
150	}
151	if _, err := client.NewSession(); err != nil {
152		t.Fatalf("the connection did not survive the channel: %v", err)
153	}
154}
155
156// A command that fails on its own while the server is stopping says
157// nothing about a restart: only a follow that Stop ended does.
158func TestStopLeavesOtherFailuresAlone(t *testing.T) {
159	srv, client := followServer(t)
160	srv.Stop()
161	sess, err := client.NewSession()
162	if err != nil {
163		t.Fatal(err)
164	}
165	defer sess.Close()
166	var stderr bytes.Buffer
167	sess.Stderr = &stderr
168	var exit *ssh.ExitError
169	if err := sess.Run("repo show nosuch/repo"); !errors.As(err, &exit) || exit.ExitStatus() != 3 {
170		t.Fatalf("repo show of a missing repository: %v, want exit 3", err)
171	}
172	if strings.Contains(stderr.String(), "restarting") {
173		t.Errorf("stderr %q", stderr.String())
174	}
175}
176
177// Stop ends a follow with exit 1 and says why, without closing the
178// connection.
179func TestStopEndsFollow(t *testing.T) {
180	srv, client := followServer(t)
181	var stderr bytes.Buffer
182	sess := startFollow(t, client, &stderr)
183	srv.Stop()
184
185	waited := make(chan error, 1)
186	go func() { waited <- sess.Wait() }()
187	select {
188	case err := <-waited:
189		var exit *ssh.ExitError
190		if !errors.As(err, &exit) || exit.ExitStatus() != 1 {
191			t.Fatalf("follow ended with %v, want exit 1", err)
192		}
193	case <-time.After(5 * time.Second):
194		t.Fatal("Stop did not end the follow")
195	}
196	if !strings.Contains(stderr.String(), "gitbay is restarting") {
197		t.Errorf("stderr %q", stderr.String())
198	}
199}