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)
}
}
// ONE ANSWER TO "WILL AN UPLOAD WORK HERE".
//
// bucket.get answered bound:true for a repository inheriting the forge's
// binding, while bucket.limit answered bound:false and attachment.begin
// refused (G-130, ticket b23f85a0). Three readers of one struct, and
// the one named for the question was the wrong one. bucket.limit had
// been corrected to re-derive "bound and not forge"; the composer had
// grown the same reading; the settings page told the admin their files
// went to forge storage. The fix is that the struct says the right
// thing once, in forgeAnswer, and nobody re-derives it.
//
// Both directions, or an agreement test proves only that everything
// says no.
func TestBucketGetLimitAndBeginAgreeOnWhetherAnUploadWorks(t *testing.T) {
buckets := newMemBuckets()
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)
}
// "own" bound a bucket itself; "inherit" has none and falls back.
if err := buckets.BindBucket("own", oltp.BucketBinding{
Endpoint: "https://s3.example", Bucket: "own-storage", Region: "auto",
AccessKey: "AKIA2", Secret: "shh2", LimitBytes: 2 << 20,
}); err != nil {
t.Fatal(err)
}
d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"own": {}, "inherit": {}}), Buckets: buckets}
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
ctx := context.Background()
for repo, want := range map[string]bool{"own": true, "inherit": false} {
get, err := r.Invoke(ctx, "bucket.get", bucketGetIn{Repo: repo})
if err != nil {
t.Fatal(err)
}
lim, err := r.Invoke(ctx, "bucket.limit", bucketLimitIn{Repo: repo})
if err != nil {
t.Fatal(err)
}
_, _, _, berr := d.boundBucket(repo)
g, l := get.(BucketInfo), lim.(bucketLimitOut)
if g.Bound != want || l.Bound != want || (berr == nil) != want {
t.Errorf("FAIL %s: bucket.get bound=%v, bucket.limit bound=%v, begin admits=%v; want all %v",
repo, g.Bound, l.Bound, berr == nil, want)
} else {
t.Logf("ok %s: get, limit and begin all say uploads work = %v", repo, want)
}
if !want {
// Unbound-because-forge says so, and says why, in the same
// words the upload refusal uses.
if !g.Forge || g.Note == "" || l.Note != g.Note || berr == nil || !strings.Contains(berr.Error(), g.Note) {
t.Errorf("FAIL %s: the three refusals do not share one reason: get={forge:%v note:%q} limit.note=%q begin=%v",
repo, g.Forge, g.Note, l.Note, berr)
} else {
t.Logf("ok %s: one reason, three surfaces", repo)
}
}
}
}