package main
import (
"context"
"net/http"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/ci"
"craftdesign.group/quarry/internal/lease"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/zitcli"
)
// needGitImport skips where this zit cannot keep a Git import current.
func needGitImport(t *testing.T) {
t.Helper()
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not on PATH")
}
out, _ := exec.Command("zit", "import-git", "--help").CombinedOutput()
if !strings.Contains(string(out), "--into") {
t.Skip("this zit has no import-git --into")
}
}
func gitIn(t *testing.T, dir string, args ...string) {
t.Helper()
args = append([]string{"-C", dir, "-c", "[email protected]", "-c", "user.name=A"}, args...)
if out, err := exec.Command("git", args...).CombinedOutput(); err != nil {
t.Fatalf("git %v: %v %s", args, err, out)
}
}
func timelineCount(t *testing.T, store string) int {
t.Helper()
out, err := exec.Command("zit", "timeline", "-R", store, "-n", "100").CombinedOutput()
if err != nil {
t.Fatalf("zit timeline: %v %s", err, out)
}
return strings.Count(string(out), "] ")
}
// #358: a repository made from a Git upstream, then kept current: a
// pull with nothing new changes nothing, new commits arrive, and a
// force-push upstream is taken in rather than refused.
func TestARepositoryFollowsAGitUpstream(t *testing.T) {
needGitImport(t)
up := t.TempDir()
gitIn(t, up, "init", "-q", "-b", "main")
gitIn(t, up, "commit", "-q", "--allow-empty", "-m", "one")
gitIn(t, up, "commit", "-q", "--allow-empty", "-m", "two")
rg := newTestRegistrar(t, 25)
rg.dir = t.TempDir()
rg.gitTestProtocol = "file"
store := t.TempDir() // an empty directory, which Add accepts
spec := api.RepoSpec{Name: "follow", Path: store, Clone: up, Git: true, Mirror: true}
if err := rg.importGitClone(context.Background(), spec); err != nil {
t.Fatalf("FAIL: the first import: %v", err)
}
if n := timelineCount(t, store); n != 2 {
t.Fatalf("FAIL: the import holds %d check-ins, want 2", n)
}
cli := zitcli.New(store)
guard := func(fn func(lease.Token) error) error { return fn(lease.Unguarded("test")) }
if changed, err := rg.pullGit("follow", cli, guard, up); err != nil || changed {
t.Errorf("FAIL: a pull with nothing new: changed %v err %v", changed, err)
}
gitIn(t, up, "commit", "-q", "--allow-empty", "-m", "three")
if changed, err := rg.pullGit("follow", cli, guard, up); err != nil || !changed {
t.Fatalf("FAIL: a pull after a new commit: changed %v err %v", changed, err)
}
if n := timelineCount(t, store); n != 3 {
t.Errorf("FAIL: after the pull the store holds %d check-ins, want 3", n)
}
gitIn(t, up, "reset", "-q", "--hard", "HEAD~1")
gitIn(t, up, "commit", "-q", "--allow-empty", "-m", "rewritten")
if changed, err := rg.pullGit("follow", cli, guard, up); err != nil || !changed {
t.Errorf("FAIL: a pull after a force-push: changed %v err %v", changed, err)
}
// A work tree lost from this node is fetched afresh; the store's
// own import map says what it holds.
if err := exec.Command("rm", "-rf", rg.gitImportDir("follow")).Run(); err != nil {
t.Fatal(err)
}
if _, err := rg.pullGit("follow", cli, guard, up); err != nil {
t.Errorf("FAIL: a pull with no work tree: %v", err)
}
}
// Only the one protocol: an upstream on another is refused by git.
func TestAGitUpstreamSpeaksOnlyHTTPS(t *testing.T) {
needGitImport(t)
up := t.TempDir()
gitIn(t, up, "init", "-q", "-b", "main")
gitIn(t, up, "commit", "-q", "--allow-empty", "-m", "one")
rg := newTestRegistrar(t, 25)
rg.dir = t.TempDir()
err := rg.gitFetch(context.Background(), filepath.Join(t.TempDir(), "w"), up)
if err == nil {
t.Fatal("FAIL: a local-path upstream was fetched with only https allowed")
}
}
// The ops refuse what a Git mirror cannot be: one without an https
// address, and one that takes local writes.
func TestAGitMirrorRefusesWhatItCannotBe(t *testing.T) {
g := newSiteRig(t)
for _, body := range []map[string]any{
{"name": "g1", "git": true},
{"name": "g2", "git": true, "clone": "http://example.org/r.git"},
{"name": "g3", "git": true, "clone": "file:///etc"},
} {
if code, out := g.doAs("alice", http.MethodPost, "/api/v1/repos", doorPassword, "", body); code != http.StatusBadRequest {
t.Errorf("FAIL: %v: %d %s", body, code, out)
}
}
if err := g.db.SetMirror("pub", "https://github.com/alice/pub.git", oltp.MinMirrorInterval); err != nil {
t.Fatal(err)
}
if err := g.db.SetMirrorGit("pub"); err != nil {
t.Fatal(err)
}
code, out := g.doAs("alice", http.MethodPost, "/api/v1/mirrors/writes", doorPassword, "",
map[string]any{"repo": "pub", "accept": true})
if code != http.StatusBadRequest || !strings.Contains(out, "Git") {
t.Errorf("FAIL: a Git mirror took local writes: %d %s", code, out)
}
if m, _, _ := g.db.MirrorFor("pub"); m.AcceptWrites || !m.Git {
t.Errorf("FAIL: the mirror is now %+v", m)
}
}
// No CI verdict tag is written into a Git mirror: the next import
// would refuse the store.
func TestNoVerdictTagIsWrittenIntoAGitMirror(t *testing.T) {
rg := newTestRegistrar(t, 25)
if err := rg.odb.AddRepo("follow", t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := rg.odb.SetMirror("follow", "https://github.com/a/b.git", oltp.MinMirrorInterval); err != nil {
t.Fatal(err)
}
if err := rg.odb.SetMirrorGit("follow"); err != nil {
t.Fatal(err)
}
writes := 0
repos := api.NewRepos(map[string]*api.Repo{"follow": {
Guard: func(fn func(lease.Token) error) error { writes++; return nil },
}})
r := oltp.CIRun{Repo: "follow", Checkin: "abc", RunID: "run1"}
if err := writeVerdict(rg.odb, repos, true, r, ci.Run{Status: "completed"}); err != nil {
t.Fatal(err)
}
if writes != 0 {
t.Errorf("FAIL: a verdict was written into a Git mirror (%d writes)", writes)
}
}