Files
felhom-controller/controller/internal/web/backups_split_test.go
T
admin 2487681396 style: gofmt normalization — no logic changes
gofmt -w across the controller tree (46 files) so gofmt -l is empty — disarms the
formatting landmine where a targeted edit + accidental gofmt -w swept ~46 unrelated
files. Pure formatting: whitespace + gofmt's optional-semicolon removal in reflowed
inline closures. One doc comment reworded ('' -> 'the empty string') to avoid gofmt's
Go-1.19 doc-comment typographic substitition ('' -> curly quote) muddying its meaning.
No build/vet/test behavior change.
2026-07-25 07:37:02 +02:00

174 lines
6.9 KiB
Go

package web
import (
"net/http/httptest"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// v0.124.0 IA split (§10 nav/state): each backups sub-page renders ONLY its mapped sections —
// a marker string per section, asserted present on its page and absent on every other page.
func splitTestData() map[string]interface{} {
return map[string]interface{}{
"Page": "backups", "Title": "Biztonsági mentés",
"Backup": &backup.FullBackupStatus{
AppDataInfo: []backup.AppBackupInfo{{StackName: "calibre-web", DisplayName: "Calibre-Web"}},
},
"AppBackupRows": []AppBackupRow{
{StackName: "calibre-web", DisplayName: "Calibre-Web", OffboxEnabled: true, Tier3State: "active"},
},
"DBSectionState": "dumps",
"Offbox": &settings.OffboxTarget{
Enabled: true, Host: "nas.local", LastStatus: "ok", EscrowState: "escrowed",
},
"OffboxConfigured": true,
"OffboxApps": []OffboxAppRow{{Name: "calibre-web", DisplayName: "Calibre-Web", Enabled: true}},
"OffboxToggledCount": 1,
"OffboxQuotaPct": 0,
"GuestBackup": map[string]interface{}{"Available": false, "Note": "n/a"},
}
}
func TestBackupsSplit_SectionsOnExactlyOnePage(t *testing.T) {
pages := map[string]string{
"backups": renderBackupPage(t, "backups", splitTestData()),
"backups_remote": renderBackupPage(t, "backups_remote", splitTestData()),
"backups_apps": renderBackupPage(t, "backups_apps", splitTestData()),
"backups_restore": renderBackupPage(t, "backups_restore", splitTestData()),
}
// marker → the ONE page it belongs to
markers := map[string]string{
"Tárhely áttekintés": "backups", // Section 0
"Rendszermentés (teljes mentés)": "backups", // whole-guest
">Adatmentés<": "backups", // stat cards (neutral branch)
`id="offbox-section"`: "backups_remote", // the offbox anchor target
"Távoli mentési cél beállítása": "backups_remote", // manual-target form
"Mely alkalmazások mentődnek": "backups_remote", // toggle list
"<h3>Ütemezés</h3>": "backups_apps", // schedule
"<h3>Adatbázisok</h3>": "backups_apps", // databases
"Alkalmazások mentési állapota": "backups_apps", // per-app rows
`id="restore-app"`: "backups_restore", // restore panel
"Visszaállítás a távoli tárolóból": "backups_restore", // the offsite restore list
`href="/backups/restore/app?name=`: "backups_restore", // R-48: the ONE entry per app
}
for marker, home := range markers {
for page, html := range pages {
has := strings.Contains(html, marker)
if page == home && !has {
t.Errorf("%s: missing its own section marker %q", page, marker)
}
if page != home && has {
t.Errorf("%s: renders %q which belongs to %s only", page, marker, home)
}
}
}
}
// The tier-3 row actions must deep-link cross-page to the offbox section (the pre-split
// #offbox-section anchors would be orphaned on the apps page).
func TestBackupsSplit_CrossPageAnchors(t *testing.T) {
data := splitTestData()
data["AppBackupRows"] = []AppBackupRow{
{StackName: "radarr", DisplayName: "Radarr", Tier3State: "off"},
}
html := renderBackupPage(t, "backups_apps", data)
if !strings.Contains(html, `href="/backups/remote#offbox-section"`) {
t.Error("tier-3 'Bekapcsolás' must link cross-page to /backups/remote#offbox-section")
}
if strings.Contains(html, `href="#offbox-section"`) {
t.Error("orphaned same-page #offbox-section anchor survives on the apps page")
}
}
// Felhom-offsite status card (v0.124.0, decision 2): three honest states, display-only —
// the card itself must never carry a form or button (no state change from the card, ever).
func TestBackupsRemote_OffsiteStatusCardStates(t *testing.T) {
cardOf := func(html string) string {
i := strings.Index(html, `id="felhom-offsite-card"`)
if i < 0 {
return ""
}
j := strings.Index(html[i:], "</div>")
return html[i : i+j]
}
// State 1: no target configured → the opt-in pitch (and no "Aktív" headline).
data := splitTestData()
data["OffboxConfigured"] = false
data["Offbox"] = nil
data["OffboxToggledCount"] = 0
html := renderBackupPage(t, "backups_remote", data)
if !strings.Contains(html, "Felhom offsite tárhely — igényelhető szolgáltatás. Titkosított, házon kívüli másolat a mentéseidről. Érdeklődj az üzemeltetőnél.") {
t.Error("state 1: the opt-in pitch is missing")
}
if strings.Contains(html, "Aktív — nincs kijelölt alkalmazás") {
t.Error("state 1 must not render the state-2 headline")
}
if card := cardOf(html); strings.Contains(card, "<form") || strings.Contains(card, "<button") {
t.Error("the status card must be display-only (no form/button)")
}
// State 2: applied, zero apps toggled → the honest active-but-empty headline.
data = splitTestData()
data["OffboxToggledCount"] = 0
data["OffboxApps"] = []OffboxAppRow{{Name: "calibre-web", DisplayName: "Calibre-Web", Enabled: false}}
html = renderBackupPage(t, "backups_remote", data)
if !strings.Contains(html, "Aktív — nincs kijelölt alkalmazás") {
t.Error("state 2: 'Aktív — nincs kijelölt alkalmazás' missing")
}
if strings.Contains(html, "igényelhető szolgáltatás") {
t.Error("state 2 must not render the state-1 pitch")
}
if card := cardOf(html); strings.Contains(card, "<form") || strings.Contains(card, "<button") {
t.Error("the status card must be display-only (no form/button)")
}
// State 3: applied + toggled → NO card; the regular status block carries the state.
html = renderBackupPage(t, "backups_remote", splitTestData())
if strings.Contains(html, `id="felhom-offsite-card"`) {
t.Error("state 3 must not render the status card (the status block is the state)")
}
if !strings.Contains(html, "Utolsó távoli mentés") {
t.Error("state 3: the regular status block is missing")
}
}
// Route → handler wiring: each sub-route sets its own .Page id, so the sidebar child renders
// active on the right page (backupMgr nil → the empty state renders, the nav still does).
func TestBackupsSplit_RoutesSetPageIDs(t *testing.T) {
s := testServer(t)
s.loadTemplates()
cases := []struct {
path string
active string
}{
{"/backups/remote", `href="/backups/remote" class="active"`},
{"/backups/apps", `href="/backups/apps" class="active"`},
{"/backups/restore", `href="/backups/restore" class="active"`},
}
for _, c := range cases {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", c.path, nil)
switch c.path {
case "/backups/remote":
s.backupsRemoteHandler(rr, req)
case "/backups/apps":
s.backupsAppsHandler(rr, req)
case "/backups/restore":
s.backupsRestoreHandler(rr, req)
}
if rr.Code != 200 {
t.Fatalf("%s: status %d", c.path, rr.Code)
}
if !strings.Contains(rr.Body.String(), c.active) {
t.Errorf("%s: sidebar child not active (want %q)", c.path, c.active)
}
}
}