package api
import (
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/attach"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
type memMirrorAttach struct {
mu sync.Mutex
rows map[string]oltp.MirrorAttach
}
func (m *memMirrorAttach) MirrorAttachment(repo, src string) (oltp.MirrorAttach, bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.rows[repo+"/"+src]
return r, ok, nil
}
func (m *memMirrorAttach) RecordMirrorAttachment(r oltp.MirrorAttach) error {
m.mu.Lock()
defer m.mu.Unlock()
m.rows[r.Repo+"/"+r.Src] = r
return nil
}
func (m *memMirrorAttach) MirrorAttachSettled(repo string, cutoff time.Time) (map[string]bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := map[string]bool{}
for _, r := range m.rows {
if r.Repo == repo && r.State != oltp.MirrorAttachFailed {
out[r.Src] = true
}
}
return out, nil
}
// #273: a Fossil mirror with storage fetches an attachment its store
// lacks from the upstream, keeps it only when it hashes to the A card's
// src, stores it in the bucket by SHA-256, and from then on serves it
// as an external file. Wrong bytes from the upstream are refused and
// recorded, and never reach the bucket.
func TestAMirrorFetchesMissingAttachmentsIntoItsBucket(t *testing.T) {
ix, z, event := attachStore(t)
good := []byte("all tests passed\n")
sum := sha1.Sum(good)
src := hex.EncodeToString(sum[:])
liar := strings.Repeat("f", 40)
for i, s := range []string{src, liar} {
if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat(string(rune('a'+i)), 40), Art: &artifact.Artifact{
Type: artifact.Attachment, Date: "2026-01-0" + string(rune('2'+i)) + "T00:00:00", User: "dan",
Attachment: &artifact.Attach{Name: "test" + string(rune('0'+i)) + ".result", Target: event, Source: s},
}}}); err != nil {
t.Fatal(err)
}
}
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/tcl/raw/" + src:
w.Write(good)
case "/tcl/raw/" + liar:
w.Write([]byte("not what the card names\n"))
default:
http.NotFound(w, r)
}
}))
t.Cleanup(upstream.Close)
s3srv, objects := newObjectS3(t)
buckets := newMemBuckets()
buckets.rows["test"] = oltp.BucketBinding{Endpoint: s3srv.URL, Bucket: "files", Region: "auto", AccessKey: "AK", Secret: "SK", LimitBytes: 1 << 20}
store := &memMirrorAttach{rows: map[string]oltp.MirrorAttach{}}
d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}}),
Mirrors: memMirrors{"test": {Repo: "test", URL: upstream.URL + "/tcl", Enabled: true}},
Buckets: buckets, Attachments: newMemAttachments(buckets), MirrorAttach: store}
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
head := func(hash string) attachmentHeadOut {
t.Helper()
out, err := r.Invoke(context.Background(), "attachment.head", attachmentHeadIn{Repo: "test", Hash: hash})
if err != nil {
t.Fatalf("FAIL: head %.8s: %v", hash, err)
}
return out.(attachmentHeadOut)
}
if h := head(src); h.Kind != attachKindMissing || h.Upstream == "" {
t.Fatalf("FAIL: before the fetch the file reads %+v, want held upstream", h)
}
mirrorAttachRuns.Wait()
shaSum := sha256.Sum256(good)
sha := hex.EncodeToString(shaSum[:])
if row, ok, _ := store.MirrorAttachment("test", src); !ok || row.State != oltp.MirrorAttachFetched || row.SHA256 != sha || row.Size != int64(len(good)) {
t.Fatalf("FAIL: the fetch recorded %+v", row)
}
if n, ok := objects.objects[attach.Key(sha)]; !ok || n != int64(len(good)) {
t.Fatalf("FAIL: the bucket holds %v", objects.objects)
}
if h := head(src); !h.External || h.SHA256 != sha || h.Kind == attachKindMissing {
t.Errorf("FAIL: after the fetch the file reads %+v, want external", h)
}
link, err := r.Invoke(context.Background(), "attachment.link", attachmentLinkIn{Repo: "test", Hash: src})
if err != nil || !link.(attachmentLinkOut).External || link.(attachmentLinkOut).Download == "" {
t.Errorf("FAIL: the link after the fetch: %+v, %v", link, err)
}
// The backfill reaches the other one: the upstream's bytes are not
// what the card names, so it is refused and recorded.
if _, err := d.mirrorAttachBackfill(context.Background(), "test"); err != nil {
t.Fatal(err)
}
if row, ok, _ := store.MirrorAttachment("test", liar); !ok || row.State != oltp.MirrorAttachFailed || !strings.Contains(row.Err, "hash") {
t.Errorf("FAIL: wrong bytes recorded %+v", row)
}
if len(objects.objects) != 1 {
t.Errorf("FAIL: refused bytes reached the bucket: %v", objects.objects)
}
}