The draft branch was named after the job. Rerunning a build pushed onto the earlier run's branch, which had review commits, so the push was rejected and the finished build failed at its last step. Separate branches per run would pile up open PRs for the same game instead. A game now has one draft at a time, on draft/<id>. Before pushing, the worker closes the game's open draft PRs (draft/<id>, and draft/<id>-<job> as earlier versions named them) and deletes their branches, then force-pushes draft/<id> and opens the new PR, which says which PR it replaces. Other games' drafts and anyone's non-draft branches are never touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
3.9 KiB
Go
116 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCheckBotNeedsPushAccess(t *testing.T) {
|
|
push := false
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "token secret" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
switch r.URL.Path {
|
|
case "/api/v1/user":
|
|
w.Write([]byte(`{"login":"git_ai_bot","email":"bot@example.com"}`))
|
|
case "/api/v1/repos/org/games":
|
|
if push {
|
|
w.Write([]byte(`{"permissions":{"push":true}}`))
|
|
} else {
|
|
w.Write([]byte(`{"permissions":{"push":false}}`))
|
|
}
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := &workerConfig{gitea: srv.URL, repo: "org/games", botToken: "secret"}
|
|
if err := c.checkBot(); err == nil || !strings.Contains(err.Error(), "git_ai_bot can't push to org/games") {
|
|
t.Errorf("read-only bot: got %v", err)
|
|
}
|
|
push = true
|
|
if err := c.checkBot(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.botLogin != "git_ai_bot" || c.botEmail != "bot@example.com" {
|
|
t.Errorf("identity = %s <%s>", c.botLogin, c.botEmail)
|
|
}
|
|
c.botToken = "wrong"
|
|
if err := c.checkBot(); err == nil || !strings.Contains(err.Error(), "GITEA_TOKEN") {
|
|
t.Errorf("bad token: got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckBotWithRepositoryOnlyToken(t *testing.T) {
|
|
// Gitea answers /user with 403 when the token only has repository scope
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v1/user":
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"message":"token does not have at least one of required scope(s), required=[read:user]"}`))
|
|
case "/api/v1/repos/org/games":
|
|
w.Write([]byte(`{"permissions":{"push":true}}`))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
t.Setenv("BOT_NAME", "gbr-bot")
|
|
c := &workerConfig{gitea: srv.URL, repo: "org/games", botToken: "secret"}
|
|
if err := c.checkBot(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.botLogin != "gbr-bot" || c.botEmail != "gbr-bot@users.noreply" {
|
|
t.Errorf("identity = %s <%s>", c.botLogin, c.botEmail)
|
|
}
|
|
}
|
|
|
|
func TestReplaceOldDraftsClosesOnlyThisGamesDrafts(t *testing.T) {
|
|
var calls []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls = append(calls, r.Method+" "+r.URL.Path)
|
|
switch {
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/org/games/pulls":
|
|
w.Write([]byte(`[
|
|
{"number": 13, "head": {"ref": "draft/example-4b174b2d"}},
|
|
{"number": 20, "head": {"ref": "draft/example"}},
|
|
{"number": 21, "head": {"ref": "draft/example-other"}},
|
|
{"number": 22, "head": {"ref": "draft/other-4b174b2d"}},
|
|
{"number": 23, "head": {"ref": "fix/example-ports"}}
|
|
]`))
|
|
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/repos/org/games/branches/draft/example-4b174b2d":
|
|
w.WriteHeader(http.StatusNoContent)
|
|
case r.Method == http.MethodDelete:
|
|
w.WriteHeader(http.StatusNotFound) // already gone is fine
|
|
default:
|
|
w.Write([]byte(`{}`))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := &workerConfig{gitea: srv.URL, repo: "org/games", botToken: "secret"}
|
|
closed, err := c.replaceOldDrafts("example")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(closed) != 2 || closed[0] != 13 || closed[1] != 20 {
|
|
t.Errorf("closed %v, want [13 20]: not another game's draft, not draft/example-other, not someone's fix branch", closed)
|
|
}
|
|
got := strings.Join(calls, "\n")
|
|
for _, want := range []string{"PATCH /api/v1/repos/org/games/pulls/13", "PATCH /api/v1/repos/org/games/pulls/20",
|
|
"DELETE /api/v1/repos/org/games/branches/draft/example-4b174b2d", "DELETE /api/v1/repos/org/games/branches/draft/example"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("missing %q in:\n%s", want, got)
|
|
}
|
|
}
|
|
for _, unwanted := range []string{"pulls/21", "pulls/22", "pulls/23"} {
|
|
if strings.Contains(got, unwanted) {
|
|
t.Errorf("touched %s:\n%s", unwanted, got)
|
|
}
|
|
}
|
|
}
|