package appexport import ( "errors" "io" "log" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" ) // hddProvider is an rtProvider that reports an HDD-backed stack (estimate scenario H + the // v0.130.0 additive-export tests in export_additive_test.go). type hddProvider struct { *rtProvider mounts []string hddPath string binds []appbackup.ClassifiedBind hasBinds bool } func (p *hddProvider) GetStackNeedsHDD(string) bool { return true } func (p *hddProvider) GetStackHDDMounts(string) []string { return p.mounts } func (p *hddProvider) GetStackHDDPath(string) string { return p.hddPath } func (p *hddProvider) GetImportRoot() string { return "" } // R-75: no import binds in this fixture // R-203: these fixtures use ENROLLED drive paths, where the namespace root IS the drive path. // Delegating keeps that identity explicit rather than hardcoding it. func (p *hddProvider) GetStackNamespaceRoot(name string) string { return p.GetStackHDDPath(name) } func (p *hddProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) { return p.binds, p.hasBinds } func newEstimator(t *testing.T, provider ExportStackProvider) *Exporter { t.Helper() return NewExporter(provider, log.New(io.Discard, "", 0), "test") } // Scenario F (the F-A fix): a volume-only app with a >1 GiB volume reports the REAL size via the // container-view sizer — not 0/"3.6 KB". This is the F-A red-proof anchor (revert EstimateExport to // dockerVolumeSize → reads 0). func TestEstimate_VolumeSize_RealNotZero(t *testing.T) { const twoGiB = int64(2) << 30 orig := volumeSizer volumeSizer = func(vol string) (int64, error) { return twoGiB, nil } defer func() { volumeSizer = orig }() e := newEstimator(t, &rtProvider{stackDir: t.TempDir(), volumes: []string{"app_data"}}) est, err := e.EstimateExport("app", t.TempDir()) if err != nil { t.Fatal(err) } if est.SizeUnknown { t.Fatalf("size must be known when the sizer succeeds") } if est.DataSizeBytes != twoGiB { t.Fatalf("DataSizeBytes = %d, want %d (WRONG would be 0 — the F-A bug)", est.DataSizeBytes, twoGiB) } if !strings.Contains(est.DataSizeHuman, "GB") { t.Fatalf("DataSizeHuman = %q, want GB-scale (WRONG would be \"3.6 KB\")", est.DataSizeHuman) } } // Scenario G: a failed volume read must never render as "fits". Size is marked unknown, the human // string says so, and FitsOnDest is forced false. func TestEstimate_VolumeSize_FailureNeverFits(t *testing.T) { orig := volumeSizer volumeSizer = func(vol string) (int64, error) { return 0, errors.New("docker: no such image") } defer func() { volumeSizer = orig }() e := newEstimator(t, &rtProvider{stackDir: t.TempDir(), volumes: []string{"app_data"}}) est, err := e.EstimateExport("app", t.TempDir()) if err != nil { t.Fatal(err) } if !est.SizeUnknown { t.Fatalf("a failed volume read must set SizeUnknown") } if est.FitsOnDest { t.Fatalf("an unknown size must NEVER render as fits_on_dest:true") } if est.DataSizeHuman != "ismeretlen méret" { t.Fatalf("DataSizeHuman = %q, want \"ismeretlen méret\"", est.DataSizeHuman) } if est.DataSizeBytes != 0 { t.Fatalf("no successful read → DataSizeBytes should be 0, got %d", est.DataSizeBytes) } } // Scenario H (regression): an HDD-backed stack must NOT touch the new volume sizer — the HDD branch // (duBytes on the mounted /mnt path) is unchanged. Platform-independent: assert the seam is not // invoked and SizeUnknown stays false. func TestEstimate_HDDPath_DoesNotUseVolumeSizer(t *testing.T) { called := false orig := volumeSizer volumeSizer = func(vol string) (int64, error) { called = true; return 0, nil } defer func() { volumeSizer = orig }() p := &hddProvider{rtProvider: &rtProvider{stackDir: t.TempDir()}, mounts: []string{t.TempDir()}} e := newEstimator(t, p) est, err := e.EstimateExport("app", t.TempDir()) if err != nil { t.Fatal(err) } if called { t.Fatalf("HDD-backed stack must not call the docker volume sizer") } if est.SizeUnknown { t.Fatalf("HDD branch must not set SizeUnknown") } }