Skip to content
quarry Artifact [d702654f57]
Login

Artifact [d702654f57]

Artifact d702654f57580fbd0a28ffd84c86b3496df10cf21a9e62452e1ab9d6fd8264d0:


package api

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

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

// A repository's uploads do not go to the forge's own bucket.
//
// bucketOf falls back to the forge binding when a repository has none,
// so uploads landed in the operator's storage without anybody being
// told: dmestas attached files to an announcement on quarry and found
// them in his personal R2 (9b8b8fb6). The disclosure half — a repo
// admin handed the endpoint, bucket name and key id — was fixed by
// forgeAnswer. This is the half that decided where the BYTES went.
//
// The refusal names the remedy, because a refusal that does not is the
// other bug this forge keeps finding.
func TestUploadsRefuseTheForgeBucketAndSayWhatToDo(t *testing.T) {
	var d Deps
	// A forge with no bucket store at all still refuses, and says so
	// in its own words rather than falling through to a nil map.
	_, _, _, err := d.boundBucket("alpha")
	if err == nil {
		t.Fatal("FAIL: a forge with no bucket store accepted an upload")
	}
	if !strings.Contains(err.Error(), "binds no buckets") {
		t.Errorf("FAIL: the refusal does not say the forge binds none: %v", err)
	}
}

// The refusal for the forge-fallback case is ErrInvalid and names the
// settings page, so the caller learns what to do rather than that
// something is wrong.
func TestTheForgeBucketRefusalIsActionable(t *testing.T) {
	// Asserted on the message the code produces, because the whole
	// point of this refusal is its words: the previous behaviour was
	// silent success into the wrong account.
	b, rerr := os.ReadFile("attachwrite.go")
	if rerr != nil {
		t.Fatal(rerr)
	}
	src := string(b)
	for _, want := range []string{
		"has no bucket of its own",
		"do not go to the",
		"bind a bucket under Settings > Attachments",
		"ops.ErrInvalid",
	} {
		if !strings.Contains(src, want) {
			t.Errorf("FAIL: the forge-bucket refusal is missing %q; a refusal that does not "+
				"name the remedy is the other half of this class of bug", want)
		}
	}
	if !strings.Contains(src, "if info.Forge {") {
		t.Error("FAIL: boundBucket no longer refuses the forge binding, so a repository's " +
			"uploads go to the operator's storage again")
	}
	// And marks are deliberately left alone.
	if !strings.Contains(src, "Marks are untouched") {
		t.Error("FAIL: the note explaining why avatars still use the forge bucket is gone; " +
			"somebody will remove forgeBucketForAvatar next")
	}
	_ = errors.Is
	_ = ops.ErrInvalid
}

// THE READ AND THE WRITE MUST ASK THE SAME QUESTION.
//
// bucket.limit answered with the binding bucketOf RESOLVED, and bucketOf
// falls back to the forge's when a repository has none. begin then
// refuses that fallback. So the uploader's own question — "will an upload
// work here?" — was answered yes, with a size limit attached, by the
// surface that exists to answer it, and then refused by the surface that
// does the work (G-130).
//
// The privileged composer path had already compensated by also reading
// bucket.get's `forge` flag; this one had nothing to read but `bound`, so
// the fix landed on one path and its neighbour was left. That is the same
// shape as the contributors count and the landing reply count: a fact one
// surface derives correctly and the one beside it does not.
func TestBucketLimitDoesNotOfferAnUploadThatBeginRefuses(t *testing.T) {
	buckets := newMemBuckets()
	// Only the FORGE has storage. A repository with none inherits it —
	// which is exactly the case begin refuses.
	if err := buckets.BindBucket(oltp.ForgeBucket, oltp.BucketBinding{
		Endpoint: "https://s3.example", Bucket: "forge-storage", Region: "auto",
		AccessKey: "AKIA", Secret: "shh", LimitBytes: 1 << 20,
	}); err != nil {
		t.Fatal(err)
	}
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"demo": {}}), Buckets: buckets}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	ctx := context.Background()

	out, err := r.Invoke(ctx, "bucket.limit", bucketLimitIn{Repo: "demo"})
	if err != nil {
		t.Fatal(err)
	}
	got := out.(bucketLimitOut)
	if got.Bound {
		t.Errorf("bucket.limit offers an upload that begin will refuse: %+v\n"+
			"the read answers the binding's question, not the uploader's", got)
	}
	if got.Limit != 0 {
		t.Errorf("a repository that cannot accept uploads was handed a size limit: %d", got.Limit)
	}
	if got.Note == "" {
		t.Error("the read says no without saying why, which is the silent shape this forge keeps removing")
	}

	// The write side must refuse the same case, or this test proves
	// nothing about the two agreeing.
	if _, _, _, err := d.boundBucket("demo"); err == nil {
		t.Fatal("boundBucket admitted the forge fallback, so there was no disagreement to fix")
	}

	// And a repository with storage of its own is bound, and answers
	// with its own limit — the fix must not turn everything unbound.
	if err := buckets.BindBucket("demo", oltp.BucketBinding{
		Endpoint: "https://s3.example", Bucket: "demo-storage", Region: "auto",
		AccessKey: "AKIA", Secret: "shh", LimitBytes: 4096,
	}); err != nil {
		t.Fatal(err)
	}
	mine, err := r.Invoke(ctx, "bucket.limit", bucketLimitIn{Repo: "demo"})
	if err != nil {
		t.Fatal(err)
	}
	if m := mine.(bucketLimitOut); !m.Bound || m.Limit != 4096 {
		t.Errorf("a repository with its own bucket is not reported bound with its own limit: %+v", m)
	}
}