Skip to content
quarry Artifact [f59fb4259e]
Login

Artifact [f59fb4259e]

Artifact f59fb4259e6318ef5700f83405689429c7ec75a7897bbcc6198a20a3bd1480bd:


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 }

// Fossil's rules, narrowed: the author edits and deletes their own post;
// an admin removes someone else's with a reason, logged and told; nobody
// else may do either, and the post never reads as anyone but its author's.
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")
}