package web import ( "io" "log" "net/http/httptest" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/appexport" "gitea.dooplex.hu/admin/felhom-controller/internal/config" ) // T-F2a: validStackName rejects traversal/escape, accepts real single-segment names. (pure) func TestValidStackName(t *testing.T) { for _, bad := range []string{"../../etc", "a/b", "..", ".", "", "a\x00b", "a\\b", "../x", "/etc"} { if validStackName(bad) { t.Errorf("validStackName(%q) = true, want false", bad) } } for _, good := range []string{"paperless-ngx", "immich", "romm", "uptime-kuma"} { if !validStackName(good) { t.Errorf("validStackName(%q) = false, want true", good) } } } // T-F2b: the restore handler rejects a traversal stack_name with the flash-error redirect and never // reaches RestoreFromRecoveryUnit (backupMgr is nil — if the gate failed to fire, it would nil-panic // or fall through to the "Mentés nincs beállítva" path, not the "Érvénytelen" one). // Companion: drop the gate → traversal falls through to the nil-backupMgr path → different redirect → fails. func TestBackupRestoreHandler_RejectsTraversal(t *testing.T) { s := &Server{cfg: &config.Config{}, logger: log.New(io.Discard, "", 0)} // backupMgr == nil form := "stack_name=../../../etc&snapshot_id=x" req := httptest.NewRequest("POST", "/backup/restore", strings.NewReader(form)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() s.backupRestoreHandler(rec, req) // must not panic loc := rec.Header().Get("Location") if !strings.Contains(loc, "rv%C3%A9nytelen") { // "Érvénytelen" (invalid name) — the gate's redirect t.Fatalf("expected traversal rejection redirect, got Location=%q", loc) } } // T-F2c: the export handler rejects a traversal stack_name with 400 (before any export work). func TestExportStart_RejectsTraversal(t *testing.T) { // non-nil exporter so we pass the "not available" guard and reach the stack_name gate (the gate // returns before the exporter is ever used). s := &Server{cfg: &config.Config{}, logger: log.New(io.Discard, "", 0), appExporter: &appexport.Exporter{}} body := `{"stack_name":"../../etc","dest_drive":"/mnt/x"}` req := httptest.NewRequest("POST", "/api/export/start", strings.NewReader(body)) rec := httptest.NewRecorder() s.apiExportStart(rec, req) // must not panic if rec.Code != 400 { t.Fatalf("expected 400 for traversal stack_name, got %d", rec.Code) } }