package main
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"net/http"
"net/http/httptest"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/events"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ingest"
"craftdesign.group/quarry/internal/lease"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/xfer"
"craftdesign.group/quarry/internal/zitcli"
)
// firstPushRig is the sync lane as serve wires it for a repository
// created empty (ticket #416): real stores made by `zit init`, the
// forge's token store as the credentials, the project-code cache the
// registrar keeps, and adoption on.
type firstPushRig struct {
rg *registrar
base string
home string
token string
dirs map[string]string
front *xfer.Fronter
// probes counts every question the front put to the forge about
// adopting a code, and claims the claims among them.
probes, claims atomic.Int64
// guarded is the project code each push was leased under, in order.
guarded []string
mu sync.Mutex
}
func newFirstPushRig(t *testing.T, repos ...string) *firstPushRig {
t.Helper()
if _, err := exec.LookPath("zit"); err != nil {
if os.Getenv("CI") == "true" {
t.Fatalf("FAIL: zit not on PATH under CI")
}
t.Skip("zit not on PATH")
}
if v := zitcli.ProbeVersion(); !zitcli.VersionAtLeast(v, zitcli.AdoptProjectVersion) {
t.Skipf("zit %q cannot adopt a project code; needs %s", v, zitcli.AdoptProjectVersion)
}
rg := newTestRegistrar(t, 25)
rg.codes, rg.codeMu = map[string]string{}, &sync.Mutex{}
db := rg.odb
db.SyncProjectCode = func(repo string) (string, bool) {
rg.codeMu.Lock()
defer rg.codeMu.Unlock()
c, ok := rg.codes[repo]
return c, ok
}
if err := db.AddAccount("jan", "pw-jan-12345", false); err != nil {
t.Fatal(err)
}
if err := db.AddAccount("kim", "pw-kim-12345", false); err != nil {
t.Fatal(err)
}
r := &firstPushRig{rg: rg, home: t.TempDir(), dirs: map[string]string{}}
f := xfer.New()
t.Cleanup(f.Close)
for _, name := range repos {
dir := t.TempDir()
r.zit(t, dir, "init")
code, err := xfer.ProjectCode(dir)
if err != nil {
t.Fatal(err)
}
vis := oltp.Private
if strings.HasPrefix(name, "pub") {
vis = oltp.Public
}
if err := db.AddRepo(name, dir, "", vis); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(name, "nolet") {
if err := db.SetCaps("jan", name, "oiy"); err != nil {
t.Fatal(err)
}
}
rg.codes[name] = code
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
if err := db.SetRepoCode(name, code); err != nil {
t.Fatal(err)
}
rg.deps.Repos.Put(name, &api.Repo{Code: code, Index: ix})
f.Register(name, dir)
r.dirs[name] = dir
}
rg.refreshPrivate()
rg.doors = &tokenDoors{db: db}
f.SetAuth(db)
f.SetLoginLimit(rg.doors)
f.SetReadGate(rg.SyncReadGate)
ad := rg.adoption(func(repo string) (string, bool) { d, ok := r.dirs[repo]; return d, ok })
inner := *ad
ad.Empty = func(repo string) bool { r.probes.Add(1); return inner.Empty(repo) }
ad.Recorded = func(repo string) string { r.probes.Add(1); return inner.Recorded(repo) }
ad.Claim = func(repo, code string) xfer.Claim { r.probes.Add(1); r.claims.Add(1); return inner.Claim(repo, code) }
ad.Adopted = func(repo string) bool { r.probes.Add(1); return inner.Adopted(repo) }
f.SetAdoption(ad)
r.front = f
mux := http.NewServeMux()
mux.Handle("POST /r/{repo}", f)
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
r.base = srv.URL + "/r/"
v, _, err := db.MintToken("jan", oltp.TokenSpec{Name: "t", Scopes: []string{oltp.ScopeSync}})
if err != nil {
t.Fatal(err)
}
r.token = v
return r
}
// lease puts the pushes under a real lease, as serve does, and records
// the code each was leased under.
func (r *firstPushRig) lease(t *testing.T) *lease.Manager {
t.Helper()
ev, err := events.Start(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(ev.Close)
m, err := lease.New(ev.JS(), "node-a", 5*time.Second)
if err != nil {
t.Fatal(err)
}
t.Cleanup(m.Close)
r.rg.leases = m
r.front.SetGuard(func(repo string, proxy func() error) error {
r.mu.Lock()
r.guarded = append(r.guarded, r.rg.codeOr(repo, ""))
r.mu.Unlock()
return r.rg.guardPush(repo, proxy)
})
return m
}
// fold brings the repository's index up to its store, as the forge's
// poll does after a push.
func (r *firstPushRig) fold(t *testing.T, repo string) {
t.Helper()
ev, err := events.Start(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer ev.Close()
e, _ := r.rg.deps.Repos.Get(repo)
if _, err := ingest.NewPipeline(zitcli.New(r.dirs[repo]), ev, e.Index, repo, "test").Reindex(); err != nil {
t.Fatalf("FAIL: folding %s: %v", repo, err)
}
}
func (r *firstPushRig) zit(t *testing.T, dir string, args ...string) (string, error) {
t.Helper()
cmd := exec.Command("zit", args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "HOME="+r.home, "ZIT_PASSWORD=")
out, err := cmd.CombinedOutput()
return string(out), err
}
func (r *firstPushRig) mustZit(t *testing.T, dir string, args ...string) string {
t.Helper()
out, err := r.zit(t, dir, args...)
if err != nil {
t.Fatalf("FAIL: zit %v: %v\n%s", args, err, out)
}
return out
}
// project makes a checkout of a project of its own with n check-ins.
func (r *firstPushRig) project(t *testing.T, n int) string {
t.Helper()
dir := t.TempDir()
r.mustZit(t, dir, "init")
for i := 0; i < n; i++ {
r.commit(t, dir)
}
return dir
}
func (r *firstPushRig) commit(t *testing.T, dir string) {
t.Helper()
name := filepath.Base(t.TempDir()) + ".txt"
if err := os.WriteFile(filepath.Join(dir, name), []byte(name+"\n"), 0o644); err != nil {
t.Fatal(err)
}
r.mustZit(t, dir, "add", name)
r.mustZit(t, dir, "commit", "-m", "c "+name)
}
func (r *firstPushRig) push(t *testing.T, dir, repo, user, token string) (string, error) {
t.Helper()
return r.zit(t, dir, "push", r.base+repo, "--user", user, "--password", token)
}
// state is a store's project code and artifact count.
func (r *firstPushRig) state(t *testing.T, dir string) (string, int) {
t.Helper()
code, n, err := xfer.StoreState(dir)
if err != nil {
t.Fatal(err)
}
return code, n
}
func (r *firstPushRig) checkins(t *testing.T, dir string) int {
t.Helper()
out := r.mustZit(t, dir, "timeline", "-n", "100", "-F", "oneline")
n := 0
for _, line := range strings.Split(out, "\n") {
if strings.TrimSpace(line) != "" {
n++
}
}
return n
}
// The first push of an existing project into a repository created
// empty is authenticated against the pusher's project code, the store
// takes that code, and every cache of it follows before the next push.
func TestAnEmptyRepositoryAdoptsTheFirstPushedProject(t *testing.T) {
r := newFirstPushRig(t, "vault", "other")
proj := r.project(t, 2)
projCode, _ := r.state(t, proj)
orig, n0 := r.state(t, r.dirs["vault"])
if n0 != 0 || orig == projCode {
t.Fatalf("FAIL: rig: vault holds %d artifacts, code %.8s", n0, orig)
}
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: the first push was refused: %v\n%s", err, out)
}
code, n1 := r.state(t, r.dirs["vault"])
if code != projCode || n1 == 0 {
t.Fatalf("FAIL: after the first push vault has code %.8s and %d artifacts; want %.8s and some", code, n1, projCode)
}
if got, _ := r.rg.deps.Repos.Get("vault"); got.Code != projCode {
t.Errorf("FAIL: the registry entry still carries %.8s", got.Code)
}
if c := r.rg.codeOr("vault", ""); c != projCode {
t.Errorf("FAIL: the code cache still carries %.8s", c)
}
// The second push is judged by the adopted code: a stale cache
// fails here.
r.commit(t, proj)
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: the second push was refused: %v\n%s", err, out)
}
_, n2 := r.state(t, r.dirs["vault"])
if n2 <= n1 {
t.Errorf("FAIL: the second push stored nothing (%d then %d artifacts)", n1, n2)
}
// A fresh clone has the adopted code and all three check-ins.
cl := filepath.Join(t.TempDir(), "clone")
if out, err := r.zit(t, ".", "clone", r.base+"vault", cl, "--user", "jan", "--password", r.token); err != nil {
t.Fatalf("FAIL: clone: %v\n%s", err, out)
}
if c, _ := r.state(t, cl); c != projCode {
t.Errorf("FAIL: the clone has code %.8s, want %.8s", c, projCode)
}
if got, want := r.checkins(t, cl), r.checkins(t, proj); got != want {
t.Errorf("FAIL: the clone has %d timeline lines, the pusher %d", got, want)
}
// A different project pushed to the now non-empty repository is
// refused, and the store is as it was.
r.fold(t, "vault")
other := r.project(t, 1)
claimsBefore := r.claims.Load()
if out, err := r.push(t, other, "vault", "jan", r.token); err == nil {
t.Errorf("FAIL: a different project was pushed into a non-empty repository:\n%s", out)
}
if c, n := r.state(t, r.dirs["vault"]); c != projCode || n != n2 {
t.Errorf("FAIL: the refused push changed vault to %.8s, %d artifacts (was %d)", c, n, n2)
}
// ...and was stopped by the forge's own check, before zit was asked.
// A forge that left it to zit would have claimed the code first.
if got := r.claims.Load(); got != claimsBefore {
t.Errorf("FAIL: a project code was claimed for a repository that is not empty")
}
if rec, _ := r.rg.odb.RepoCode("vault"); rec != projCode {
t.Errorf("FAIL: vault's row records %.8s, want %.8s", rec, projCode)
}
// The same project is held by vault now, so another empty repository
// does not take it: chat and the write lease are keyed by the code.
otherOrig, _ := r.state(t, r.dirs["other"])
taken, err := r.push(t, proj, "other", "jan", r.token)
if err == nil {
t.Errorf("FAIL: a project code another repository holds was adopted:\n%s", taken)
}
if bad, _ := r.push(t, proj, "other", "jan", "qat_wrong"); bad == taken {
t.Errorf("FAIL: a proved caller whose project is already hosted is told what any bad credential is told")
}
if c, n := r.state(t, r.dirs["other"]); c != otherOrig || n != 0 {
t.Errorf("FAIL: the refused push changed other to %.8s, %d artifacts", c, n)
}
if !t.Failed() {
t.Log("ok")
}
}
// A first push that does not verify is refused exactly as any other
// that does not, whoever it names, and leaves the empty store alone.
func TestAnUnverifiedFirstPushIsRefusedAlikeAndChangesNothing(t *testing.T) {
r := newFirstPushRig(t, "empty", "full")
// "full" holds a project already, so a push of another is judged
// the ordinary way.
seed := r.project(t, 1)
if out, err := r.push(t, seed, "full", "jan", r.token); err != nil {
t.Fatalf("FAIL: seeding: %v\n%s", err, out)
}
proj := r.project(t, 1)
orig, _ := r.state(t, r.dirs["empty"])
refusal := func(repo, user, token string) string {
out, err := r.push(t, proj, repo, user, token)
if err == nil {
t.Fatalf("FAIL: %s as %s with %.6s... was accepted:\n%s", repo, user, token, out)
}
return strings.ReplaceAll(strings.TrimSpace(out), user, "USER")
}
// The wrong token for an account that exists, the right token for
// an account that holds none, and a name that is no account.
wrong := refusal("empty", "jan", "qat_wrong")
if got := refusal("empty", "kim", r.token); got != wrong {
t.Errorf("FAIL: an account without the token is told differently from a wrong token:\n%s\n%s", got, wrong)
}
if got := refusal("empty", "nobody", "qat_wrong"); got != wrong {
t.Errorf("FAIL: a name that is no account is told differently:\n%s\n%s", got, wrong)
}
if got := refusal("full", "jan", "qat_wrong"); got != wrong {
t.Errorf("FAIL: an empty repository refuses differently from a non-empty one:\n%s\n%s", got, wrong)
}
if c, n := r.state(t, r.dirs["empty"]); c != orig || n != 0 {
t.Errorf("FAIL: refused pushes left empty at %.8s with %d artifacts", c, n)
}
if !t.Failed() {
t.Log("ok")
}
}
// A malformed project code in the card derives no secret to check, so
// it is refused like any unverified card and adopts nothing.
func TestAMalformedProjectCodeIsNeverAdopted(t *testing.T) {
for _, code := range []string{"", "abc", strings.Repeat("g", 40), strings.ToUpper(strings.Repeat("a", 40)), strings.Repeat("a", 41)} {
if xfer.ValidProjectCode(code) {
t.Errorf("FAIL: %q taken for a project code", code)
}
}
for _, code := range []string{strings.Repeat("a", 40), strings.Repeat("0f", 32)} {
if !xfer.ValidProjectCode(code) {
t.Errorf("FAIL: %q refused as a project code", code)
}
}
if !t.Failed() {
t.Log("ok")
}
}
// The first contact must be a push: a pull or a sync of a project the
// empty repository does not hold is refused by zit, and changes nothing.
func TestAFirstPullOrSyncIsRefusedByDesign(t *testing.T) {
r := newFirstPushRig(t, "empty")
proj := r.project(t, 1)
orig, _ := r.state(t, r.dirs["empty"])
for _, verb := range []string{"pull", "sync"} {
out, err := r.zit(t, proj, verb, r.base+"empty", "--user", "jan", "--password", r.token)
if err == nil {
t.Errorf("FAIL: a first %s was accepted:\n%s", verb, out)
}
if c, n := r.state(t, r.dirs["empty"]); c != orig || n != 0 {
t.Errorf("FAIL: a first %s left empty at %.8s with %d artifacts", verb, c, n)
}
}
// ...and the push that follows is accepted.
if out, err := r.push(t, proj, "empty", "jan", r.token); err != nil {
t.Errorf("FAIL: the push after the refused pull: %v\n%s", err, out)
}
if !t.Failed() {
t.Log("ok")
}
}
// post sends a raw sync body and returns what came back, status and all.
func (r *firstPushRig) post(t *testing.T, repo, body string) string {
t.Helper()
resp, err := http.Post(r.base+repo, "application/x-fossil", bytes.NewReader([]byte(body)))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return resp.Status + "\n" + string(b)
}
// A caller who has proved nothing starts nothing: a push naming another
// project, anonymous or with a login that does not verify, is put no
// question about adopting it, whether the repository is empty and public,
// private, or does not exist, and a private repository answers exactly as
// one that is not there.
func TestAnUnprovedFirstPushAsksNothingAndPrivateIsAbsent(t *testing.T) {
r := newFirstPushRig(t, "pubempty", "vault")
foreign := strings.Repeat("ab", 20)
server := strings.Repeat("cd", 20)
cards := map[string]string{
"anonymous": "push " + server + " " + foreign + "\n",
"badlogin": "login jan 0123456789abcdef " + strings.Repeat("0", 40) + "\npush " + server + " " + foreign + "\n",
"nonmember": "login nobody 0123456789abcdef " + strings.Repeat("1", 40) + "\npush " + server + " " + foreign + "\n",
"mixedcodes": "login jan 0123456789abcdef " + strings.Repeat("2", 40) + "\npush " + server + " " + foreign + "\npush " + server + " " + server + "\n",
}
for name, body := range cards {
got := map[string]string{}
for _, repo := range []string{"pubempty", "vault", "ghost"} {
got[repo] = r.post(t, repo, body)
}
if got["vault"] != got["ghost"] {
t.Errorf("FAIL: %s: a private repository answers differently from one that is not there:\n%s\n%s", name, got["vault"], got["ghost"])
}
}
if n := r.probes.Load(); n != 0 {
t.Errorf("FAIL: %d questions were put to the forge for callers who proved nothing", n)
}
// The same through a real client with a wrong token.
proj := r.project(t, 1)
for _, repo := range []string{"pubempty", "vault", "ghost"} {
r.push(t, proj, repo, "jan", "qat_wrong")
}
if n := r.probes.Load(); n != 0 {
t.Errorf("FAIL: %d questions were put to the forge for a wrong token", n)
}
if !t.Failed() {
t.Log("ok")
}
}
// The push is leased under the project code the forge holds when it
// arrives, and once the store has adopted another the lease moves to it
// and the old key is given up.
func TestTheLeaseMovesToTheAdoptedCode(t *testing.T) {
r := newFirstPushRig(t, "vault")
m := r.lease(t)
orig, _ := r.state(t, r.dirs["vault"])
proj := r.project(t, 2)
adopted, _ := r.state(t, proj)
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: the first push: %v\n%s", err, out)
}
r.mu.Lock()
first := append([]string(nil), r.guarded...)
r.mu.Unlock()
if len(first) == 0 || first[0] != orig {
t.Errorf("FAIL: the first push was leased under %v, want %.8s first", first, orig)
}
if m.Holds(orig) {
t.Errorf("FAIL: the lease on the old code is still held")
}
r.commit(t, proj)
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: the second push: %v\n%s", err, out)
}
r.mu.Lock()
later := append([]string(nil), r.guarded[len(first):]...)
r.mu.Unlock()
if len(later) == 0 {
t.Fatalf("FAIL: the second push took no lease")
}
for _, c := range later {
if c != adopted {
t.Errorf("FAIL: a later push was leased under %.8s, want %.8s", c, adopted)
}
}
if !m.Holds(adopted) || m.Holds(orig) {
t.Errorf("FAIL: holds adopted=%v old=%v", m.Holds(adopted), m.Holds(orig))
}
if !t.Failed() {
t.Log("ok")
}
}
// A cache that did not follow the store is put right by the next push
// that names the store's real code, not left to refuse every push.
func TestAStaleCodeCacheHealsOnTheNextPush(t *testing.T) {
r := newFirstPushRig(t, "vault")
orig, _ := r.state(t, r.dirs["vault"])
proj := r.project(t, 1)
adopted, _ := r.state(t, proj)
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: the first push: %v\n%s", err, out)
}
// As if the read after the adoption had failed: the caches say the
// old code, the row says the claim.
r.rg.codeMu.Lock()
r.rg.codes["vault"] = orig
r.rg.codeMu.Unlock()
r.commit(t, proj)
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: a push after a stale cache was refused: %v\n%s", err, out)
}
if c := r.rg.codeOr("vault", ""); c != adopted {
t.Errorf("FAIL: the cache holds %.8s after the heal, want %.8s", c, adopted)
}
if !t.Failed() {
t.Log("ok")
}
}
// A store that cannot be read when a push is answered is read again by
// itself.
func TestACodeThatCannotBeReadIsReadAgain(t *testing.T) {
r := newFirstPushRig(t, "vault")
dir := filepath.Join(t.TempDir(), "later")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
r.rg.codeMu.Lock()
r.rg.codes["late"] = "stale"
r.rg.codeMu.Unlock()
if r.rg.refreshCode("late", dir) {
t.Fatalf("FAIL: an unreadable store was reported read")
}
r.mustZit(t, dir, "init")
want, _ := r.state(t, dir)
deadline := time.Now().Add(15 * time.Second)
for r.rg.codeOr("late", "") != want {
if time.Now().After(deadline) {
t.Fatalf("FAIL: the code was never read again")
}
time.Sleep(100 * time.Millisecond)
}
if !t.Failed() {
t.Log("ok")
}
}
type purgeRecorder struct {
api.ChatPlane
mu sync.Mutex
purged []string
}
func (p *purgeRecorder) PurgeChannel(code string) error {
p.mu.Lock()
defer p.mu.Unlock()
p.purged = append(p.purged, code)
return nil
}
// What was keyed by the code the store had before adopting another goes
// with it: its chat channel and robot lines are erased, only for the old
// code, and a code another repository still has is left alone.
func TestAdoptionErasesWhatWasKeyedByTheOldCode(t *testing.T) {
r := newFirstPushRig(t, "vault", "twin")
chat := &purgeRecorder{}
r.rg.deps.Chat = chat
orig, _ := r.state(t, r.dirs["vault"])
if err := r.rg.odb.RecordRobotLine(orig, 1, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
t.Fatal(err)
}
proj := r.project(t, 1)
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: push: %v\n%s", err, out)
}
chat.mu.Lock()
purged := append([]string(nil), chat.purged...)
chat.mu.Unlock()
if len(purged) != 1 || purged[0] != orig {
t.Errorf("FAIL: channels erased = %v, want only the old code", purged)
}
if n, err := r.rg.odb.RobotLines(orig, 0); err != nil || n != 0 {
t.Errorf("FAIL: %d robot lines remain under the old code (%v)", n, err)
}
// A repository that still has the old code keeps its channel.
r.rg.codeMu.Lock()
r.rg.codes["twin"] = "kept"
r.rg.codeMu.Unlock()
chat.purged = nil
r.rg.retire("vault", "kept")
if len(chat.purged) != 0 {
t.Errorf("FAIL: a channel another repository still has was erased")
}
if !t.Failed() {
t.Log("ok")
}
}
// signed frames rest as a request logged in as user with token, signing
// with the secret derived from project code (RFC ยง6.1-6.3), the way a
// sync client does; with a garbage token it is a card that does not
// verify anywhere.
func signed(code, user, token, rest string) string {
nonce := sha1.Sum([]byte(rest))
nonceHex := hex.EncodeToString(nonce[:])
secret := xfer.SharedSecret(code, user, token)
sig := sha1.Sum([]byte(nonceHex + secret))
return "login " + user + " " + nonceHex + " " + hex.EncodeToString(sig[:]) + "\n" + rest
}
// A login that verifies only under the project code its own card names
// is no login: replayed against a private mirror, a private repository
// that holds something, or an empty one its account has no letter on, it
// is answered byte for byte as a card that does not verify, and claims
// nothing.
func TestALoginThatVerifiesOnlyUnderTheCardsCodeConfersNothing(t *testing.T) {
r := newFirstPushRig(t, "mirror", "full", "nolet")
seed := r.project(t, 1)
for _, repo := range []string{"mirror", "full"} {
// Each holds a different project, as a mirror and a repository
// that has been pushed to do.
p := r.project(t, 1)
if out, err := r.push(t, p, repo, "jan", r.token); err != nil {
t.Fatalf("FAIL: seeding %s: %v\n%s", repo, err, out)
}
r.fold(t, repo)
}
_ = seed
r.front.SetWriteGate(func(repo string) (bool, string) { return repo != "mirror", "https://upstream.example/x" })
claims := r.claims.Load()
// The project of the captured request: a code no repository here has.
foreign := strings.Repeat("ab", 20)
server := strings.Repeat("cd", 20)
rest := "push " + server + " " + foreign + "\npull " + server + " " + foreign + "\n"
for _, repo := range []string{"mirror", "full", "nolet"} {
replay := r.post(t, repo, signed(foreign, "jan", r.token, rest))
unverified := r.post(t, repo, signed(foreign, "jan", "qat_not_the_token", rest))
if replay != unverified {
t.Errorf("FAIL: %s: a card verifying under its own project code was answered differently from one that verifies nowhere:\n%s\n---\n%s", repo, replay, unverified)
}
anon := r.post(t, repo, rest)
if replay != anon && strings.Contains(replay, "igot") {
t.Errorf("FAIL: %s: the replay was served a pull", repo)
}
}
// Verified, with the letter, the forge looks at emptiness and finds
// the two that hold something not empty; nothing is claimed.
if got := r.claims.Load(); got != claims {
t.Errorf("FAIL: %d codes were claimed for repositories that hold something or are not the account's", got-claims)
}
if !t.Failed() {
t.Log("ok")
}
}
// Rows that cannot hold an unrecorded code do not stand in the way of a
// first push: a failed import is a record, an import still running has no
// store to compare yet.
func TestAFailedOrCloningRowDoesNotBlockAdoption(t *testing.T) {
r := newFirstPushRig(t, "vault")
for name, state := range map[string]string{"failedimp": oltp.Failed, "cloningimp": oltp.Cloning} {
if err := r.rg.odb.AddRepo(name, t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
if err := r.rg.odb.SetRepoState(name, state, "x"); err != nil {
t.Fatal(err)
}
}
proj := r.project(t, 1)
projCode, _ := r.state(t, proj)
if out, err := r.push(t, proj, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: with a failed and a cloning row present the first push was refused: %v\n%s", err, out)
}
if c, _ := r.state(t, r.dirs["vault"]); c != projCode {
t.Errorf("FAIL: vault has code %.8s, want %.8s", c, projCode)
}
if !t.Failed() {
t.Log("ok")
}
}
// A Ready row with no recorded code is given the one its store has, so it
// stops being a possible holder of a code nobody can name.
func TestAReadyRowWithNoCodeIsBackfilled(t *testing.T) {
r := newFirstPushRig(t, "vault")
dir := t.TempDir()
r.mustZit(t, dir, "init")
want, _ := r.state(t, dir)
if err := r.rg.odb.AddRepo("older", dir, "", oltp.Public); err != nil {
t.Fatal(err)
}
if got, _ := r.rg.odb.RepoCode("older"); got != "" {
t.Fatalf("FAIL: rig: older already records %q", got)
}
r.rg.backfillCodes()
if got, _ := r.rg.odb.RepoCode("older"); got != want {
t.Errorf("FAIL: older records %.8s after the backfill, want %.8s", got, want)
}
if !t.Failed() {
t.Log("ok")
}
}
// A shared project code is logged when either repository is private and
// not when both are public, and only once.
func TestASharedCodeWarnsOnlyWhenOneSidePrivate(t *testing.T) {
r := newFirstPushRig(t, "a", "b")
var buf bytes.Buffer
log.SetOutput(&buf)
t.Cleanup(func() { log.SetOutput(os.Stderr) })
odb := r.rg.odb
add := func(name, vis string) {
if err := odb.AddRepo(name, t.TempDir(), "", vis); err != nil {
t.Fatal(err)
}
if err := odb.SetRepoCode(name, "SHARED"); err != nil {
t.Fatal(err)
}
}
add("pub1", oltp.Public)
add("pub2", oltp.Public)
r.rg.warnSharedCode("pub2", "SHARED")
if strings.Contains(buf.String(), "WARNING") {
t.Errorf("FAIL: two public repositories sharing a code were warned about")
}
add("priv", oltp.Private)
r.rg.warnSharedCode("priv", "SHARED")
r.rg.warnSharedCode("priv", "SHARED")
got := buf.String()
if !strings.Contains(got, "priv") || !strings.Contains(got, "pub1") {
t.Errorf("FAIL: no warning naming both repositories: %q", got)
}
if n := strings.Count(got, "WARNING: repositories priv (private) and pub1"); n != 1 {
t.Errorf("FAIL: the pair priv/pub1 was logged %d times, want once", n)
}
if !t.Failed() {
t.Log("ok")
}
}
// quarry ticket 431: a first push of a Git import (sub-manifests on, as
// import-git writes by default) was adopted, and the forge's live round
// then indexed only the flat check-ins at the bottom of its history:
// the repository answered indexBehind until someone reindexed it. The
// round after the push must index the pushed tip on its own.
func TestAnAdoptedGitImportIsIndexedWithoutAReindex(t *testing.T) {
r := newFirstPushRig(t, "vault")
needGitImport(t)
t.Setenv("GIT_AUTHOR_DATE", "2001-01-01T00:00:00Z")
t.Setenv("GIT_COMMITTER_DATE", "2001-01-01T00:00:00Z")
up := t.TempDir()
gitIn(t, up, "init", "-q", "-b", "main")
const commits = 12
for i := range commits {
dir := filepath.Join(up, fmt.Sprintf("d%d", i%3))
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%d.txt", i)), []byte(fmt.Sprintln(i)), 0o644); err != nil {
t.Fatal(err)
}
gitIn(t, up, "add", "-A")
gitIn(t, up, "commit", "-q", "-m", fmt.Sprint("commit ", i))
}
imported := filepath.Join(t.TempDir(), "imported")
// k=1: small leaves, so the history is mostly tree check-ins.
r.mustZit(t, up, "import-git", up, imported, "--submanifest-k", "1", "--quiet")
tip := strings.Fields(r.mustZit(t, imported, "info", "trunk"))[0]
if raw := r.mustZit(t, imported, "artifact", tip, "--raw"); !strings.Contains(raw, "\nS ") {
t.Fatalf("rig: the imported tip is not a tree check-in:\n%s", raw)
}
// The repository's index as the forge follows it: the creation
// round, then the poll's first tick, which always runs one.
ev, err := events.Start(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(ev.Close)
e, _ := r.rg.deps.Repos.Get("vault")
pipe := ingest.NewPipeline(zitcli.New(r.dirs["vault"]), ev, e.Index, "vault", "test")
for range 2 {
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
}
if out, err := r.push(t, imported, "vault", "jan", r.token); err != nil {
t.Fatalf("FAIL: the first push was refused: %v\n%s", err, out)
}
if got, _ := r.rg.deps.Repos.Get("vault"); got.Code == e.Code {
t.Fatalf("rig: the push was not adopted")
}
if _, err := pipe.Sync(); err != nil {
t.Fatalf("FAIL: the round after the push: %v", err)
}
got, _ := r.rg.deps.Repos.Get("vault")
if _, _, ok, err := got.Index.CheckinByPrefix(tip); err != nil || !ok {
t.Errorf("FAIL: the index lacks the pushed tip %.12s (%v): the repository answers indexBehind", tip, err)
}
if n, err := got.Index.CheckinCount(); err != nil || n < commits {
t.Errorf("FAIL: %d check-ins indexed (%v), want at least %d", n, err, commits)
}
if !t.Failed() {
t.Log("ok")
}
}