feat(master): check the registry now from the service page (#35)

This commit is contained in:
2026-10-14 14:20:29 +02:00
parent 2989fa224d
commit e7c0e2fd61
11 changed files with 356 additions and 18 deletions
+148
View File
@@ -938,3 +938,151 @@ func TestDigestWritesArePublishedBeforeTagsAreListed(t *testing.T) {
close(reg.tagRelease)
<-done
}
func TestPollService(t *testing.T) {
ref := repoA + ":latest" // not version-like: no tag listing
newDigest := map[string]string{ref: "sha256:new"}
auto := func(edit func(*model.Service)) model.Service {
s := svc(1, ref)
s.UpdateMode = model.ModeAuto
if edit != nil {
edit(&s)
}
return s
}
tests := []struct {
name string
svc model.Service
reg *fakeRegistry
startErr error
wantErr error // matched with ErrorIs
wantNotPoll string
wantHeads []string
wantTags []string
check func(t *testing.T, st *fakeStore, j *fakeJobs)
}{
{name: "manual with a new digest is only marked",
svc: svc(1, ref), reg: &fakeRegistry{digests: newDigest}, wantHeads: []string{ref},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) {
require.Len(t, st.available, 1)
assert.Equal(t, model.SourcePoll, st.available[0].a.Source)
assert.Empty(t, j.starts)
}},
{name: "auto with a new digest starts a poll job",
svc: auto(nil), reg: &fakeRegistry{digests: newDigest}, wantHeads: []string{ref},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) {
assert.Equal(t, []startCall{{1, jobs.StartRequest{Trigger: model.TriggerPoll, Ref: ref}}}, j.starts)
assert.Equal(t, []lastTagCall{{1, "sha256:new"}}, st.lastTag)
}},
{name: "auto that cannot be updated is marked but last_tag_digest stays",
svc: auto(nil), reg: &fakeRegistry{digests: newDigest}, wantHeads: []string{ref},
startErr: &model.NotUpdatableError{Reason: model.ReasonDrifted},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) {
assert.Len(t, st.available, 1)
assert.Len(t, j.starts, 1)
assert.Empty(t, st.lastTag)
}},
{name: "auto that already got the digest is not started",
svc: auto(func(s *model.Service) { s.LastTagDigest = "sha256:new" }),
reg: &fakeRegistry{digests: newDigest}, wantHeads: []string{ref},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) { assert.Empty(t, j.starts) }},
{name: "auto whose desired_ref names the digest is not started",
svc: auto(func(s *model.Service) { s.DesiredRef = ref + "@sha256:new" }),
reg: &fakeRegistry{digests: newDigest}, wantHeads: []string{ref},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) { assert.Empty(t, j.starts) }},
{name: "registry error is stored, not returned",
svc: svc(1, ref), reg: &fakeRegistry{errs: map[string]error{ref: errors.New("head: boom")}}, wantHeads: []string{ref},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) {
assert.Equal(t, []pollErrorCall{{[]int64{1}, "head: boom"}}, st.pollErrors)
}},
{name: "a previous poll error is cleared on success",
svc: auto(func(s *model.Service) {
s.UpdateMode, s.ActualDigest, s.LastPollError = model.ModeManual, "sha256:new", "head: boom"
}),
reg: &fakeRegistry{digests: newDigest}, wantHeads: []string{ref},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) {
assert.Equal(t, []pollErrorCall{{[]int64{1}, ""}}, st.pollErrors)
}},
{name: "no tag is not polled",
svc: svc(1, repoA+"@sha256:old"), reg: &fakeRegistry{}, wantNotPoll: model.ReasonNoTag},
{name: "built is not polled",
svc: auto(func(s *model.Service) { s.Built = true }), reg: &fakeRegistry{}, wantNotPoll: "built from source"},
{name: "no digest is not polled",
svc: auto(func(s *model.Service) { s.ActualDigest = "" }), reg: &fakeRegistry{}, wantNotPoll: "no registry digest"},
{name: "removed is not found",
svc: auto(func(s *model.Service) { s.RemovedAt = ptr(time.Now()) }), reg: &fakeRegistry{}, wantErr: model.ErrNotFound},
{name: "a comparable tag lists the repo once and suggests the newer tag",
svc: newerSvc(1, repoB, "1.27.3"),
reg: newerRegistry(map[string][]string{repoB: {"1.27.4", "1.28.0"}}, repoB+":1.27.3"),
wantHeads: []string{repoB + ":1.27.3"}, wantTags: []string{repoB},
check: func(t *testing.T, st *fakeStore, j *fakeJobs) {
require.Len(t, st.newer, 1)
assert.Equal(t, newerCall{1, repoB + ":1.28.0", time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC)}, st.newer[0])
}},
{name: "a tag that cannot have a newer tag lists nothing",
svc: newerSvc(1, repoB, "latest"), reg: newerRegistry(nil, repoB+":latest"), wantHeads: []string{repoB + ":latest"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &fakeStore{services: []model.Service{tt.svc}}
j := &fakeJobs{errFor: map[int64]error{1: tt.startErr}}
p, _ := newPoller(st, j, tt.reg, Config{})
err := p.PollService(context.Background(), 1)
var np *NotPolledError
switch {
case tt.wantNotPoll != "":
require.ErrorAs(t, err, &np)
assert.Equal(t, tt.wantNotPoll, np.Reason)
case tt.wantErr != nil:
assert.ErrorIs(t, err, tt.wantErr)
default:
require.NoError(t, err)
}
assert.Equal(t, tt.wantHeads, tt.reg.sortedCalls())
assert.Equal(t, tt.wantTags, tt.reg.tagCalls)
if tt.check != nil {
tt.check(t, st, j)
}
})
}
}
func TestPollServiceUnknownID(t *testing.T) {
reg := &fakeRegistry{}
p, _ := newPoller(&fakeStore{}, &fakeJobs{}, reg, Config{})
err := p.PollService(context.Background(), 9)
assert.ErrorIs(t, err, model.ErrNotFound)
assert.Empty(t, reg.calls)
}
func TestPollServiceOnlyThisService(t *testing.T) {
ref := repoA + ":1.4"
st := &fakeStore{services: []model.Service{svc(1, ref), svc(2, ref)}}
reg := &fakeRegistry{digests: map[string]string{ref: "sha256:new"}}
p, _ := newPoller(st, &fakeJobs{}, reg, Config{})
require.NoError(t, p.PollService(context.Background(), 1))
require.Len(t, st.available, 1)
assert.Equal(t, int64(1), st.available[0].id)
assert.Equal(t, []string{ref}, reg.sortedCalls())
}
func TestPollServiceCancelled(t *testing.T) {
ref := repoA + ":1.4"
st := &fakeStore{services: []model.Service{svc(1, ref)}}
reg := &fakeRegistry{release: make(chan struct{})} // lookups only end by cancel
p, _ := newPoller(st, &fakeJobs{}, reg, Config{})
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := p.PollService(ctx, 1)
assert.ErrorIs(t, err, context.Canceled)
assert.Empty(t, st.pollErrors)
assert.Empty(t, st.available)
}