| @@ -2,6 +2,7 @@ package hookd |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "bytes" |
| 5 | "fmt" |
| 5 | 6 | "os" |
| 6 | 7 | "path/filepath" |
| 7 | 8 | "strings" |
| @@ -204,3 +205,82 @@ func mustMR(t *testing.T, st *store.Store, repoID, n int64) store.MR { |
| 204 | 205 | } |
| 205 | 206 | return mr |
| 206 | 207 | } |
| 208 | |
| 209 | // A push the queue cannot attribute to a writer dequeues: one from a |
| 210 | // write deploy key, though the account that registered it can write, and |
| 211 | // one whose pusher cannot be looked up. |
| 212 | func TestPostReceiveDequeuesUncheckedPush(t *testing.T) { |
| 213 | for _, tc := range []struct { |
| 214 | name string |
| 215 | user func(alice int64) int64 |
| 216 | scope func(repoID int64) string |
| 217 | reason string |
| 218 | }{ |
| 219 | {"deploy key", func(a int64) int64 { return a }, func(id int64) string { return fmt.Sprintf("deploy:%d:rw", id) }, |
| 220 | "a deploy key pushed, and a deploy key cannot merge"}, |
| 221 | {"unknown pusher", func(int64) int64 { return 9999 }, func(int64) string { return "full" }, |
| 222 | "could not check who pushed"}, |
| 223 | } { |
| 224 | t.Run(tc.name, func(t *testing.T) { |
| 225 | st, err := store.Open(":memory:") |
| 226 | if err != nil { |
| 227 | t.Fatal(err) |
| 228 | } |
| 229 | t.Cleanup(func() { st.Close() }) |
| 230 | if err := st.MigrateUp(); err != nil { |
| 231 | t.Fatal(err) |
| 232 | } |
| 233 | alice, _ := st.CreateUser("alice", false) |
| 234 | repoID, _ := st.CreateRepo("user", alice, "app", "public") |
| 235 | st.UpdateRepoSettings(repoID, func(s *store.RepoSettings) { s.RequireApprovals = 1 }) |
| 236 | repo, _ := st.RepoByID(repoID) |
| 237 | root := t.TempDir() |
| 238 | f := &shapeFixture{t: t, st: st, repo: repo, uid: alice, root: root, src: filepath.Join(root, "src")} |
| 239 | f.dir = control.RepoDir(root, repo.OwnerName, repo.Name) |
| 240 | cfg := config.Config{} |
| 241 | cfg.Server.Root = root |
| 242 | srv := &Server{cfg: cfg, st: st} |
| 243 | os.MkdirAll(f.src, 0o755) |
| 244 | f.git(root, "init", "-q", "-b", "main", "src") |
| 245 | f.write("README", "x\n") |
| 246 | f.git(f.src, "add", ".") |
| 247 | f.git(f.src, "commit", "-q", "-m", "base") |
| 248 | f.git(f.src, "checkout", "-q", "-b", "feature") |
| 249 | f.write("feature.txt", "y\n") |
| 250 | f.git(f.src, "add", ".") |
| 251 | f.git(f.src, "commit", "-q", "-m", "change") |
| 252 | head := f.sha("HEAD") |
| 253 | os.MkdirAll(filepath.Dir(f.dir), 0o755) |
| 254 | f.git(root, "init", "-q", "--bare", f.dir) |
| 255 | f.sync() |
| 256 | f.git(f.dir, "update-ref", "refs/merge-requests/1/head", head) |
| 257 | if _, err := st.CreateMR(repo.ID, alice, repo.ID, "feature", "main", "t", "", head, "md", false); err != nil { |
| 258 | t.Fatal(err) |
| 259 | } |
| 260 | var out, errOut bytes.Buffer |
| 261 | c := &control.Ctx{User: store.User{ID: alice, Username: "alice"}, Scope: "full", Store: st, Cfg: cfg, Stdout: &out, Stderr: &errOut} |
| 262 | if code := control.Dispatch(c, []string{"mr", "merge", repo.Path(), "1", "--when-ready"}); code != protocol.ExitOK { |
| 263 | t.Fatalf("queue: exit %d, %s", code, errOut.String()) |
| 264 | } |
| 265 | |
| 266 | f.write("feature.txt", "z\n") |
| 267 | f.git(f.src, "commit", "-q", "-am", "more") |
| 268 | pushed := f.sha("HEAD") |
| 269 | f.sync() |
| 270 | srv.postReceive(Request{RepoID: repo.ID, UserID: tc.user(alice), Scope: tc.scope(repo.ID), |
| 271 | Updates: []policy.RefUpdate{{Ref: "refs/heads/feature", Old: head, New: pushed}}}) |
| 272 | |
| 273 | mr := mustMR(t, st, repo.ID, 1) |
| 274 | if mr.QueuedAt != "" || mr.State != "open" { |
| 275 | t.Fatalf("!1 = state %s queued_at %q, want open and dequeued", mr.State, mr.QueuedAt) |
| 276 | } |
| 277 | cs, _ := st.ListMRComments(mr.ID) |
| 278 | for _, c := range cs { |
| 279 | if c.Kind == "system" && strings.Contains(c.Body, tc.reason) { |
| 280 | return |
| 281 | } |
| 282 | } |
| 283 | t.Fatalf("timeline does not say %q: %+v", tc.reason, cs) |
| 284 | }) |
| 285 | } |
| 286 | } |