Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/fakes_test.go
T
jschoubben 24b244bc88
mesh/delivery merged: source.main moved to novox/mesh-catalog@27a7757b970e
mesh/check pass 24b244bc88dc on 27a7757b970e | carried from 6b6d5cba7195 | unit 2046 run, 2327 kept, 6 skipped | integration 2066 run, 2346 kept, 10 s…
mesh-delivery-gate: ask no Approve of a merge the forge account cannot carry out, and say why (issue 1945)
car-hunter #2 was approved and its commit status then refused 403. Before the ask the
gate reads merge.writes for each repository it covers; where the merging account cannot
write, nothing is asked, the delivery waits on 'the forge account cannot write <repo>:
gitea_grant_write' and a condition is raised. gitea.repo.writable reads it again and
asks an approved, refused merge of it again.
2026-10-26 16:39:56 +01:00

404 lines
12 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"testing"
"time"
)
// What a test stands the holder on: a store in memory that can be made to fail, a controller that records
// what it was asked and answers what it is told, a forge that records every note, view and status.
type memStore struct {
mu sync.Mutex
deliveries map[string][]byte
groups map[string][]byte
failing bool
// puts counts the deliveries written, failed or not.
puts int
}
func newMemStore() *memStore {
return &memStore{deliveries: map[string][]byte{}, groups: map[string][]byte{}}
}
func (s *memStore) PutDelivery(d *Delivery) error {
s.mu.Lock()
defer s.mu.Unlock()
s.puts++
if s.failing {
return errors.New("the bus is away")
}
s.deliveries[d.ID] = mustJSON(d)
return nil
}
func (s *memStore) DeleteDelivery(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.deliveries, id)
return nil
}
func (s *memStore) PutGroup(g *Group) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.failing {
return errors.New("the bus is away")
}
s.groups[g.ID] = mustJSON(g)
return nil
}
func (s *memStore) DeleteGroup(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.groups, id)
return nil
}
func (s *memStore) Deliveries() ([]*Delivery, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []*Delivery
for key, raw := range s.deliveries {
if key == carriedKey { // the mark an earlier owner's carry-over wrote, as the real store skips it
continue
}
var d Delivery
if err := json.Unmarshal(raw, &d); err != nil {
return nil, err
}
out = append(out, &d)
}
return out, nil
}
func (s *memStore) Groups() ([]*Group, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []*Group
for _, raw := range s.groups {
var g Group
if err := json.Unmarshal(raw, &g); err != nil {
return nil, err
}
out = append(out, &g)
}
return out, nil
}
type fakeForge struct {
mu sync.Mutex
notes map[string][]string
views map[string]string
statuses map[string]string
// statusWrites counts the statuses set.
statusWrites int
down bool
// viewErr is what View answers instead, when set: a pull request the forge no longer holds.
viewErr error
viewCalls int
// What Statuses reads: the pull requests by owner/repo#number, the base's required statuses, whether the
// protection cannot be read, and what it was asked.
pulls map[string]ForgePull
// unwritable are the repositories (owner/repo) the forge's merging account cannot write (issue 1945).
unwritable map[string]bool
required []string
protectionDown bool
asked []string
// contains answers gitea_contains by "<commit> in <in>"; a pair it does not hold is a forge that cannot say.
contains map[string]bool
containsDown bool
containsAsks []string
// open is what OpenPulls answers; openDown a forge that cannot say.
open []OpenPull
openDown bool
openCalls int
// merges and comments are what was asked of the forge for an approved merge (novox/hq issue 722); approvedHeads the
// heads each merge was handed as approved (ADR 0421 §9).
merges, comments []string
approvedHeads [][]string
refuse error
// refuseRepo refuses the merges of one repository only ("owner/name"), beside refuse (issue 1049).
refuseRepo map[string]error
// calls are the comments in the order asked.
calls []string
}
func (f *fakeForge) OpenPulls() ([]OpenPull, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.openCalls++
if f.openDown {
return nil, errors.New("the forge is away")
}
return append([]OpenPull(nil), f.open...), nil
}
func newFakeForge() *fakeForge {
return &fakeForge{notes: map[string][]string{}, views: map[string]string{}, statuses: map[string]string{},
pulls: map[string]ForgePull{}, contains: map[string]bool{}}
}
func (f *fakeForge) Note(owner, repo, commit, line string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.down {
return errors.New("the forge is away")
}
key := owner + "/" + repo + "@" + commit
for _, l := range f.notes[key] {
if l == line {
return nil // appended once
}
}
f.notes[key] = append(f.notes[key], line)
return nil
}
func (f *fakeForge) View(owner, repo string, number int, body string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.viewCalls++
if f.viewErr != nil {
return f.viewErr
}
if f.down {
return errors.New("the forge is away")
}
f.views[fmt.Sprintf("%s/%s#%d", owner, repo, number)] = body
return nil
}
func (f *fakeForge) Status(owner, repo, commit, context, state, description, target string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.down {
return errors.New("the forge is away")
}
f.statusWrites++
f.statuses[owner+"/"+repo+"@"+commit+" "+context] = state + " " + description + " → " + target
return nil
}
// Statuses answers what the fake was told: a pull request's head from pulls, the statuses set on the commit
// (those Status set too, as the forge would keep them), and the base's required statuses from required.
func (f *fakeForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.down {
return nil, errors.New("the forge is away")
}
f.asked = append(f.asked, fmt.Sprintf("%s/%s#%d@%s base=%s", owner, repo, number, commit, base))
out := &ForgeChecks{}
if number > 0 {
if p, ok := f.pulls[fmt.Sprintf("%s/%s#%d", owner, repo, number)]; ok {
out.Pull = &p
if commit == "" {
commit = p.HeadSHA
}
if base == "" {
base = p.Base
}
} else if commit == "" {
return nil, fmt.Errorf("no pull request %d", number)
}
}
for key, said := range f.statuses {
at, context, _ := strings.Cut(key, " ")
if !strings.HasPrefix(at, owner+"/"+repo+"@") || !sameCommit(strings.TrimPrefix(at, owner+"/"+repo+"@"), commit) {
continue
}
if len(strings.TrimPrefix(at, owner+"/"+repo+"@")) > len(commit) {
commit = strings.TrimPrefix(at, owner+"/"+repo+"@")
}
state, rest, _ := strings.Cut(said, " ")
description, link, _ := strings.Cut(rest, " → ")
out.Statuses = append(out.Statuses, ForgeStatus{Context: context, State: state, Description: description,
TargetURL: link, UpdatedAt: "2026-10-06T12:00:00Z", Creator: "mesh-admin"})
}
sort.Slice(out.Statuses, func(i, j int) bool { return out.Statuses[i].Context < out.Statuses[j].Context })
out.Commit = commit
if base == "" {
base = "main"
}
out.Merge = ForgeMerge{Branch: base, Protected: f.required != nil, Required: f.required}
if f.unwritable[owner+"/"+repo] {
no := false
out.Merge.Writes, out.Merge.CannotWrite = &no, "the forge account cannot write "+owner+"/"+repo+": gitea_grant_write"
}
if f.protectionDown {
out.Merge.Says = "the protection of " + base + " could not be read"
return out, nil
}
ok := true
for _, r := range f.required {
state := "missing"
for _, st := range out.Statuses {
if st.Context == r {
state = st.State
}
}
if state != "success" {
ok = false
out.Merge.Blocking = append(out.Merge.Blocking, struct {
Context string `json:"context"`
State string `json:"state"`
}{r, state})
}
}
out.Merge.Mergeable = &ok
out.Merge.Says = fmt.Sprintf("%s requires %v", base, f.required)
return out, nil
}
// Contains answers what the fake was told of two commits.
func (f *fakeForge) Contains(owner, repo, commit, in string) (bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.containsAsks = append(f.containsAsks, commit+" in "+in)
if f.containsDown || f.down {
return false, errors.New("the forge is away")
}
yes, said := f.contains[commit+" in "+in]
if !said {
return false, fmt.Errorf("the forge has no commit %s or %s", commit, in)
}
return yes, nil
}
func (f *fakeForge) notesOn(commit string) []string {
f.mu.Lock()
defer f.mu.Unlock()
for key, lines := range f.notes {
if strings.HasSuffix(key, "@"+commit) {
return append([]string(nil), lines...)
}
}
return nil
}
// world is a holder over fakes, with a clock a test moves.
type world struct {
t *testing.T
h *Holder
store *memStore
forge *fakeForge
op *fakeOperator
merges *fakeMerges
source *fakeSource
owed *fakeOwed
// asked is the key of the ask a test answered.
asked string
now time.Time
logbuf strings.Builder
said []map[string]any
mu sync.Mutex
}
func newWorld(t *testing.T) *world {
return newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC))
}
func newWorldOver(t *testing.T, store *memStore, forge *fakeForge, now time.Time) *world {
w := &world{t: t, store: store, forge: forge, now: now}
w.h = &Holder{Store: store, Forge: forge,
Now: func() time.Time { w.mu.Lock(); defer w.mu.Unlock(); return w.now },
Logf: func(format string, a ...any) { t.Logf(format, a...); fmt.Fprintf(&w.logbuf, format+"\n", a...) },
Emit: func(event string, body any) error {
w.mu.Lock()
defer w.mu.Unlock()
var m map[string]any
_ = json.Unmarshal(mustJSON(body), &m)
m["event-kind"] = event
w.said = append(w.said, m)
return nil
}}
// The operator approves at once unless a test says otherwise (w.op.auto = false): most tests are not about the Approve.
w.op = newFakeOperator(w.h.Now)
w.op.auto = true
w.h.Operator = w.op
w.merges, w.source = newFakeMerges(forge), &fakeSource{}
w.h.Merges, w.h.Source = w.merges, w.source
w.merges.h = w.h
w.owed = newFakeOwed(forge)
w.h.Owed = w.owed
w.h.init()
if err := w.h.Load(); err != nil {
t.Fatal(err)
}
return w
}
func (w *world) later(d time.Duration) {
w.mu.Lock()
w.now = w.now.Add(d)
w.mu.Unlock()
}
// settleAll takes a look a few times: the answers the teller wrote are read at each, and each member of a group handed to the
// forge bridge is merged by the fake bridge, whose word is heard by the next look.
func (w *world) settleAll() {
for range 4 {
w.h.React()
}
}
func (w *world) delivery(id string) *Delivery {
w.t.Helper()
w.h.mu.Lock()
defer w.h.mu.Unlock()
d := w.h.deliveries[id]
if d == nil {
w.t.Fatalf("no delivery %s; there are %v", id, w.ids())
}
c := *d
return &c
}
func (w *world) ids() []string {
var out []string
for id := range w.h.deliveries {
out = append(out, id)
}
sort.Strings(out)
return out
}
func (w *world) state(id string) State { return w.delivery(id).State }
// pr is a pull request's head as the forge announces it.
func pr(repo string, number int, sha, branch string, paths ...string) PullEvent {
owner, name, _ := strings.Cut(repo, "/")
return PullEvent{Owner: owner, Repo: name, Number: number, Base: "main", Head: branch, HeadSHA: sha,
HTMLURL: fmt.Sprintf("https://forge.invalid/%s/pulls/%d", repo, number), Paths: paths,
// Reviewed at its head, as the Approve is asked only for a reviewed head (novox/hq ADR 0496, issue 1803).
Body: "Reviewed-by: tester@shanks at " + sha}
}
// verdict is the controller's `checked` for a head.
func verdict(repo string, number int, sha, gate string) CheckedEvent {
owner, name, _ := strings.Cut(repo, "/")
c := CheckedEvent{Owner: owner, Repo: name, Number: number, Commit: sha, Verdict: gate, Summary: "the gate said " + gate,
ID: "check-of-" + sha}
return c
}
// merged is the forge's `pull.merged`.
func merged(repo string, number int, head, merge string) PullEvent {
p := pr(repo, number, head, "feature")
p.MergeCommit = merge
p.MergedAt = "2026-10-06T12:30:00Z"
return p
}
// Comment records what was asked (novox/hq issue 722).
func (f *fakeForge) Comment(owner, repo string, number int, body string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.comments = append(f.comments, fmt.Sprintf("%s/%s#%d: %s", owner, repo, number, body))
f.calls = append(f.calls, "comment "+body)
return nil
}
func (w *world) logs() string { return w.logbuf.String() }