tvl-depot/fun/clbot/gerrit/watcher_test.go
Luke Granger-Brown c05803ff14 feat(clbot): Create Gerrit watcher and basic clbot binary.
gerrit.Watcher is a class which watches the Gerrit stream-events SSH
connection and produces events.

There's a basic CLBot binary as well, to demonstrate driving it to
produce messages on the logging output. It doesn't really do anything
else.

Change-Id: I274fe0a77c8329f79456425405e2fbdc3ca2edf0
Reviewed-on: https://cl.tvl.fyi/c/depot/+/245
Reviewed-by: tazjin <mail@tazj.in>
2020-06-14 17:16:32 +00:00

190 lines
6.7 KiB
Go

package gerrit
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/subtle"
"fmt"
"net"
"testing"
"time"
"code.tvl.fyi/fun/clbot/gerrit/gerritevents"
log "github.com/golang/glog"
"github.com/google/go-cmp/cmp"
"golang.org/x/crypto/ssh"
)
var (
sshServerSigner, sshServerPublicKey = mustNewKey()
sshClientSigner, sshClientPublicKey = mustNewKey()
)
func mustNewKey() (ssh.Signer, ssh.PublicKey) {
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
panic(err)
}
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
panic(err)
}
publicKey, err := ssh.NewPublicKey(key.Public())
if err != nil {
panic(err)
}
return signer, publicKey
}
func newSSHServer(lines string) (addr string, cleanup func(), err error) {
config := &ssh.ServerConfig{
PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
pkBytes := pubKey.Marshal()
wantPKBytes := sshClientPublicKey.Marshal()
if subtle.ConstantTimeCompare(pkBytes, wantPKBytes) == 0 {
return nil, fmt.Errorf("unauthorized")
}
return &ssh.Permissions{}, nil
},
}
config.AddHostKey(sshServerSigner)
ln, err := net.Listen("tcp", ":0")
if err != nil {
log.Fatalf("Listen on tcp/:0: %v", err)
}
handle := func(conn net.Conn) {
defer conn.Close()
sc, newchch, newreqch, err := ssh.NewServerConn(conn, config)
if err != nil {
log.Fatalf("NewServerConn: %v", err)
}
go ssh.DiscardRequests(newreqch)
for newCh := range newchch {
if newCh.ChannelType() != "session" {
newCh.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, reqs, err := newCh.Accept()
if err != nil {
log.Fatalf("Could not accept channel: %v", err)
}
go func(in <-chan *ssh.Request) {
for req := range in {
req.Reply(req.Type == "exec", nil)
}
}(reqs)
channel.Write([]byte(lines))
sc.SendRequest("goaway", false, nil)
}
}
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go handle(conn)
}
}()
cleanup = func() {
ln.Close()
}
return ln.Addr().String(), cleanup, err
}
func ts(s string) gerritevents.Time {
t, err := time.Parse("2006-01-02 15:04:05 -0700 MST", s)
if err != nil {
panic(err)
}
return gerritevents.Time{t}
}
func optStr(s string) *string { return &s }
func TestWatcher(t *testing.T) {
tcs := []struct {
name string
lines string
want []gerritevents.Event
}{{
name: "no events",
}, {
name: "single test event",
lines: `{"author":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"approvals":[{"type":"Code-Review","description":"Code-Review","value":"2","oldValue":"0"}],"comment":"Patch Set 3: Code-Review+2","patchSet":{"number":3,"revision":"6fe272d3f82c6efdfe1167fab98bf918efc03fe5","parents":["d984b6018cf68c7e8b7169b475d90134fbcee767"],"ref":"refs/changes/44/244/3","uploader":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"createdOn":1592081910,"author":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"kind":"REWORK","sizeInsertions":83,"sizeDeletions":-156},"change":{"project":"depot","branch":"master","id":"I546c701145fa204b7ba7518a8a56a783588629e0","number":244,"subject":"refactor(ops/nixos): Move my NixOS configurations to //users/tazjin","owner":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"url":"https://cl.tvl.fyi/c/depot/+/244","commitMessage":"refactor(ops/nixos): Move my NixOS configurations to //users/tazjin\n\nNixOS modules move one level up because it\u0027s unlikely that //ops/nixos\nwill contain actual systems at this point (they\u0027re user-specific).\n\nThis is the first users folder, so it is also added to the root\nreadTree invocation for the repository.\n\nChange-Id: I546c701145fa204b7ba7518a8a56a783588629e0\n","createdOn":1592081577,"status":"NEW"},"project":"depot","refName":"refs/heads/master","changeKey":{"id":"I546c701145fa204b7ba7518a8a56a783588629e0"},"type":"comment-added","eventCreatedOn":1592081929}
`,
want: []gerritevents.Event{
&gerritevents.CommentAdded{
Type: "comment-added",
Change: gerritevents.Change{
Project: "depot",
Branch: "master",
ID: "I546c701145fa204b7ba7518a8a56a783588629e0",
Number: 244,
Subject: "refactor(ops/nixos): Move my NixOS configurations to //users/tazjin",
Owner: gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
URL: "https://cl.tvl.fyi/c/depot/+/244",
CommitMessage: "refactor(ops/nixos): Move my NixOS configurations to //users/tazjin\n\nNixOS modules move one level up because it's unlikely that //ops/nixos\nwill contain actual systems at this point (they're user-specific).\n\nThis is the first users folder, so it is also added to the root\nreadTree invocation for the repository.\n\nChange-Id: I546c701145fa204b7ba7518a8a56a783588629e0\n",
CreatedOn: ts("2020-06-13 21:52:57 +0100 BST"),
Status: "NEW",
},
PatchSet: gerritevents.PatchSet{
Number: 3,
Revision: "6fe272d3f82c6efdfe1167fab98bf918efc03fe5",
Parents: []string{"d984b6018cf68c7e8b7169b475d90134fbcee767"},
Ref: "refs/changes/44/244/3",
Uploader: gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
Author: gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
CreatedOn: ts("2020-06-13 21:58:30 +0100 BST"),
Kind: "REWORK",
SizeInsertions: 83,
SizeDeletions: -156,
},
Author: gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
Approvals: []gerritevents.Approval{{Type: "Code-Review", Description: "Code-Review", Value: "2", OldValue: optStr("0")}},
Comment: "Patch Set 3: Code-Review+2",
EventCreatedOn: ts("2020-06-13 21:58:49 +0100 BST"),
},
},
}}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
serverAddr, cleanup, err := newSSHServer(tc.lines)
if err != nil {
t.Fatalf("newSSHServer: %v", err)
}
t.Cleanup(cleanup)
config := &ssh.ClientConfig{
User: "bert",
Auth: []ssh.AuthMethod{ssh.PublicKeys(sshClientSigner)},
HostKeyCallback: ssh.FixedHostKey(sshServerPublicKey),
Timeout: 10 * time.Millisecond,
}
w, err := New(ctx, "tcp", serverAddr, config)
if err != nil {
t.Fatalf("New: %v", err)
}
var gotEvents []gerritevents.Event
for ev := range w.Events() {
gotEvents = append(gotEvents, ev)
}
if diff := cmp.Diff(gotEvents, tc.want); diff != "" {
t.Errorf("got events != want events: diff:\n%v", diff)
}
})
}
}