package api
import (
"context"
"errors"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/ops"
)
// memMirrors is a MirrorStore in memory, enough to drive posture.
type memMirrors map[string]Mirror
func (m memMirrors) Mirrors() ([]Mirror, error) {
var out []Mirror
for _, v := range m {
out = append(out, v)
}
return out, nil
}
func (m memMirrors) MirrorFor(repo string) (Mirror, bool, error) { v, ok := m[repo]; return v, ok, nil }
func (m memMirrors) SetMirror(repo, url string, every int) error {
m[repo] = Mirror{Repo: repo, URL: url, Every: every, Enabled: true}
return nil
}
func (m memMirrors) DetachMirror(repo string) error {
v := m[repo]
v.Enabled, v.DetachedFrom = false, v.URL
m[repo] = v
return nil
}
func (m memMirrors) SetMirrorWrites(repo string, accept bool) error {
v := m[repo]
v.AcceptWrites = accept
m[repo] = v
return nil
}
func (m memMirrors) RecordMirrorRun(string, time.Duration, bool, error) (int, error) { return 0, nil }
func (m memMirrors) MirrorPulls(string, int) ([]MirrorPull, error) { return nil, nil }
func (m memMirrors) MirrorDiverged(string, string) error { return nil }
func TestMirrorState(t *testing.T) {
now := time.Date(2026, 8, 21, 12, 0, 0, 0, time.UTC)
stamp := func(ago time.Duration) string { return now.Add(-ago).UTC().Format(time.RFC3339) }
cases := []struct {
name string
m Mirror
want string
}{
{"fresh", Mirror{Enabled: true, Every: 3600, LastOK: stamp(10 * time.Minute)}, MirrorCurrent},
{"edge", Mirror{Enabled: true, Every: 3600, LastOK: stamp(119 * time.Minute)}, MirrorCurrent},
{"quiet too long", Mirror{Enabled: true, Every: 3600, LastOK: stamp(3 * time.Hour)}, MirrorLagging},
{"never ran", Mirror{Enabled: true, Every: 3600}, MirrorLagging},
{"failing", Mirror{Enabled: true, Every: 3600, LastOK: stamp(time.Minute), Failures: 1}, MirrorFailing},
{"detached", Mirror{Enabled: false, DetachedFrom: "https://x/home"}, MirrorDetached},
}
for _, c := range cases {
if got := mirrorState(c.m, now); got != c.want {
t.Errorf("%s: got %s, want %s", c.name, got, c.want)
}
}
}
// A mirror refuses local writes until the operator allows them, and a
// detached store is native again. repo.get carries the posture.
func TestMirrorRefusesWrites(t *testing.T) {
ix := wikiIndex(t)
ms := memMirrors{}
_ = ms.SetMirror("test", "https://upstream.example/home", 3600)
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix), Mirrors: ms}); err != nil {
t.Fatal(err)
}
ctx := context.Background()
newTicket := func() error {
_, err := r.Invoke(ctx, "ticket.new", ticketNewIn{Repo: "test", Title: "t"})
return err
}
err := newTicket()
if !errors.Is(err, ops.ErrForbidden) || !strings.Contains(err.Error(), "upstream.example") {
t.Fatalf("ticket on a mirror: want forbidden naming the upstream, got %v", err)
}
for _, op := range []struct {
name string
in any
}{
{"wiki.save", wikiSaveIn{Repo: "test", Name: "p", Body: "b"}},
{"forum.post", forumPostIn{Repo: "test", Title: "t", Body: "b"}},
{"technote.add", technoteAddIn{Repo: "test", Body: "b"}},
} {
if _, err := r.Invoke(ctx, op.name, op.in); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("%s on a mirror: want forbidden, got %v", op.name, err)
}
}
got, err := r.Invoke(ctx, "repo.get", repoGetIn{Repo: "test"})
if err != nil {
t.Fatal(err)
}
info := got.(repoInfo)
if !info.Mirror.Tracking || info.Mirror.URL != "https://upstream.example/home" || info.Mirror.State != MirrorLagging {
t.Fatalf("repo.get posture: %+v", info.Mirror)
}
if _, err := r.Invoke(ctx, "mirror.writes", mirrorWritesIn{Repo: "test", Accept: true}); err != nil {
t.Fatal(err)
}
// (This harness has no store to write through, so the gate is
// asserted directly once writes are allowed.)
if ok, _ := (Deps{Mirrors: ms}).Writable("test"); !ok {
t.Fatal("writes allowed, still refused")
}
if _, err := r.Invoke(ctx, "mirror.detach", mirrorDetachIn{Repo: "test"}); err != nil {
t.Fatal(err)
}
got, _ = r.Invoke(ctx, "repo.get", repoGetIn{Repo: "test"})
info = got.(repoInfo)
if info.Mirror.Tracking || info.DetachedFrom != "https://upstream.example/home" {
t.Fatalf("detached posture: %+v detachedFrom=%q", info.Mirror, info.DetachedFrom)
}
if ok, _ := (Deps{Mirrors: ms}).Writable("test"); !ok {
t.Fatal("a detached store is native and writable")
}
}