diff --git a/SPToolbox-logo-ico.png b/SPToolbox-logo-ico.png
new file mode 100644
index 0000000..2955eef
Binary files /dev/null and b/SPToolbox-logo-ico.png differ
diff --git a/SharepointToolbox.Tests/Services/StorageServiceTests.cs b/SharepointToolbox.Tests/Services/StorageServiceTests.cs
index ed8e3a7..f2b4e71 100644
--- a/SharepointToolbox.Tests/Services/StorageServiceTests.cs
+++ b/SharepointToolbox.Tests/Services/StorageServiceTests.cs
@@ -28,4 +28,21 @@ public class StorageServiceTests
var node = new StorageNode { TotalSizeBytes = 5000L, FileStreamSizeBytes = 3000L };
Assert.Equal(2000L, node.VersionSizeBytes);
}
+
+ [Fact]
+ public void StorageNode_DefaultKind_IsLibrary()
+ {
+ var node = new StorageNode();
+ Assert.Equal(StorageNodeKind.Library, node.Kind);
+ }
+
+ [Fact]
+ public void StorageScanOptions_DefaultIncludeFlags_AreAllTrue()
+ {
+ var opts = new StorageScanOptions();
+ Assert.True(opts.IncludeHiddenLibraries);
+ Assert.True(opts.IncludePreservationHold);
+ Assert.True(opts.IncludeListAttachments);
+ Assert.True(opts.IncludeRecycleBin);
+ }
}
diff --git a/SharepointToolbox.Tests/ViewModels/StorageViewModelFilterTests.cs b/SharepointToolbox.Tests/ViewModels/StorageViewModelFilterTests.cs
new file mode 100644
index 0000000..7c8f7db
--- /dev/null
+++ b/SharepointToolbox.Tests/ViewModels/StorageViewModelFilterTests.cs
@@ -0,0 +1,141 @@
+using System.Reflection;
+using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using SharepointToolbox.Core.Models;
+using SharepointToolbox.Services;
+using SharepointToolbox.ViewModels;
+using SharepointToolbox.ViewModels.Tabs;
+using Xunit;
+
+namespace SharepointToolbox.Tests.ViewModels;
+
+///
+/// Verifies that the report filter flags (Show*) project the raw scan output
+/// (_allNodes) into Results correctly: hiding a kind drops the
+/// matching root nodes plus their entire subtree, preserving DFS ordering.
+///
+public class StorageViewModelFilterTests
+{
+ public StorageViewModelFilterTests() => WeakReferenceMessenger.Default.Reset();
+
+ private static StorageViewModel CreateVm()
+ {
+ var vm = new StorageViewModel(
+ new Mock().Object,
+ new Mock().Object,
+ NullLogger.Instance);
+ vm.SetCurrentProfile(new TenantProfile { Name = "T", TenantUrl = "https://t", ClientId = "c" });
+ return vm;
+ }
+
+ /// Inject a flat node list straight into the private _allNodes field
+ /// and trigger a rebuild via toggling a Show flag.
+ private static void Seed(StorageViewModel vm, List flat)
+ {
+ var field = typeof(StorageViewModel).GetField("_allNodes",
+ BindingFlags.Instance | BindingFlags.NonPublic)!;
+ field.SetValue(vm, flat);
+ // Toggle off+on to force RebuildFilteredResults().
+ vm.ShowLibraries = false;
+ vm.ShowLibraries = true;
+ }
+
+ private static List MakeMixedTree() => new()
+ {
+ new() { Name = "Documents", Kind = StorageNodeKind.Library, IndentLevel = 0, TotalSizeBytes = 100 },
+ new() { Name = "Sub", Kind = StorageNodeKind.Library, IndentLevel = 1, TotalSizeBytes = 50 },
+ new() { Name = "Preserve", Kind = StorageNodeKind.PreservationHold, IndentLevel = 0, TotalSizeBytes = 200 },
+ new() { Name = "[Recycle]", Kind = StorageNodeKind.RecycleBin, IndentLevel = 0, TotalSizeBytes = 300 },
+ new() { Name = "[Attach] L",Kind = StorageNodeKind.ListAttachments, IndentLevel = 0, TotalSizeBytes = 75 },
+ };
+
+ [Fact]
+ public void AllShowFlagsTrue_AllNodesAppear()
+ {
+ var vm = CreateVm();
+ Seed(vm, MakeMixedTree());
+ Assert.Equal(5, vm.Results.Count);
+ }
+
+ [Fact]
+ public void HideRecycleBin_RemovesOnlyRecycleNode()
+ {
+ var vm = CreateVm();
+ Seed(vm, MakeMixedTree());
+ vm.ShowRecycleBin = false;
+ Assert.DoesNotContain(vm.Results, n => n.Kind == StorageNodeKind.RecycleBin);
+ Assert.Equal(4, vm.Results.Count);
+ }
+
+ [Fact]
+ public void HidePreservationHold_RemovesPreservationNode()
+ {
+ var vm = CreateVm();
+ Seed(vm, MakeMixedTree());
+ vm.ShowPreservationHold = false;
+ Assert.DoesNotContain(vm.Results, n => n.Kind == StorageNodeKind.PreservationHold);
+ }
+
+ [Fact]
+ public void HideLibraries_DropsLibraryRootAndItsChildren()
+ {
+ var vm = CreateVm();
+ Seed(vm, MakeMixedTree());
+ vm.ShowLibraries = false;
+ // "Documents" + "Sub" both gone — Sub's subtree dropped with its parent root.
+ Assert.DoesNotContain(vm.Results, n => n.Name == "Documents");
+ Assert.DoesNotContain(vm.Results, n => n.Name == "Sub");
+ }
+
+ [Fact]
+ public void CombineRecycleBinStages_True_MergesStagesIntoSingleRow()
+ {
+ var vm = CreateVm();
+ var nodes = new List
+ {
+ new() { Name = "[Recycle Bin] First-stage", Kind = StorageNodeKind.RecycleBin, IndentLevel = 0,
+ SiteTitle = "S1", TotalSizeBytes = 100, FileStreamSizeBytes = 100, TotalFileCount = 3 },
+ new() { Name = "[Recycle Bin] Second-stage", Kind = StorageNodeKind.RecycleBin, IndentLevel = 0,
+ SiteTitle = "S1", TotalSizeBytes = 250, FileStreamSizeBytes = 250, TotalFileCount = 7 },
+ };
+ Seed(vm, nodes);
+ vm.CombineRecycleBinStages = true;
+
+ var bins = vm.Results.Where(n => n.Kind == StorageNodeKind.RecycleBin).ToList();
+ Assert.Single(bins);
+ Assert.Equal(350, bins[0].TotalSizeBytes);
+ Assert.Equal(10, bins[0].TotalFileCount);
+ }
+
+ [Fact]
+ public void CombineRecycleBinStages_False_KeepsSeparateRows()
+ {
+ var vm = CreateVm();
+ var nodes = new List
+ {
+ new() { Name = "[Recycle Bin] First-stage", Kind = StorageNodeKind.RecycleBin, IndentLevel = 0,
+ SiteTitle = "S1", TotalSizeBytes = 100 },
+ new() { Name = "[Recycle Bin] Second-stage", Kind = StorageNodeKind.RecycleBin, IndentLevel = 0,
+ SiteTitle = "S1", TotalSizeBytes = 250 },
+ };
+ Seed(vm, nodes);
+ vm.CombineRecycleBinStages = false;
+
+ Assert.Equal(2, vm.Results.Count(n => n.Kind == StorageNodeKind.RecycleBin));
+ }
+
+ [Fact]
+ public void HideAll_LeavesEmptyResults()
+ {
+ var vm = CreateVm();
+ Seed(vm, MakeMixedTree());
+ vm.ShowLibraries = false;
+ vm.ShowHiddenLibraries = false;
+ vm.ShowPreservationHold = false;
+ vm.ShowListAttachments = false;
+ vm.ShowRecycleBin = false;
+ vm.ShowSubsites = false;
+ Assert.Empty(vm.Results);
+ }
+}
diff --git a/SharepointToolbox/App.xaml b/SharepointToolbox/App.xaml
index 5d96c6a..96a5640 100644
--- a/SharepointToolbox/App.xaml
+++ b/SharepointToolbox/App.xaml
@@ -16,6 +16,7 @@
+