Skip to content
quarry Artifact [5df146b9c5]
Login

Artifact [5df146b9c5]

Artifact 5df146b9c547af8498ecde831792c4abe936bb0ee23ecd83660c04ce2c1f48a6:


package api

import (
	"context"
	"errors"
	"strings"
	"testing"

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

// A removal is the uploader's or a moderator's, and nobody else's.
//
// G-147, found by jva in the hour the attachment surface first became
// testable from a non-admin vantage: b and B sit in the READER preset,
// because attaching is a reader's act in fossil's model, and this op
// took those letters and nothing else. So any reader could permanently
// remove ANY attachment — an administrator's vouched one included —
// while the sibling `reject` refused them, which handed a non-moderator
// the destructive half of moderation and none of the reviewing half.
//
// The three rows are one decision rather than three: removing your own
// upload is part of attaching, removing somebody else's is moderation,
// and the only thing that separates them is the A card's author.
func TestARemovalIsTheUploadersOrAModeratorsAlone(t *testing.T) {
	ix, z, event := attachStore(t)
	ctx := context.Background()
	for _, who := range []string{"lou", "dan"} {
		art, blob, err := z.AttachAdd(testToken, ctx, event, who+".txt", "text/plain", who, "", []byte(who+"'s file\n"))
		if err != nil {
			t.Fatalf("attaching as %s: %v", who, err)
		}
		if err := ix.FoldNow([]index.Parsed{{Hash: art, Art: &artifact.Artifact{
			Type: artifact.Attachment, Date: "2026-01-02T00:00:00", User: who, Mimetype: "text/plain",
			Attachment: &artifact.Attach{Name: who + ".txt", Target: event, Source: blob}}}}); err != nil {
			t.Fatal(err)
		}
	}
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}})}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	reader := as("lou", "bB")    // exactly what the Reader preset carries here
	moderator := as("mo", "bBl") // l is the moderation letter for a technote

	_, err := r.Invoke(reader, "attachment.remove",
		attachmentRemoveIn{Repo: "test", Target: event, Name: "dan.txt"})
	if !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a reader removed somebody else's attachment: %v", err)
	} else if !strings.Contains(err.Error(), "dan") || !strings.Contains(err.Error(), "l capability") {
		t.Errorf("FAIL: the refusal names neither the uploader nor the capability: %v", err)
	}
	if _, err := r.Invoke(reader, "attachment.remove",
		attachmentRemoveIn{Repo: "test", Target: event, Name: "lou.txt"}); err != nil {
		t.Errorf("FAIL: a reader could not remove their OWN attachment: %v", err)
	}
	if _, err := r.Invoke(moderator, "attachment.remove",
		attachmentRemoveIn{Repo: "test", Target: event, Name: "dan.txt"}); err != nil {
		t.Errorf("FAIL: a moderator could not remove another's attachment: %v", err)
	}
	// An anonymous caller matches an anonymous upload's empty author by
	// string alone; two absences are not one person.
	if _, err := r.Invoke(ops.WithCaps(context.Background(), oltp.CapSet("bB")), "attachment.remove",
		attachmentRemoveIn{Repo: "test", Target: event, Name: "dan.txt"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: an anonymous caller removed an attachment: %v", err)
	}
	t.Log("ok: removal is the uploader's or a moderator's")
}