package api
import (
"context"
"errors"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/timing"
)
// A mirror can carry an A card whose blob its upstream never served:
// all six attachments on tcl's ticket 5d170b5c are like that. The head
// answer "not in this store" was never remembered, so every render of
// that page asked the store six times, warm or cold. It is remembered
// until the next fold — the only way the blob can arrive — and after a
// fold the store is asked again.
func TestAMissingAttachmentIsNotAskedForAgainUntilAFold(t *testing.T) {
ix, z, event := attachStore(t)
ghost := strings.Repeat("d", 40)
if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat("a", 40), Art: &artifact.Artifact{
Type: artifact.Attachment, Date: "2026-01-02T00:00:00", User: "dan",
Attachment: &artifact.Attach{Name: "gone.txt", Target: event, Source: ghost},
}}}); err != nil {
t.Fatal(err)
}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}})}); err != nil {
t.Fatal(err)
}
// head answers whether the ask reached the store.
head := func(what string) bool {
ctx, tm := timing.With(context.Background())
_, err := r.Invoke(ctx, "attachment.head", attachmentHeadIn{Repo: "test", Hash: ghost})
if !errors.Is(err, ops.ErrNotFound) {
t.Fatalf("FAIL: %s: a blob the store lacks answered %v, want not found", what, err)
}
return tm.Category(timing.Zit) > 0
}
if !head("first ask") {
t.Fatal("FAIL: the first ask never reached the store")
}
if head("second ask") {
t.Fatal("FAIL: the second ask went back to the store for a blob it already knew was missing")
}
if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat("b", 40), Art: &artifact.Artifact{
Type: artifact.Wiki, WikiTitle: "Later", Date: "2026-01-03T00:00:00", User: "dan", Body: []byte("x\n")}}}); err != nil {
t.Fatal(err)
}
if !head("after a fold") {
t.Fatal("FAIL: after a fold the missing verdict was still served; the blob may have arrived")
}
t.Log("ok: a missing blob is remembered until the next fold, then asked for again")
}
// #273: zit carries no attachment content, so a Fossil mirror lacks the
// bytes of attachments its upstream never sent as artifacts. The head
// says the file is held upstream and where, with no credentials from
// the mirror's address; a repository that follows nothing still says
// not found, and so does one that follows Git, which has no attachments.
func TestAMirrorsMissingAttachmentPointsUpstream(t *testing.T) {
ix, z, event := attachStore(t)
ghost := strings.Repeat("e", 40)
if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat("c", 40), Art: &artifact.Artifact{
Type: artifact.Attachment, Date: "2026-01-02T00:00:00", User: "dan",
Attachment: &artifact.Attach{Name: "test.result", Target: event, Source: ghost},
}}}); err != nil {
t.Fatal(err)
}
head := func(m memMirrors) (attachmentHeadOut, error) {
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Mirrors: m, Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}})}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(context.Background(), "attachment.head", attachmentHeadIn{Repo: "test", Hash: ghost})
if err != nil {
return attachmentHeadOut{}, err
}
return out.(attachmentHeadOut), nil
}
got, err := head(memMirrors{"test": {Repo: "test", URL: "https://anon:[email protected]/tcl/", Enabled: true}})
if err != nil || got.Kind != attachKindMissing || got.Upstream != "https://core.tcl-lang.org/tcl/raw/"+ghost {
t.Fatalf("FAIL: a Fossil mirror's missing file answered %+v, %v", got, err)
}
if _, err := head(memMirrors{}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: a repository that follows nothing answered %v, want not found", err)
}
if _, err := head(memMirrors{"test": {Repo: "test", URL: "https://github.com/x/y", Git: true, Enabled: true}}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: a Git mirror answered %v, want not found", err)
}
}