Files
felhom-controller/controller/internal/stacks/digest_test.go
T

81 lines
3.6 KiB
Go

package stacks
import (
"path/filepath"
"strings"
"testing"
"time"
)
var dA = "sha256:" + strings.Repeat("a", 64)
var dB = "sha256:" + strings.Repeat("b", 64)
func TestDigest_RenderAndStrip(t *testing.T) {
in := "# header\nservices:\n web:\n image: redis:7-alpine # pinned\n restart: unless-stopped\n db:\n image: \"mariadb:11.8\"\nvolumes:\n x:\n"
out := string(renderDigests([]byte(in), map[string]string{"web": dA, "db": "not-a-digest"}))
if !strings.Contains(out, " image: redis:7-alpine@"+dA+" # pinned") {
t.Fatalf("web not rendered:\n%s", out)
}
if !strings.Contains(out, " image: \"mariadb:11.8\"") {
t.Fatalf("an invalid digest must leave the line alone:\n%s", out)
}
again := string(renderDigests([]byte(out), map[string]string{"web": dB}))
if strings.Count(again, "@sha256:") != 1 || !strings.Contains(again, "redis:7-alpine@"+dB) {
t.Fatalf("re-render must replace, not stack, the digest:\n%s", again)
}
imgs, err := parseComposeImagesBytes([]byte(again))
if err != nil || imgs["web"] != "redis:7-alpine" {
t.Fatalf("the parsed ref must be digest-free: %v %v", imgs, err)
}
}
// TestDigest_FloatingTagBehindOnlyForANewerTestedDigest — the badge rule, every arm.
//
// COMPANION RED-PROOF (REPORT): make digestBehind return false — the "newer test" case then reads
// Current and this test fails.
func TestDigest_FloatingTagBehindOnlyForANewerTestedDigest(t *testing.T) {
installAt := time.Date(2026, 9, 20, 0, 0, 0, 0, time.UTC)
mk := func(instDigest, instAt string, cat map[string]string, testedAt time.Time) Stack {
return Stack{Deployed: true, CatalogImages: map[string]string{"web": "redis:7-alpine"},
CatalogDigests: cat, CatalogTestedAt: testedAt,
AppConfig: &AppConfig{InstalledImages: map[string]InstalledImage{"web": {Ref: "redis:7-alpine", Digest: instDigest, At: instAt}}}}
}
cases := []struct {
name string
s Stack
want UpdateOrder
}{
{"same digest", mk(dA, installAt.Format(time.RFC3339), map[string]string{"web": dA}, installAt.Add(time.Hour)), UpdateOrderCurrent},
{"newer tested digest", mk(dA, installAt.Format(time.RFC3339), map[string]string{"web": dB}, installAt.Add(time.Hour)), UpdateOrderBehind},
{"test older than the install", mk(dA, installAt.Format(time.RFC3339), map[string]string{"web": dB}, installAt.Add(-time.Hour)), UpdateOrderCurrent},
{"install time unknown", mk(dA, "", map[string]string{"web": dB}, installAt.Add(time.Hour)), UpdateOrderCurrent},
{"installed digest unknown", mk("", installAt.Format(time.RFC3339), map[string]string{"web": dB}, installAt.Add(time.Hour)), UpdateOrderCurrent},
{"no tested digest", mk(dA, installAt.Format(time.RFC3339), nil, time.Time{}), UpdateOrderCurrent},
}
for _, c := range cases {
if got := CatalogOrder(c.s); got != c.want {
t.Errorf("%s: %v, want %v", c.name, got, c.want)
}
}
}
// The update runs EXACTLY the tested image: the pinned step's compose carries the entry's digest, while
// the pin itself stays a plain ref.
func TestDigest_TheUpdateRendersTheStepsTestedDigest(t *testing.T) {
m, dir, _, _, _ := ladderManager(t, true)
catDir := filepath.Dir(m.CatalogTemplatePath("nextcloud", "docker-compose.yml"))
// the ladder fixture's digests are sha256:aaaa…
if err := m.StartGuardedUpdate("nextcloud"); err != nil {
t.Fatal(err)
}
waitUpdateDone(t, m, "nextcloud")
body := fileBody(t, ComposePathIn(dir))
if !strings.Contains(body, "image: "+ladderB+"@"+dA) {
t.Fatalf("the live compose does not pin the tested digest:\n%s", body)
}
if pinOf(t, dir) != ladderB {
t.Fatalf("the pin must stay a plain ref, got %q", pinOf(t, dir))
}
_ = catDir
}