bridge_test.go

  1package bridge
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"net/http"
  7	"net/http/httptest"
  8	"os"
  9	"os/exec"
 10	"path/filepath"
 11	"strings"
 12	"testing"
 13
 14	"git.kilimanjaro.io/ci/internal/eventlog"
 15	"git.kilimanjaro.io/ci/internal/lex"
 16)
 17
 18func sh(t *testing.T, dir string, args ...string) string {
 19	t.Helper()
 20	cmd := exec.Command(args[0], args[1:]...)
 21	cmd.Dir = dir
 22	cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=a", "GIT_AUTHOR_EMAIL=a@x", "GIT_COMMITTER_NAME=a",
 23		"GIT_COMMITTER_EMAIL=a@x", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null")
 24	out, err := cmd.CombinedOutput()
 25	if err != nil {
 26		t.Fatalf("%v: %v\n%s", args, err, out)
 27	}
 28	return strings.TrimSpace(string(out))
 29}
 30
 31func write(t *testing.T, p, s string) {
 32	t.Helper()
 33	if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
 34		t.Fatal(err)
 35	}
 36	if err := os.WriteFile(p, []byte(s), 0o644); err != nil {
 37		t.Fatal(err)
 38	}
 39}
 40
 41// repo makes a bare repo with two commits on main and returns it and the shas.
 42func repo(t *testing.T) (bare, c1, c2, work string) {
 43	t.Helper()
 44	work = filepath.Join(t.TempDir(), "w")
 45	os.MkdirAll(work, 0o755)
 46	sh(t, work, "git", "init", "-q", "-b", "main")
 47	write(t, filepath.Join(work, "a.txt"), "1")
 48	sh(t, work, "git", "add", "-A")
 49	sh(t, work, "git", "commit", "-qm", "one")
 50	c1 = sh(t, work, "git", "rev-parse", "HEAD")
 51	write(t, filepath.Join(work, "docs/b.md"), "2")
 52	write(t, filepath.Join(work, "a.txt"), "1b")
 53	sh(t, work, "git", "add", "-A")
 54	sh(t, work, "git", "commit", "-qm", "two")
 55	c2 = sh(t, work, "git", "rev-parse", "HEAD")
 56	bare = filepath.Join(t.TempDir(), "r.git")
 57	sh(t, "", "git", "clone", "-q", "--bare", work, bare)
 58	return
 59}
 60
 61func TestBuildPush(t *testing.T) {
 62	ctx := context.Background()
 63	bare, c1, c2, _ := repo(t)
 64	lines, err := ParseHookInput(strings.NewReader(c1 + " " + c2 + " refs/heads/main\n" + zeroSha + " " + c2 + " refs/heads/feature\n" + c1 + " " + zeroSha + " refs/heads/old\n"))
 65	if err != nil {
 66		t.Fatal(err)
 67	}
 68	p, err := BuildPush(ctx, bare, "r", "bt", lines, []string{"skip-ci"})
 69	if err != nil {
 70		t.Fatal(err)
 71	}
 72	if len(p.Updates) != 3 {
 73		t.Fatalf("%+v", p)
 74	}
 75	main := p.Updates[0]
 76	if !main.Meta.IsDefaultRef || strings.Join(main.ChangedFiles, ",") != "a.txt,docs/b.md" || main.PushOptions[0] != "skip-ci" {
 77		t.Errorf("main: %+v %+v", main, main.Meta)
 78	}
 79	if c := main.Meta.CommitCount.ByEmail; len(c) != 1 || c[0].Email != "a@x" || c[0].Count != 1 {
 80		t.Errorf("commit count %+v", c)
 81	}
 82	feature := p.Updates[1]
 83	if feature.Meta.IsDefaultRef || feature.ChangedFiles != nil {
 84		t.Errorf("new branch: changed files must be unknown: %+v", feature)
 85	}
 86	if c := feature.Meta.CommitCount.ByEmail; len(c) != 0 {
 87		t.Errorf("new branch at main's tip has no new commits: %+v", c)
 88	}
 89	if del := p.Updates[2]; del.NewSha != zeroSha {
 90		t.Errorf("delete: %+v", del)
 91	}
 92}
 93
 94func TestPushOptions(t *testing.T) {
 95	env := map[string]string{"GIT_PUSH_OPTION_COUNT": "2", "GIT_PUSH_OPTION_0": "ci-skip", "GIT_PUSH_OPTION_1": "x=y"}
 96	got := PushOptions(func(k string) string { return env[k] })
 97	if strings.Join(got, ",") != "ci-skip,x=y" {
 98		t.Error(got)
 99	}
100	if PushOptions(func(string) string { return "" }) != nil {
101		t.Error("no options")
102	}
103}
104
105func TestServerIngestAndServe(t *testing.T) {
106	ctx := context.Background()
107	dir := t.TempDir()
108	log, err := eventlog.Open(filepath.Join(dir, "bridge.db"))
109	if err != nil {
110		t.Fatal(err)
111	}
112	defer log.Close()
113	s := &Server{Log: log, SpoolDir: filepath.Join(dir, "spool"), ForgeHost: "git.example", OwnerDid: "did:plc:owner", Token: "t"}
114	srv := httptest.NewServer(s.Handler())
115	defer srv.Close()
116
117	bare, c1, c2, _ := repo(t)
118	p, _ := BuildPush(ctx, bare, "sovrn", "bt", []RefLine{{c1, c2, "refs/heads/main"}}, nil)
119	// the hook spools and pokes; the server isn't running Run here, so ingest directly
120	if err := Notify(ctx, s.SpoolDir, srv.URL, "t", p); err != nil {
121		t.Fatal(err)
122	}
123	os.WriteFile(filepath.Join(s.SpoolDir, "zz-garbage.json"), []byte("{"), 0o644)
124	n, err := s.Ingest()
125	if err != nil || n != 1 {
126		t.Fatalf("ingested %d: %v", n, err)
127	}
128	if left, _ := filepath.Glob(filepath.Join(s.SpoolDir, "*.json")); len(left) != 0 {
129		t.Errorf("spool not emptied: %v", left)
130	}
131	if bad, _ := filepath.Glob(filepath.Join(s.SpoolDir, "*.bad")); len(bad) != 1 {
132		t.Errorf("garbage not moved aside: %v", bad)
133	}
134	evs, _ := log.List(0, 10)
135	if len(evs) != 1 || evs[0].Nsid != lex.GitRefUpdateNSID {
136		t.Fatalf("%+v", evs)
137	}
138	var u lex.GitRefUpdate
139	json.Unmarshal(evs[0].Event, &u)
140	if u.Repo != "did:web:git.example:sovrn" || u.CommitterDid != "did:plc:owner" || *u.OwnerDid != "did:plc:owner" || u.LexiconTypeID != lex.GitRefUpdateNSID {
141		t.Errorf("stamped %+v", u)
142	}
143
144	// the stream needs the token; the DID document and health don't
145	for path, want := range map[string]int{"/events": 401, "/.well-known/did.json": 200, "/healthz": 200} {
146		resp, err := http.Get(srv.URL + path)
147		if err != nil {
148			t.Fatal(err)
149		}
150		resp.Body.Close()
151		if resp.StatusCode != want {
152			t.Errorf("%s: %d, want %d", path, resp.StatusCode, want)
153		}
154	}
155}
156
157func TestNotifyWithoutServerStillSpools(t *testing.T) {
158	dir := t.TempDir()
159	p := Push{Repo: "r", Updates: []lex.GitRefUpdate{{Ref: "refs/heads/main"}}}
160	if err := Notify(context.Background(), dir, "http://127.0.0.1:1", "", p); err != nil {
161		t.Fatal(err)
162	}
163	if files, _ := filepath.Glob(filepath.Join(dir, "*.json")); len(files) != 1 {
164		t.Errorf("spooled %v", files)
165	}
166}