internal/control/runnerattach_test.go
187 lines · 6680 bytes
1package control
2
3import (
4 "bytes"
5 "strconv"
6 "strings"
7 "testing"
8
9 "gitbay.org/gitbay/internal/config"
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// attachFixture: alice (not admin) owns alice/app with a build queued;
15// mallory (not admin) owns mallory/evil with an older build queued. Each
16// has a runner-scoped key. The Ctx polls as the given user with the given
17// key, which is what the SSH listener produces.
18type attachFixture struct {
19 st *store.Store
20 alice, mallory int64
21 aliceKey, malloryKey store.SSHKey
22 app, evil store.Repo
23 appBuild, evilBuild int64
24}
25
26func newAttachFixture(t *testing.T) attachFixture {
27 t.Helper()
28 st, err := store.Open(":memory:")
29 if err != nil {
30 t.Fatal(err)
31 }
32 t.Cleanup(func() { st.Close() })
33 if err := st.MigrateUp(); err != nil {
34 t.Fatal(err)
35 }
36 var f attachFixture
37 f.st = st
38 mk := func(name, fp string) (int64, store.SSHKey, store.Repo, string) {
39 uid, err := st.CreateUser(name, false)
40 if err != nil {
41 t.Fatal(err)
42 }
43 if err := st.AddSSHKey(uid, fp, "ssh-ed25519", []byte(fp), "runner"); err != nil {
44 t.Fatal(err)
45 }
46 k, _ := st.SSHKeyByFingerprint(fp)
47 repoName := map[string]string{"alice": "app", "mallory": "evil"}[name]
48 rid, err := st.CreateRepo("user", uid, repoName, "public")
49 if err != nil {
50 t.Fatal(err)
51 }
52 repo, _ := st.RepoByID(rid)
53 return uid, k, repo, repoName
54 }
55 f.mallory, f.malloryKey, f.evil, _ = mk("mallory", "SHA256:mallory")
56 f.alice, f.aliceKey, f.app, _ = mk("alice", "SHA256:alice")
57 // mallory's build is older, so an unrestricted claim would take it.
58 f.evilBuild, err = st.CreateBuild(f.evil.ID, "unit", "aaa111", "main", "[]", "", "", true)
59 if err != nil {
60 t.Fatal(err)
61 }
62 f.appBuild, err = st.CreateBuild(f.app.ID, "unit", "bbb222", "main", "[]", "", "", true)
63 if err != nil {
64 t.Fatal(err)
65 }
66 return f
67}
68
69func (f attachFixture) ctx(uid int64, key store.SSHKey, admin bool) (*Ctx, *bytes.Buffer) {
70 var out bytes.Buffer
71 name := "alice"
72 if uid == f.mallory {
73 name = "mallory"
74 }
75 return &Ctx{
76 User: store.User{ID: uid, Username: name, IsAdmin: admin},
77 Scope: key.Scope,
78 Source: key.Fingerprint,
79 Store: f.st,
80 Cfg: config.Config{Server: config.Server{Root: "/nonexistent", SiteURL: "https://x.test"}},
81 Stdin: strings.NewReader(""),
82 Stdout: &out,
83 Stderr: &out,
84 }, &out
85}
86
87// A runner key with no attachment claims nothing, whatever is queued.
88func TestRunnerNextUnattachedClaimsNothing(t *testing.T) {
89 f := newAttachFixture(t)
90 c, out := f.ctx(f.alice, f.aliceKey, false)
91 if code := runRunnerNext(c, nil); code != protocol.ExitOK || !strings.Contains(out.String(), "no pending builds") {
92 t.Fatalf("exit %d: %s", code, out.String())
93 }
94 b, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild)
95 if b.Status != "pending" {
96 t.Fatalf("unattached key claimed a build: %s", b.Status)
97 }
98}
99
100// An attached key claims its repository's build and not the older one
101// queued elsewhere; naming a repository outside the attachments is refused.
102func TestRunnerNextAttachedClaimsOwnRepoOnly(t *testing.T) {
103 f := newAttachFixture(t)
104 if err := f.st.AttachRunner(f.aliceKey.ID, f.app.ID); err != nil {
105 t.Fatal(err)
106 }
107 c, out := f.ctx(f.alice, f.aliceKey, false)
108 if code := runRunnerNext(c, nil); code != protocol.ExitOK || !strings.Contains(out.String(), "alice/app") {
109 t.Fatalf("exit %d: %s", code, out.String())
110 }
111 if b, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild); b.Status != "pending" {
112 t.Fatalf("mallory's build was touched: %s", b.Status)
113 }
114 c, out = f.ctx(f.alice, f.aliceKey, false)
115 if code := runRunnerNext(c, []string{"mallory/evil"}); code != protocol.ExitDenied {
116 t.Fatalf("naming an unattached repo: exit %d, want %d: %s", code, protocol.ExitDenied, out.String())
117 }
118}
119
120// The heartbeat is recorded against the key, and admin runners shows it
121// with its fingerprint and attachments.
122func TestAdminRunnersShowsKeyAndAttachments(t *testing.T) {
123 f := newAttachFixture(t)
124 if err := f.st.AttachRunner(f.aliceKey.ID, f.app.ID); err != nil {
125 t.Fatal(err)
126 }
127 c, _ := f.ctx(f.alice, f.aliceKey, false)
128 runRunnerNext(c, nil)
129 admin, out := f.ctx(f.alice, f.aliceKey, true)
130 admin.Scope = "full"
131 if code := runAdminRunners(admin, nil); code != protocol.ExitOK {
132 t.Fatalf("admin runners: exit %d: %s", code, out.String())
133 }
134 if !strings.Contains(out.String(), "alice\tSHA256:alice\t") || !strings.Contains(out.String(), "\talice/app\t") {
135 t.Fatalf("row lacks fingerprint or attachments:\n%s", out.String())
136 }
137}
138
139// Untrusted builds are skipped unless the runner asks.
140func TestRunnerNextUntrustedFlag(t *testing.T) {
141 f := newAttachFixture(t)
142 if err := f.st.AttachRunner(f.aliceKey.ID, f.app.ID); err != nil {
143 t.Fatal(err)
144 }
145 c, _ := f.ctx(f.alice, f.aliceKey, false)
146 runRunnerNext(c, nil) // takes the trusted build
147 fork, err := f.st.CreateBuild(f.app.ID, "unit", "ccc333", "refs/merge-requests/1/head", "[]", "", "", false)
148 if err != nil {
149 t.Fatal(err)
150 }
151 c, out := f.ctx(f.alice, f.aliceKey, false)
152 runRunnerNext(c, nil)
153 if !strings.Contains(out.String(), "no pending builds") {
154 t.Fatalf("fork head claimed without --untrusted: %s", out.String())
155 }
156 c, out = f.ctx(f.alice, f.aliceKey, false)
157 if code := runRunnerNext(c, []string{"--untrusted"}); code != protocol.ExitOK || !strings.Contains(out.String(), "alice/app") {
158 t.Fatalf("--untrusted did not claim the fork head: exit %d %s", code, out.String())
159 }
160 if b, _ := f.st.BuildByNumber(f.app.ID, fork); b.Status != "running" {
161 t.Fatalf("fork build is %s, want running", b.Status)
162 }
163}
164
165// runner done and runner log on a build whose repository is not attached
166// to the key are refused.
167func TestRunnerDoneRefusedForUnattachedBuild(t *testing.T) {
168 f := newAttachFixture(t)
169 if err := f.st.AttachRunner(f.malloryKey.ID, f.evil.ID); err != nil {
170 t.Fatal(err)
171 }
172 c, _ := f.ctx(f.mallory, f.malloryKey, false)
173 runRunnerNext(c, nil) // mallory holds her own build
174 evil, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild)
175 c, out := f.ctx(f.alice, f.aliceKey, false)
176 id := strconv.FormatInt(evil.ID, 10)
177 if code := runRunnerDone(c, []string{id, "success"}); code != protocol.ExitDenied {
178 t.Fatalf("done on an unattached build: exit %d, want %d: %s", code, protocol.ExitDenied, out.String())
179 }
180 c, out = f.ctx(f.alice, f.aliceKey, false)
181 if code := runRunnerLog(c, []string{id}); code != protocol.ExitDenied {
182 t.Fatalf("log on an unattached build: exit %d, want %d: %s", code, protocol.ExitDenied, out.String())
183 }
184 if b, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild); b.Status != "running" {
185 t.Fatalf("build was finished by a foreign key: %s", b.Status)
186 }
187}