9cc8424954
/ now 302s to /launcher (ONE canonical URL per page — launcher body never served at /); the Vezérlőpult keeps /dashboard + its nav slot (operator ruling, reversing v0.163.0). Nav: Indítópult active on /launcher, Vezérlőpult href=/dashboard active there. Post-login default (/) and the topbar logo (/) flow through the redirect; login target unchanged. Repointed 2 dashboard-card tests /→/dashboard. Corrected the stale vacation/agent-DOWN note in CLAUDE.md (agent is up at 192.168.0.162:8443). Tests: 302 target+status, /dashboard 200, nav hrefs/active; red-proof verified.
175 lines
6.6 KiB
Go
175 lines
6.6 KiB
Go
package web
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
|
)
|
|
|
|
// F3 (AUDIT-vacation-remote-ops-2026-07-20): the dashboard's "Utolsó mentés" row branches on
|
|
// .BackupStatus, a key dashboardHandler never set — so the {{if}} arm was unreachable and every box
|
|
// reported "Még nem futott" forever, even with dumps on disk and crossdrive_completed events in the
|
|
// hub. These tests drive the REAL handler through ServeHTTP so they bite on the handler wiring, not
|
|
// just on the template.
|
|
|
|
// dashDumpProvider lists one deployed stack on a known drive, so the backup manager's per-drive dump
|
|
// scan has somewhere to look. Everything else comes from blockProvider (async_restore_test.go).
|
|
type dashDumpProvider struct {
|
|
blockProvider
|
|
stack string
|
|
}
|
|
|
|
func (p *dashDumpProvider) ListDeployedStacks() []backup.StackSummary {
|
|
return []backup.StackSummary{{Name: p.stack}}
|
|
}
|
|
|
|
// newDashboardServer builds a Server that renders the real dashboard through ServeHTTP, backed by a
|
|
// real backup.Manager. When dumpAt is non-zero a DB dump file is planted on the app's drive with
|
|
// that modtime — GetFullStatus then synthesizes LastDBDump from it, which is what the card must show.
|
|
func newDashboardServer(t *testing.T, dumpAt time.Time) *Server {
|
|
t.Helper()
|
|
lg := log.New(io.Discard, "", 0)
|
|
dir := t.TempDir()
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Customer.ID = "test-customer"
|
|
cfg.Customer.Name = "Teszt Ügyfél"
|
|
cfg.Customer.Domain = "example.hu"
|
|
cfg.Paths.StacksDir = filepath.Join(dir, "stacks")
|
|
cfg.Paths.DataDir = filepath.Join(dir, "data")
|
|
cfg.Paths.SystemDataPath = filepath.Join(dir, "system")
|
|
cfg.Stacks.ComposeCommand = "docker compose" // skip detection (not needed for page renders)
|
|
cfg.Backup.Enabled = true // the card only renders when backup is enabled
|
|
|
|
sett, err := settings.Load(filepath.Join(dir, "settings.json"), lg)
|
|
if err != nil {
|
|
t.Fatalf("settings: %v", err)
|
|
}
|
|
stackMgr, err := stacks.NewManager(cfg, lg)
|
|
if err != nil {
|
|
t.Fatalf("stacks manager: %v", err)
|
|
}
|
|
|
|
drive := filepath.Join(dir, "drive")
|
|
bm := backup.NewManager(cfg, sett, lg)
|
|
bm.SetStackProvider(&dashDumpProvider{blockProvider{hdd: drive}, "bookstack"})
|
|
|
|
if !dumpAt.IsZero() {
|
|
dumpDir := backup.AppDBDumpPath(backup.NamespaceRoot(drive, true), "bookstack")
|
|
if err := os.MkdirAll(dumpDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f := filepath.Join(dumpDir, "bookstack-mariadb.sql")
|
|
body := "-- MySQL dump\n-- Server version 12.3\n" + strings.Repeat("-- padding to clear the min-size validator\n", 4)
|
|
if err := os.WriteFile(f, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chtimes(f, dumpAt, dumpAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
bm.RefreshCache(time.Now()) // populates the cache the card reads (and the dump-file scan)
|
|
|
|
s := &Server{cfg: cfg, settings: sett, stackMgr: stackMgr, backupMgr: bm, logger: lg, version: "test"}
|
|
s.loadTemplates()
|
|
return s
|
|
}
|
|
|
|
// backupCard returns just the dashboard's backup card, so an assertion cannot accidentally match
|
|
// the same words elsewhere on the page.
|
|
func backupCard(t *testing.T, body string) string {
|
|
t.Helper()
|
|
i := strings.Index(body, `class="backup-status-card"`)
|
|
if i < 0 {
|
|
t.Fatalf("no backup card on the dashboard (BackupEnabled false?)")
|
|
}
|
|
rest := body[i:]
|
|
if j := strings.Index(rest, "</div>\n</div>"); j > 0 {
|
|
return rest[:j]
|
|
}
|
|
return rest
|
|
}
|
|
|
|
// Scenario B — a real dump on disk ⇒ the card shows ITS timestamp, never "Még nem futott".
|
|
// COMPANION red-proof: delete `data["BackupStatus"] = fullStatus.LastDBDump` from dashboardHandler
|
|
// → the card falls back to "Még nem futott" and this test FAILS. Run → fail → restore (in REPORT).
|
|
func TestDashboardBackupCard_ShowsLastRun(t *testing.T) {
|
|
dumpAt := time.Date(2026, 7, 19, 3, 30, 0, 0, time.Local)
|
|
s := newDashboardServer(t, dumpAt)
|
|
|
|
rec := getPage(t, s, "/dashboard") // v0.170.0: the Vezérlőpult moved from "/" to "/dashboard"
|
|
if rec.Code != 200 {
|
|
t.Fatalf("GET /dashboard = %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
card := backupCard(t, rec.Body.String())
|
|
|
|
want := dumpAt.Format("2006-01-02 15:04")
|
|
if !strings.Contains(card, want) {
|
|
t.Errorf("card must show the real last run %q, got:\n%s", want, card)
|
|
}
|
|
if strings.Contains(card, "Még nem futott") {
|
|
t.Errorf("a box WITH a dump must not claim it never ran, got:\n%s", card)
|
|
}
|
|
}
|
|
|
|
// Scenario C — no dump anywhere ⇒ the fresh-box branch still renders honestly (nil passes through;
|
|
// the fix must not fabricate a zero timestamp like "0001-01-01 00:00").
|
|
func TestDashboardBackupCard_FreshBoxStaysHonest(t *testing.T) {
|
|
s := newDashboardServer(t, time.Time{})
|
|
|
|
rec := getPage(t, s, "/dashboard") // v0.170.0: the Vezérlőpult moved from "/" to "/dashboard"
|
|
if rec.Code != 200 {
|
|
t.Fatalf("GET /dashboard = %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
card := backupCard(t, rec.Body.String())
|
|
|
|
if !strings.Contains(card, "Még nem futott") {
|
|
t.Errorf("a box with NO dump must say so, got:\n%s", card)
|
|
}
|
|
if strings.Contains(card, "0001-01-01") {
|
|
t.Errorf("nil dump must not render a zero-value timestamp, got:\n%s", card)
|
|
}
|
|
}
|
|
|
|
// Scenario D — a FAILED dump renders "Sikertelen", not a timestamp and not "Még nem futott".
|
|
// The failure flag only exists in the manager's in-memory run state (unreachable from this package
|
|
// without shelling out to docker), so this one asserts the template branch directly.
|
|
func TestDashboardBackupCard_FailedRunShowsSikertelen(t *testing.T) {
|
|
data := map[string]interface{}{
|
|
"Page": "dashboard", "Title": "Vezérlőpult",
|
|
"MissingStorage": map[string]string{},
|
|
"NetworkWarnings": map[string]string{},
|
|
"NetworkStubs": map[string]string{},
|
|
"Subdomains": map[string]string{},
|
|
"RunningCount": 0, "StoppedCount": 0, "TotalCount": 0,
|
|
"SystemInfo": system.SystemInfo{},
|
|
"Domain": "demo-felhom.eu",
|
|
"BackupEnabled": true,
|
|
"BackupStatus": &backup.DBDumpStatus{
|
|
LastRun: time.Date(2026, 7, 19, 3, 30, 0, 0, time.Local),
|
|
Success: false,
|
|
},
|
|
}
|
|
card := backupCard(t, renderBackupPage(t, "dashboard", data))
|
|
|
|
if !strings.Contains(card, "Sikertelen") {
|
|
t.Errorf("a failed dump must render Sikertelen, got:\n%s", card)
|
|
}
|
|
if strings.Contains(card, "2026-07-19 03:30") {
|
|
t.Errorf("a failed dump must NOT render as a successful timestamp, got:\n%s", card)
|
|
}
|
|
if strings.Contains(card, "Még nem futott") {
|
|
t.Errorf("a failed dump is not 'never ran', got:\n%s", card)
|
|
}
|
|
}
|