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")
}