Files
felhom.eu/hub/internal/web/artifact_installability_test.go
T

242 lines
9.2 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/gitea"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// The installability gate (R-273). On 2026-08-08 agent v0.128.0 was published as a package and
// never git-tagged; the hub vouched it, and every fresh install and reinstall died at step 5 of 8,
// as root, on a virgin machine, for most of a day. These tests pin the check at the moment of risk.
//
// Each scenario names the WRONG outcome it exists to prevent, because a test whose failure mode is
// unstated is a test nobody can weigh.
// fakeRegistry serves the two URLs the gate probes. Anything not explicitly present 404s, which is
// the shape a pruned package or an unpushed tag really has.
type fakeRegistry struct {
tagFiles map[string]bool // "<repo>/<tag>/<path>"
packages map[string]bool // "<pkg>/<version>"
status int // when non-zero, every request answers with this instead (E)
// downloadGone models R-287: the version's METADATA still answers while the file itself is
// gone. Keyed "<pkg>/<version>"; consulted only on the download route.
downloadGone map[string]bool
// shaless models a version whose metadata answers but carries no sha256.
shaless map[string]bool
hits int
}
func (f *fakeRegistry) start(t *testing.T) *gitea.Client {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f.hits++
if f.status != 0 {
w.WriteHeader(f.status)
return
}
p := strings.TrimPrefix(r.URL.Path, "/")
// Package METADATA (api/v1/packages/<owner>/generic/<pkg>/<ver>/files) — what
// resolveArtifactSHA calls before the gate ever runs. Serving it keeps these tests about
// the installability gate rather than about sha resolution.
if strings.HasPrefix(p, "api/v1/packages/") && strings.HasSuffix(p, "/files") {
seg := strings.Split(p, "/")
if len(seg) >= 8 && f.packages[seg[5]+"/"+seg[6]] {
sha := strings.Repeat("c", 64)
if f.shaless[seg[5]+"/"+seg[6]] {
sha = ""
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"name":"` + seg[5] + `","sha256":"` + sha + `"}]`))
return
}
w.WriteHeader(http.StatusNotFound)
return
}
// raw/tag: <owner>/<repo>/raw/tag/<tag>/<path...>
if i := strings.Index(p, "/raw/tag/"); i >= 0 {
repo := p[:i]
repo = repo[strings.Index(repo, "/")+1:]
rest := p[i+len("/raw/tag/"):]
slash := strings.Index(rest, "/")
if slash > 0 && f.tagFiles[repo+"/"+rest[:slash]+"/"+rest[slash+1:]] {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
return
}
// packages: api/packages/<owner>/generic/<pkg>/<version>/<file>
if strings.HasPrefix(p, "api/packages/") {
seg := strings.Split(p, "/")
if len(seg) >= 6 && f.packages[seg[4]+"/"+seg[5]] && !f.downloadGone[seg[4]+"/"+seg[5]] {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(srv.Close)
return gitea.New(srv.URL, "admin", "", "")
}
// healthy is the registry as it should be: agent tagged + published, golden published.
func healthy() *fakeRegistry {
return &fakeRegistry{
tagFiles: map[string]bool{"felhom-agent/v0.128.0/configs/felhom-mkfs-guarded.sh": true},
packages: map[string]bool{"felhom-agent/0.128.0": true, "felhom-golden/0.210.0": true},
}
}
func saveArtifacts(t *testing.T, s *Server) *httptest.ResponseRecorder {
t.Helper()
form := url.Values{
"agent_version": {"0.128.0"},
"golden_version": {"0.210.0"},
"agent_sha256": {strings.Repeat("a", 64)},
"golden_sha256": {strings.Repeat("b", 64)},
}
r := httptest.NewRequest(http.MethodPost, "/configuration/artifacts", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.handleSetArtifacts(w, r)
return w
}
func flashOf(w *httptest.ResponseRecorder) string {
loc := w.Header().Get("Location")
if i := strings.Index(loc, "flash="); i >= 0 {
return loc[i+len("flash="):]
}
return ""
}
func assertUnchanged(t *testing.T, st *store.Store, why string) {
t.Helper()
if m := st.GetArtifactManifest(); m.AgentVersion != "" || m.GoldenVersion != "" {
t.Fatalf("%s: manifest was WRITTEN anyway: %+v", why, m)
}
}
// A — the tag is missing and the package is there. THE ONE THAT MATTERS: this is 2026-08-09 exactly.
// WRONG OUTCOME: saved, and every install dies five steps in, as root, on a virgin machine.
func TestInstallability_A_TagMissing(t *testing.T) {
s, st := newTestServer(t)
fr := healthy()
fr.tagFiles = map[string]bool{} // published, never tagged
s.SetGiteaClient(fr.start(t))
w := saveArtifacts(t, s)
if got := flashOf(w); got != "artifact_tag_missing" {
t.Fatalf("flash = %q, want artifact_tag_missing — Friday's defect is not being caught", got)
}
assertUnchanged(t, st, "A")
}
// B — the tag is there and the package was pruned out from under it (R-287).
// WRONG OUTCOME: saved, and the installer 404s on the binary itself.
func TestInstallability_B_PackagePruned(t *testing.T) {
s, st := newTestServer(t)
fr := healthy()
// R-287's real shape: metadata still answers, the FILE does not. Modelled with a separate
// download map so the sha resolver succeeds and the gate's download leg is what convicts.
fr.downloadGone = map[string]bool{"felhom-agent/0.128.0": true}
s.SetGiteaClient(fr.start(t))
w := saveArtifacts(t, s)
if got := flashOf(w); got != "artifact_pkg_missing" {
t.Fatalf("flash = %q, want artifact_pkg_missing", got)
}
assertUnchanged(t, st, "B")
}
// C — both artifacts are installable, but the registry can produce no checksum for one of them.
// The manifest must not record a version with an empty sha: that is vouching bytes nobody verified.
//
// NOTE ON WHERE THIS IS ENFORCED, because the first draft of this test was wrong. With a Gitea
// client configured, resolveArtifactSHA fetches the sha AUTHORITATIVELY and ignores anything
// submitted — so a blank field in the form cannot produce a blank stored sha, and the scenario as
// originally written ("submit an empty sha") modelled nothing real. The genuine shape is Gitea
// answering with no sha256, and the EXISTING refusal owns it. This test pins the guarantee rather
// than the mechanism: whatever refuses, the manifest must be unchanged.
// WRONG OUTCOME: the hub vouches a checksum for bytes it never saw.
func TestInstallability_C_NoChecksum(t *testing.T) {
s, st := newTestServer(t)
fr := healthy()
fr.shaless = map[string]bool{"felhom-agent/0.128.0": true} // metadata answers, with no sha256
s.SetGiteaClient(fr.start(t))
w := saveArtifacts(t, s)
if got := flashOf(w); got == "artifacts_set" {
t.Fatalf("a version with no resolvable checksum SAVED — the hub vouched bytes nobody verified")
}
assertUnchanged(t, st, "C")
}
// D — everything correct. The gate must be invisible on a good approval.
// WRONG OUTCOME: a new obstacle in front of a correct operator action.
func TestInstallability_D_HappyPathStillSaves(t *testing.T) {
s, st := newTestServer(t)
s.SetGiteaClient(healthy().start(t))
w := saveArtifacts(t, s)
if got := flashOf(w); got != "artifacts_set" {
t.Fatalf("flash = %q, want artifacts_set — a correct approval was blocked", got)
}
m := st.GetArtifactManifest()
if m.AgentVersion != "0.128.0" || m.GoldenVersion != "0.210.0" {
t.Fatalf("manifest not saved on the happy path: %+v", m)
}
}
// E — the registry cannot answer. It must refuse, and it must say "could not verify" rather than
// "missing": those are different facts and the operator acts differently on each.
// WRONG OUTCOME: saved with a warning. A warning beside a success is read as a success.
func TestInstallability_E_RegistryUnreachable(t *testing.T) {
s, st := newTestServer(t)
fr := healthy()
fr.status = http.StatusBadGateway
s.SetGiteaClient(fr.start(t))
w := saveArtifacts(t, s)
got := flashOf(w)
if got == "artifacts_set" {
t.Fatalf("saved while the registry was unreachable — the warning-beside-a-success shape")
}
if got != "artifact_unverifiable" {
t.Fatalf("flash = %q, want artifact_unverifiable — an undetermined result must not be "+
"reported as a missing artifact", got)
}
assertUnchanged(t, st, "E")
}
// The gate must not fire when there is no registry client at all (the legacy manual path), or a
// hub with Gitea unconfigured could never vouch anything.
func TestInstallability_NoGiteaClient_StillSaves(t *testing.T) {
s, st := newTestServer(t)
w := saveArtifacts(t, s)
if got := flashOf(w); got != "artifacts_set" {
t.Fatalf("flash = %q, want artifacts_set with no Gitea client configured", got)
}
if st.GetArtifactManifest().AgentVersion != "0.128.0" {
t.Fatal("manifest not saved on the no-client path")
}
}
// The probe must assert the path the INSTALLER fetches. A gate that probes some other path that
// merely exists is how 2026-08-09 stayed invisible.
func TestInstallability_ProbesTheInstallersOwnPath(t *testing.T) {
if installerProbeConfig != "configs/felhom-mkfs-guarded.sh" {
t.Fatalf("probe path = %q; it must be the FIRST config felhom-host-install.sh fetches "+
"(step 5/8), which is the file whose 404 broke every install on 2026-08-09",
installerProbeConfig)
}
}