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}