Skip to content
quarry Artifact [cbe1883151]
Login

Artifact [cbe1883151]

Artifact cbe188315142d6ebd3b92ed70e44bf0da37fa6ab852f2661a2a5d44c3ca0fd18:


package api

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

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

type memRemovals map[string]string

func (m memRemovals) RecordRemoval(repo, post, reason, actor string) error {
	m[post] = reason
	return nil
}
func (m memRemovals) Removals(string) (map[string]string, error) { return map[string]string(m), nil }

// The author edits and deletes their own post; an admin removes someone
// else's with a reason, logged and told; a member with neither role may
// do neither to another's post.
func TestForumEditAndDelete(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)}}),
		Removals: memRemovals{}, Forum: db}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	out, err := r.Invoke(as("dan", "3"), "forum.post", forumPostIn{Repo: "test", Title: "hello", Body: "first\n"})
	if err != nil {
		t.Fatal(err)
	}
	root := out.(forumPostOut).Hash
	rout, err := r.Invoke(as("eve", "3"), "forum.reply", forumReplyIn{Repo: "test", Root: root, Body: "buy watches\n"})
	if err != nil {
		t.Fatal(err)
	}
	reply := rout.(forumReplyOut).Hash
	posts := func() []forumRendered {
		t.Helper()
		g, err := r.Invoke(as("dan", "2"), "forum.get", forumGetIn{Repo: "test", Root: root})
		if err != nil {
			t.Fatal(err)
		}
		return g.(forumGetOut).Posts
	}

	if _, err := r.Invoke(as("eve", "3"), "forum.edit", forumEditIn{Repo: "test", Post: root, Body: "hijacked\n"}); !errors.Is(err, ops.ErrForbidden) {
		t.Fatalf("FAIL: editing someone else's post: err = %v", err)
	}
	if _, err := r.Invoke(as("dan", "3"), "forum.edit", forumEditIn{Repo: "test", Post: root, Body: "first, fixed\n"}); err != nil {
		t.Fatal(err)
	}
	ps := posts()
	if len(ps) != 2 || !strings.Contains(ps[0].HTML, "first, fixed") || ps[0].User != "dan" || ps[0].Edits != 1 {
		t.Fatalf("FAIL: after the author's edit: %+v", ps)
	}
	t.Log("ok: only the author edits; the thread shows the newest text under the author")

	if _, err := r.Invoke(as("dan", "3"), "forum.delete", forumDeleteIn{Repo: "test", Post: reply}); !errors.Is(err, ops.ErrForbidden) {
		t.Fatalf("FAIL: a non-admin deleting someone else's post: err = %v", err)
	}
	admin := as("mo", "a")
	if _, err := r.Invoke(admin, "forum.delete", forumDeleteIn{Repo: "test", Post: reply}); !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("FAIL: an admin removal with no reason: err = %v", err)
	}
	if _, err := r.Invoke(admin, "forum.delete", forumDeleteIn{Repo: "test", Post: reply, Reason: "spam"}); err != nil {
		t.Fatal(err)
	}
	ps = posts()
	if len(ps) != 2 || !ps[1].Deleted || ps[1].Removed != "spam" || ps[1].User != "eve" || ps[1].EditedBy != "mo" {
		t.Fatalf("FAIL: after the admin's removal: %+v", ps[1])
	}
	if l, _ := db.ModLog("test", 10); len(l) != 1 || l[0].Action != "remove" || l[0].Author != "eve" || l[0].Reason != "spam" {
		t.Fatalf("FAIL: log = %+v", l)
	}
	if _, err := r.Invoke(admin, "forum.delete", forumDeleteIn{Repo: "test", Post: reply, Reason: "again"}); !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("FAIL: deleting a deleted post: err = %v", err)
	}
	t.Log("ok: an admin removes with a reason: tombstone keeps the author, shows the reason, logs it")

	if _, err := r.Invoke(as("dan", "3"), "forum.delete", forumDeleteIn{Repo: "test", Post: root}); err != nil {
		t.Fatal(err)
	}
	ps = posts()
	if l, _ := db.ModLog("test", 10); !ps[0].Deleted || ps[0].Removed != "" || len(l) != 1 {
		t.Fatalf("FAIL: the author's own delete: %+v, log %+v", ps[0], l)
	}
	t.Log("ok: the author deletes their own with no reason and no log line")
}

// Edits beyond the author's own: the person answering for an agent edits
// the agent's posts, a forum moderator (5) edits anyone's. The post says
// who edited it, forum.get hands the source to exactly those who may
// edit, and a moderator's edit is in the moderation log.
func TestForumEditsByAnAgentsOwnerAndAModerator(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() })
	for _, who := range []string{"dmestas", "eve"} {
		if err := db.AddAccount(who, "a-long-enough-password", who == "dmestas"); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.NewAgent("bot", "dmestas", -1); err != nil {
		t.Fatal(err)
	}
	if err := db.NewForgeAgent("zeitgeist", "dmestas"); err != nil {
		t.Fatal(err)
	}
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z, Sync: zitSync(z, ix)}}),
		Removals: memRemovals{}, Forum: db, Accounts: acctAdapter{db}}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	post := func(who, body string) string {
		t.Helper()
		out, err := r.Invoke(as(who, "3"), "forum.post", forumPostIn{Repo: "test", Title: who, Body: body})
		if err != nil {
			t.Fatal(err)
		}
		return out.(forumPostOut).Hash
	}
	get := func(who, letters, root string) forumRendered {
		t.Helper()
		g, err := r.Invoke(as(who, letters), "forum.get", forumGetIn{Repo: "test", Root: root})
		if err != nil {
			t.Fatal(err)
		}
		return g.(forumGetOut).Posts[0]
	}
	edit := func(who, letters, p, body string) error {
		_, err := r.Invoke(as(who, letters), "forum.edit", forumEditIn{Repo: "test", Post: p, Body: body})
		return err
	}

	for _, agent := range []string{"bot", "zeitgeist"} {
		p := post(agent, "an agent wrote this\n")
		if get("dmestas", "3", p).Source == "" || get("eve", "3", p).Source != "" {
			t.Errorf("FAIL: %s's source should go to the person answering for it and nobody else", agent)
		}
		if err := edit("eve", "3", p, "not yours\n"); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: eve edited %s's post: err = %v", agent, err)
		}
		if err := edit("dmestas", "3", p, "fixed by the person who answers for it\n"); err != nil {
			t.Fatalf("FAIL: dmestas editing %s's post: %v", agent, err)
		}
		if g := get("eve", "3", p); g.User != agent || g.EditedBy != "dmestas" || !strings.Contains(g.HTML, "fixed by") {
			t.Errorf("FAIL: after the edit %s's post reads %+v", agent, g)
		}
	}
	t.Log("ok: whoever answers for an agent edits its posts, and the post says who did")

	p := post("eve", "eve's words\n")
	if get("mo", "5", p).Source == "" {
		t.Error("FAIL: a moderator is not handed the source to edit")
	}
	if err := edit("mo", "5", p, "eve's words, moderated\n"); err != nil {
		t.Fatalf("FAIL: a moderator's edit: %v", err)
	}
	if g := get("eve", "3", p); g.User != "eve" || g.EditedBy != "mo" {
		t.Errorf("FAIL: after the moderator's edit: %+v", g)
	}
	l, _ := db.ModLog("test", 10)
	if len(l) != 1 || l[0].Action != "edit" || l[0].Author != "eve" {
		t.Errorf("FAIL: the moderation log reads %+v", l)
	}
	t.Log("ok: a moderator edits anyone's post, marked on the post and in the log")
}