Skip to content
quarry Artifact [2fd26e9ae3]
Login

Artifact [2fd26e9ae3]

Artifact 2fd26e9ae344f07c96e653eb995cbca06ff59965d23544eb7e5888e6db37bd22:


package api

import (
	"errors"
	"path/filepath"
	"testing"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// rosterAccounts is a fixed roster; nothing else on AccountStore is
// reachable from the purge rules.
type rosterAccounts struct {
	AccountStore
	list []Account
}

func (r rosterAccounts) Accounts() ([]Account, error) { return r.list, nil }

type memPurgeGrants map[string]bool

func (m memPurgeGrants) GrantPurge(repo, account, by string) error {
	m[repo+"/"+account] = true
	return nil
}
func (m memPurgeGrants) RevokePurge(repo, account string) error {
	delete(m, repo+"/"+account)
	return nil
}
func (m memPurgeGrants) HasPurgeGrant(repo, account string) bool { return m[repo+"/"+account] }

// Who may purge: a person who administers the repository; the
// repository's curator (an agent run by a site admin) only once a person
// has granted it; no other agent, a forge agent included.
func TestWhoMayPurge(t *testing.T) {
	grants := memPurgeGrants{}
	d := Deps{Version: "v", Repos: testRepos(wikiIndex(t)), PurgeGrants: grants, Redactions: memRedactions{},
		Accounts: rosterAccounts{list: []Account{
			{Name: "dan"}, {Name: "site", Admin: true}, {Name: "jan"},
			{Name: "curator", Operator: "site"}, {Name: "helper", Operator: "jan"},
			{Name: "zeitgeist", Operator: forgeOperator},
		}}}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	may := func(who, letters string) bool { return d.mayPurge(as(who, letters), "test") }
	for _, c := range []struct {
		who, letters string
		want         bool
	}{
		{"dan", "a", true},
		{"dan", "k5", false},
		{"zeitgeist", "a", false},
		{"curator", "a", false},
		{"helper", "a", false},
	} {
		if got := may(c.who, c.letters); got != c.want {
			t.Fatalf("FAIL: before any grant, %s holding %q may purge = %v, want %v", c.who, c.letters, got, c.want)
		}
	}
	t.Log("ok: a person admin may; every agent may not")

	if _, err := r.Invoke(as("curator", "a"), "purge.grant", purgeGrantIn{Repo: "test", Account: "curator"}); !errors.Is(err, ops.ErrForbidden) {
		t.Fatalf("FAIL: an agent granting itself purge: err = %v", err)
	}
	for _, who := range []string{"helper", "zeitgeist", "jan"} {
		if _, err := r.Invoke(as("dan", "a"), "purge.grant", purgeGrantIn{Repo: "test", Account: who}); !errors.Is(err, ops.ErrInvalid) {
			t.Fatalf("FAIL: granting purge to non-curator %s: err = %v", who, err)
		}
	}
	if _, err := r.Invoke(as("dan", "a"), "purge.grant", purgeGrantIn{Repo: "test", Account: "curator"}); err != nil {
		t.Fatal(err)
	}
	if !may("curator", "a") || may("curator", "k") {
		t.Fatal("FAIL: a granted curator: may purge with a, and only with a")
	}
	if _, err := r.Invoke(as("dan", "a"), "purge.grant", purgeGrantIn{Repo: "test", Account: "curator", Revoke: true}); err != nil {
		t.Fatal(err)
	}
	if may("curator", "a") {
		t.Fatal("FAIL: a revoked curator may still purge")
	}
	t.Log("ok: only a person grants; only a curator receives; revoke ends it")

	// The fixture's Home has two versions: aaa3 current, aaa2 before it.
	if _, err := r.Invoke(as("dan", "a"), "wiki.purge", purgeIn{Repo: "test", Name: "Home", Rev: "aaa2", Reason: "leaked token"}); !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("FAIL: purging a version that was never redacted: err = %v", err)
	}
	if _, err := r.Invoke(as("jan", "k5"), "wiki.purge", purgeIn{Repo: "test", Name: "Home", Rev: "aaa2", Reason: "x"}); err == nil {
		t.Fatal("FAIL: a non-admin purged")
	}
	t.Log("ok: purge refuses an unredacted version and a non-admin")
}

// A purge removes the version from the store for good: the history loses
// its row, the text cannot be fetched by anyone, and the log keeps why.
func TestPurgingARedactedWikiVersion(t *testing.T) {
	ix, z, _ := attachStore(t)
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z, Sync: zitSync(z, ix)}}),
		Forum: db, Redactions: db, PurgeGrants: db,
		Accounts: rosterAccounts{list: []Account{{Name: "dan"}}}}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	dan := as("dan", "a")
	var hashes []string
	for _, body := range []string{"token: s3cr3t\n", "token removed\n"} {
		out, err := r.Invoke(dan, "wiki.save", wikiSaveIn{Repo: "test", Name: "Ops", Body: body})
		if err != nil {
			t.Fatal(err)
		}
		hashes = append(hashes, out.(wikiSaveOut).Hash)
	}
	leaked := hashes[0]
	if _, err := r.Invoke(dan, "wiki.redact", redactIn{Repo: "test", Name: "Ops", Rev: leaked[:12], Reason: "leaked token"}); err != nil {
		t.Fatal(err)
	}
	out, err := r.Invoke(dan, "wiki.purge", purgeIn{Repo: "test", Name: "Ops", Rev: leaked[:12], Reason: "leaked token"})
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("shun: %q", out.(purgeOut).Shun)
	h, err := r.Invoke(dan, "wiki.history", wikiHistoryIn{Repo: "test", Name: "Ops"})
	if err != nil {
		t.Fatal(err)
	}
	if hs := h.(wikiHistoryOut); hs.Count != 1 || hs.Edits[0].Hash != hashes[1] || len(hs.Redacted) != 0 {
		t.Fatalf("FAIL: history after purge = %+v", hs)
	}
	if _, err := r.Invoke(dan, "wiki.get", wikiGetIn{Repo: "test", Name: "Ops", Rev: leaked[:12]}); !errors.Is(err, ops.ErrNotFound) {
		t.Fatalf("FAIL: the purged version is still readable: err = %v", err)
	}
	if l, _ := db.ModLog("test", 10); len(l) != 2 || l[0].Action != "purge" || l[0].Reason != "leaked token" {
		t.Fatalf("FAIL: log = %+v", l)
	}
	t.Log("ok: purged version gone from history and reads; the log says why")
}