using Explorer.Domain; using Explorer.Domain.Abstractions; using Explorer.Storage.Sqlite; using Microsoft.Extensions.Logging.Abstractions; namespace Explorer.Storage.Tests; internal static class Stores { public static async Task Open() { var path = Path.Combine(Path.GetTempPath(), "ew-tests", Guid.NewGuid().ToString("N"), "index.db"); var store = new SqliteIndexStore(path, NullLogger.Instance); await store.OpenAsync(); return store; } public static async Task AddSourceAsync(this IIndexStore store, string root) { var s = new Source { StableKey = Guid.NewGuid().ToString("N"), Kind = SourceKind.NtfsLocal, DisplayName = "Test", LastRootPath = root, Status = SourceStatus.Online }; s.Id = await store.Sources.UpsertAsync(s); return s; } } public class SchemaTests { [Fact] public async Task Opens_and_quick_check_ok() { await using var store = await Stores.Open(); var check = await store.QuickCheckAsync(); Assert.Equal("ok", check, ignoreCase: true); await store.Analysis.EnsureReadyAsync(); await store.Analysis.EnsureReadyAsync(); Assert.Empty(await store.Analysis.GetDirectoryRootsAsync()); Assert.Equal(await store.Analysis.GetIndexStampAsync(), await store.Analysis.GetIndexStampAsync()); } [Fact] public async Task Source_roundtrip_and_identity_fields() { await using var store = await Stores.Open(); var source = await store.AddSourceAsync(@"T:\"); Assert.True(source.Id > 0); source.VolumeGuid = @"\\?\Volume{xyz}\"; source.VolumeSerial = 1234; await store.Sources.UpsertAsync(source); var loaded = await store.Sources.GetAsync(source.Id); Assert.Equal(@"\\?\Volume{xyz}\", loaded!.VolumeGuid); Assert.Equal(1234, loaded.VolumeSerial); } } public class EntryAndSearchTests { [Fact] public async Task Upsert_is_idempotent_and_search_filters_work() { await using var store = await Stores.Open(); var source = await store.AddSourceAsync(@"C:\data"); var root = new IndexEntry { SourceId = source.Id, Name = "data", NameNorm = "data", IsDirectory = true, PathRel = "", LastSeenUtc = DateTimeOffset.UtcNow, ScanGeneration = 1, Status = EntryStatus.Present }; root.Id = await store.Entries.UpsertAsync(root); var file = new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = "Movie.mkv", NameNorm = "movie.mkv", Extension = "mkv", IsDirectory = false, SizeBytes = 12L * 1024 * 1024 * 1024, PathRel = "Movie.mkv", LastSeenUtc = DateTimeOffset.UtcNow, ModifiedUtc = DateTimeOffset.Parse("2024-06-01Z"), ScanGeneration = 1 }; var id1 = await store.Entries.UpsertAsync(file); file.SizeBytes = 13L * 1024 * 1024 * 1024; var id2 = await store.Entries.UpsertAsync(file); Assert.Equal(id1, id2); var found = await store.Search.SearchAsync(new SearchRequest { Name = "*.mkv", MinSize = 10L * 1024 * 1024 * 1024, SourceIds = [source.Id], Take = 50 }); Assert.Contains(found, e => e.Name == "Movie.mkv"); var byDate = await store.Search.SearchAsync(new SearchRequest { ModifiedBefore = DateTimeOffset.Parse("2025-01-01Z"), SourceIds = [source.Id] }); Assert.Contains(byDate, e => e.Name == "Movie.mkv"); } [Fact] public async Task Folder_aggregates_and_tombstones() { await using var store = await Stores.Open(); var source = await store.AddSourceAsync(@"C:\lib"); var root = new IndexEntry { SourceId = source.Id, Name = "lib", NameNorm = "lib", IsDirectory = true, PathRel = "", LastSeenUtc = DateTimeOffset.UtcNow }; root.Id = await store.Entries.UpsertAsync(root); var dir = new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = "Movies", NameNorm = "movies", IsDirectory = true, PathRel = "Movies", LastSeenUtc = DateTimeOffset.UtcNow }; dir.Id = await store.Entries.UpsertAsync(dir); var file = new IndexEntry { SourceId = source.Id, ParentId = dir.Id, Name = "a.bin", NameNorm = "a.bin", IsDirectory = false, SizeBytes = 1000, PathRel = @"Movies\a.bin", LastSeenUtc = DateTimeOffset.UtcNow }; file.Id = await store.Entries.UpsertAsync(file); await store.Entries.ApplySizeDeltaToAncestorsAsync(dir.Id, 1000, 1, 0); var loadedDir = await store.Entries.GetAsync(dir.Id); Assert.Equal(1000, loadedDir!.AggregateSize); await store.Entries.TombstoneAsync(file.Id, DateTimeOffset.UtcNow); loadedDir = await store.Entries.GetAsync(dir.Id); Assert.Equal(0, loadedDir!.AggregateSize); var tomb = await store.Entries.GetAsync(file.Id); Assert.Equal(EntryStatus.Deleted, tomb!.Status); await store.Entries.DeleteExpiredTombstonesAsync(DateTimeOffset.UtcNow.AddDays(1)); Assert.Null(await store.Entries.GetAsync(file.Id)); } [Fact] public async Task Rename_updates_subtree_paths() { await using var store = await Stores.Open(); var source = await store.AddSourceAsync(@"C:\r"); var root = new IndexEntry { SourceId = source.Id, Name = "r", NameNorm = "r", IsDirectory = true, PathRel = "", LastSeenUtc = DateTimeOffset.UtcNow }; root.Id = await store.Entries.UpsertAsync(root); var dir = new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = "Old", NameNorm = "old", IsDirectory = true, PathRel = "Old", LastSeenUtc = DateTimeOffset.UtcNow }; dir.Id = await store.Entries.UpsertAsync(dir); var file = new IndexEntry { SourceId = source.Id, ParentId = dir.Id, Name = "f.txt", NameNorm = "f.txt", PathRel = @"Old\f.txt", LastSeenUtc = DateTimeOffset.UtcNow }; file.Id = await store.Entries.UpsertAsync(file); await store.Entries.RenameSubtreePathAsync(source.Id, "Old", "New"); var moved = await store.Entries.GetAsync(file.Id); Assert.Equal(@"New\f.txt", moved!.PathRel); } [Fact] public async Task Duplicate_candidates_are_same_size_groups() { await using var store = await Stores.Open(); var source = await store.AddSourceAsync(@"C:\d"); var root = new IndexEntry { SourceId = source.Id, Name = "d", NameNorm = "d", IsDirectory = true, PathRel = "", LastSeenUtc = DateTimeOffset.UtcNow }; root.Id = await store.Entries.UpsertAsync(root); foreach (var name in new[] { "a.bin", "b.bin", "c.bin" }) { await store.Entries.UpsertAsync(new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = name, NameNorm = name, IsDirectory = false, SizeBytes = name == "c.bin" ? 50 : 100, PathRel = name, LastSeenUtc = DateTimeOffset.UtcNow }); } await store.Hashes.EnqueueSizeCollisionsAsync(source.Id); var work = await store.Hashes.DequeueAsync(10); Assert.Equal(2, work.Count); Assert.All(work, w => Assert.Equal(100, w.SizeBytes)); } }