d698ce343b
gates / gates (push) Successful in 14s
R-487: the local backup lists are keyed on the drives, not on what is deployed — a removed app whose unit was kept is listed with the restore that reinstalls it, the picker answers for it, and the restore opens the unit where it sits. R-491: a removal clears the app's update hold. R-490: /api/system/info reaches the API router and reads the default storage path. R-489: volumes_removed is the real before/after difference, [] when none. R-476: a Tier-2 copy is dated by its data, not its manifest. R-456: the boot-orphan rule is pinned. Every fix red-proofed.
112 lines
4.6 KiB
Go
112 lines
4.6 KiB
Go
package web
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
)
|
|
|
|
// R-487 — a removed app whose unit was kept renders on the backups page with the restore that brings
|
|
// it back; a page without such rows carries none of it (the negative control).
|
|
func TestR487_BackupsPageListsARemovedAppWithItsRestore(t *testing.T) {
|
|
data := splitTestData()
|
|
data["AppBackupRows"] = []AppBackupRow{
|
|
{StackName: "calibre-web", DisplayName: "Calibre-Web"},
|
|
{StackName: "gone-app", DisplayName: "Gone", Removed: true, RemovedUnitTime: "2026-09-13T00:30:00Z", StorageLabel: "HDD"},
|
|
}
|
|
html := renderBackupPage(t, "backups_apps", data)
|
|
for _, want := range []string{
|
|
`name="stack_name" value="gone-app"`,
|
|
`name="snapshot_id" value="helyi"`,
|
|
`action="/backup/restore"`,
|
|
`class="layer-badge">Megtartva<`,
|
|
} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("removed row must render %q", want)
|
|
}
|
|
}
|
|
if strings.Count(html, `class="layer-badge">Megtartva<`) != 1 {
|
|
t.Error("the kept badge must render for the removed row only")
|
|
}
|
|
control := renderBackupPage(t, "backups_apps", splitTestData())
|
|
if strings.Contains(control, "Megtartva") || strings.Contains(control, `value="helyi"`) {
|
|
t.Error("negative control: a page with no removed app must not offer the restore")
|
|
}
|
|
}
|
|
|
|
// R-487 — the restore picker lists removed apps in their own group.
|
|
func TestR487_RestorePickerListsRemovedApps(t *testing.T) {
|
|
data := splitTestData()
|
|
data["RemovedApps"] = []backup.RemovedAppUnit{{StackName: "gone-app", DisplayName: "Gone"}}
|
|
html := renderBackupPage(t, "backups_restore", data)
|
|
if !strings.Contains(html, `<option value="gone-app" data-removed="true"`) {
|
|
t.Error("removed app must be an option in the picker")
|
|
}
|
|
if strings.Contains(renderBackupPage(t, "backups_restore", splitTestData()), "data-removed") {
|
|
t.Error("negative control: no removed apps, no group")
|
|
}
|
|
}
|
|
|
|
// r487Provider is the smallest stack provider that can say "this app is not deployed".
|
|
type r487Provider struct{ deployed []string }
|
|
|
|
func (p *r487Provider) GetStackComposePath(name string) (string, bool) {
|
|
for _, s := range p.deployed {
|
|
if s == name {
|
|
return "/stacks/" + name + "/docker-compose.yml", true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
func (p *r487Provider) ListDeployedStacks() []backup.StackSummary {
|
|
out := make([]backup.StackSummary, 0, len(p.deployed))
|
|
for _, s := range p.deployed {
|
|
out = append(out, backup.StackSummary{Name: s})
|
|
}
|
|
return out
|
|
}
|
|
func (p *r487Provider) GetStackHDDMounts(string) []string { return nil }
|
|
func (p *r487Provider) GetStackHDDPath(string) string { return "" }
|
|
func (p *r487Provider) GetImportRoot() string { return "" }
|
|
func (p *r487Provider) GetDockerVolumes(string) []string { return nil }
|
|
func (p *r487Provider) StopStack(string) error { return nil }
|
|
func (p *r487Provider) StartStack(string) error { return nil }
|
|
func (p *r487Provider) RefreshAndIsRunning(string) bool { return true }
|
|
func (p *r487Provider) GetStackRecoveryInfo(string) (backup.RecoveryInfo, bool) {
|
|
return backup.RecoveryInfo{}, false
|
|
}
|
|
func (p *r487Provider) RecoverStackSecrets(string, []string) map[string]string { return nil }
|
|
func (p *r487Provider) RecreateStackDefinitionFromUnit(string, string, map[string]string) error {
|
|
return nil
|
|
}
|
|
func (p *r487Provider) StartStackServices(string, []string) error { return nil }
|
|
func (p *r487Provider) GetStackClassifiedBinds(string) ([]backup.ClassifiedBind, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
// R-487 — the page's row builder appends a removed app's kept unit after the deployed rows, from the
|
|
// real manager over a real drive layout (the seam is the provider, and it says only "not deployed").
|
|
func TestR487_BuildAppBackupRowsAppendsRemovedApps(t *testing.T) {
|
|
s := testServer(t)
|
|
sys := t.TempDir()
|
|
cfg := &config.Config{}
|
|
cfg.Paths.SystemDataPath = sys
|
|
mgr := backup.NewManager(cfg, s.settings, s.logger)
|
|
mgr.SetStackProvider(&r487Provider{})
|
|
s.backupMgr = mgr
|
|
unit := backup.RecoveryUnitPath(backup.NamespaceRootFor(sys, sys), "gone-app")
|
|
if err := os.MkdirAll(unit, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(backup.UnitManifestFile(unit), []byte(`{"schema_version":2,"app_name":"gone-app","display_name":"Gone","created_at":"2026-09-12T02:15:29Z"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows := s.buildAppBackupRows(&backup.FullBackupStatus{})
|
|
if len(rows) != 1 || rows[0].StackName != "gone-app" || !rows[0].Removed || rows[0].DisplayName != "Gone" || rows[0].RemovedUnitTime == "" {
|
|
t.Fatalf("want one removed row for gone-app, got %+v", rows)
|
|
}
|
|
}
|