package sync import ( "io" "log" "os" "path/filepath" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // R-971 — the 15-minute correction refuses a catalog definition the box's allow-list does not admit: the deployed // app keeps its last good compose AND its .felhom.yml (nothing of the app moves this cycle), read back off disk. const tplOldFixedPrivileged = `services: web: image: nextcloud:31.0.14-apache privileged: true healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:80/status.php"] ` // RED-PROOF (P2/redproof-allowlist.txt): with the admit call removed from copyTemplates, the live compose gains // `privileged: true` and this test fails. func TestR971_RefusedCorrectionWritesNothing(t *testing.T) { for _, wired := range []bool{true, false} { // the manager's check, and the syncer's own placeholder-env check s, stackDir, _ := renderFixture(t, tplOldFixedPrivileged) live := filepath.Join(stackDir, "docker-compose.yml") meta := filepath.Join(stackDir, ".felhom.yml") write(t, live, tplOld) write(t, meta, "OLD META\n") write(t, stacks.AppliedComposePath(stackDir), tplOld) s.SetRenderPlanFn(pinnedPlan(stackDir, map[string]string{"web": "nextcloud:31.0.14-apache"}, true)) asked := 0 if wired { s.SetAdmitFn(func(app string, data []byte) error { asked++ return s.admit2(app, data) }) } _, updated, err := s.copyTemplates() if err != nil { t.Fatal(err) } if got := readFile(t, live); got != tplOld { t.Fatalf("[wired=%v] the refused definition reached the live compose:\n%s", wired, got) } // The app is not reported updated, so the post-sync hook does not redeploy it: it keeps running as it was. for _, u := range updated { if u == "nextcloud" { t.Fatalf("[wired=%v] a refused app was reported updated — the post-sync redeploy would touch it", wired) } } if got := readFile(t, meta); got != "OLD META\n" { t.Fatalf("[wired=%v] the .felhom.yml of a refused app moved: %q", wired, got) } if wired && asked == 0 { t.Fatal("the manager's check was never asked") } } } // admit2 is the placeholder-env check (what an unwired syncer uses), callable from the wired case. func (s *Syncer) admit2(app string, data []byte) error { saved := s.admitFn s.admitFn = nil defer func() { s.admitFn = saved }() return s.admit(app, data) } // Positive control: the same fix without the privilege flows exactly as before (Group A's assertion). func TestR971_AdmittedCorrectionStillFlows(t *testing.T) { s, stackDir, _ := renderFixture(t, tplOldFixed) live := filepath.Join(stackDir, "docker-compose.yml") write(t, live, tplOld) write(t, stacks.AppliedComposePath(stackDir), tplOld) s.SetRenderPlanFn(pinnedPlan(stackDir, map[string]string{"web": "nextcloud:31.0.14-apache"}, true)) if _, _, err := s.copyTemplates(); err != nil { t.Fatal(err) } if got := readFile(t, live); !strings.Contains(got, "status.php") { t.Fatalf("an admitted fix did not flow:\n%s", got) } } // A catalog template named like a controller infra stack never lands in that stack's directory. func TestR971_CatalogNeverWritesAnInfraStack(t *testing.T) { root := t.TempDir() cfg := &config.Config{} cfg.Paths.DataDir = filepath.Join(root, "data") cfg.Paths.StacksDir = filepath.Join(root, "stacks") for _, name := range []string{"traefik", "cloudflared", "filebrowser", "samba", "felhom-controller"} { cat := filepath.Join(cfg.Paths.DataDir, "catalog-cache", "templates", name) dst := filepath.Join(cfg.Paths.StacksDir, name) for _, d := range []string{cat, dst} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } } write(t, filepath.Join(cat, "docker-compose.yml"), "services:\n x:\n image: evil/x:1\n") write(t, filepath.Join(dst, "docker-compose.yml"), "CONTROLLER RENDERED\n") } s := New(cfg, log.New(io.Discard, "", 0), func() error { return nil }, nil) if _, _, err := s.copyTemplates(); err != nil { t.Fatal(err) } for _, name := range []string{"traefik", "cloudflared", "filebrowser", "samba", "felhom-controller"} { if got := readFile(t, filepath.Join(cfg.Paths.StacksDir, name, "docker-compose.yml")); got != "CONTROLLER RENDERED\n" { t.Errorf("%s: the catalog overwrote the controller's own compose: %q", name, got) } } } // An app that is not installed: its refused template is not written either (the install would refuse it again). func TestR971_RefusedTemplateOfAnAppNotInstalled(t *testing.T) { s, stackDir, _ := renderFixture(t, tplOldFixedPrivileged) live := filepath.Join(stackDir, "docker-compose.yml") write(t, live, tplOld) if _, _, err := s.copyTemplates(); err != nil { t.Fatal(err) } if got := readFile(t, live); got != tplOld { t.Fatalf("a refused template was written for an app not installed:\n%s", got) } }