package monitor import ( "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/infra" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) func contains(ss []string, want string) bool { for _, s := range ss { if s == want { return true } } return false } // EffectiveProtected must drop cloudflared when no tunnel token is configured (LAN-only node), so the // health loop doesn't report it missing forever — but keep it when a token IS configured. func TestEffectiveProtectedDropsCloudflaredWithoutToken(t *testing.T) { base := config.StacksConfig{Protected: []string{"traefik", "cloudflared", "felhom-controller", "filebrowser"}} cfgNoTok := &config.Config{Stacks: base} got := EffectiveProtected(cfgNoTok, settings.SMBSettings{}) if contains(got, "cloudflared") { t.Errorf("cloudflared must be dropped when no tunnel token: %v", got) } for _, must := range []string{"traefik", "felhom-controller", "filebrowser"} { if !contains(got, must) { t.Errorf("%s must remain protected: %v", must, got) } } cfgTok := &config.Config{Stacks: base} cfgTok.Infrastructure.CFTunnelToken = "tok" if !contains(EffectiveProtected(cfgTok, settings.SMBSettings{}), "cloudflared") { t.Error("cloudflared must remain protected when a tunnel token is configured") } } // R-7b Scenario E, BOTH directions. Sharing is a customer-toggled feature, so the samba container can // never be in the golden controller.yaml — the effective set must add it dynamically when sharing is // live (so a dead sharing service raises the same protected-container issue as a dead traefik) and must // leave it out when sharing is OFF (so a box that never enabled it never reports a missing container). // Red-proof: delete the samba append and the enabled case fails. // // R-77 TIGHTENED THE "ON" CASE, and this test was updated with it: "on" now means // Enabled AND UserSet, because reconcileSambaAt refuses to deploy without a household password. The // previous fixture used Enabled alone and therefore asserted the very behaviour that produced the // live false alarm on demo-hp (2026-07-26). The three-state matrix is in // TestScenarioE_SambaProtectedOnlyWhenActuallyDeployed below. func TestEffectiveProtectedTracksSharingToggle(t *testing.T) { cfg := &config.Config{Stacks: config.StacksConfig{Protected: []string{"traefik", "felhom-controller"}}} on := EffectiveProtected(cfg, settings.SMBSettings{Enabled: true, UserSet: true}) if !contains(on, infra.SambaContainerName) { t.Errorf("sharing ON: %q must be watched, got %v", infra.SambaContainerName, on) } off := EffectiveProtected(cfg, settings.SMBSettings{Enabled: false}) if contains(off, infra.SambaContainerName) { t.Errorf("sharing OFF: %q must NOT be watched, got %v", infra.SambaContainerName, off) } // The base set is untouched in both directions. for _, set := range [][]string{on, off} { for _, must := range []string{"traefik", "felhom-controller"} { if !contains(set, must) { t.Errorf("%s must remain protected: %v", must, set) } } } } // R-77 Scenario E — the samba protected-set gate must mirror reconcileSambaAt's BOTH early returns. // // The live false alarm (demo-hp, 2026-07-26): sharing was enabled without a household password, so // reconcileSambaAt deliberately did not deploy the container, but EffectiveProtected added it anyway // and the box reported health=fail for a state the controller itself had chosen. func TestScenarioE_SambaProtectedOnlyWhenActuallyDeployed(t *testing.T) { cfg := &config.Config{Stacks: config.StacksConfig{ Protected: []string{"traefik", "cloudflared", "felhom-controller", "filebrowser"}}} cfg.Infrastructure.CFTunnelToken = "tok" // keep cloudflared in, so the samba change is isolated for _, tc := range []struct { name string smb settings.SMBSettings want bool why string }{ {"(1) sharing OFF", settings.SMBSettings{Enabled: false, UserSet: false}, false, "a box that never enabled sharing must stay quiet"}, {"(2) sharing ON, no password", settings.SMBSettings{Enabled: true, UserSet: false}, false, "THE BUG: reconcileSambaAt refuses to deploy without a password — a deliberate state, not a fault"}, {"(3) sharing ON, password set", settings.SMBSettings{Enabled: true, UserSet: true}, true, "the container really should be running — a dead one must STILL alarm (do not over-suppress)"}, } { if got := contains(EffectiveProtected(cfg, tc.smb), infra.SambaContainerName); got != tc.want { t.Errorf("%s: samba protected = %v, want %v — %s", tc.name, got, tc.want, tc.why) } } // Not over-suppressed: the rest of the protected set is untouched in every sharing state. for _, smb := range []settings.SMBSettings{ {Enabled: false}, {Enabled: true}, {Enabled: true, UserSet: true}, } { set := EffectiveProtected(cfg, smb) for _, must := range []string{"traefik", "cloudflared", "felhom-controller", "filebrowser"} { if !contains(set, must) { t.Errorf("smb=%+v: %q must always stay protected, got %v", smb, must, set) } } } }