using Explorer.Application; using Explorer.Domain; using Explorer.Domain.Abstractions; using Explorer.FileOperations; using Explorer.Storage.Sqlite; using Microsoft.Extensions.Logging.Abstractions; namespace Explorer.FileOperations.Tests; public class FolderSyncServiceTests { [Fact] public async Task Enqueue_copy_waits_when_the_destination_is_offline() { await using var ctx = await SyncHarness.CreateAsync(); var plan = await ctx.Sync.PreviewAsync(ctx.Profile); Assert.True(plan.CanEnqueue); ctx.Volumes.Reachable = false; await ctx.Queue.StartAsync(CancellationToken.None); await ctx.Sync.EnqueueAsync(ctx.Profile, plan); await WaitUntil(() => ctx.Queue.Snapshot().Any(j => j.Status == TransferStatus.Waiting)); Assert.Equal(0, ctx.Shell.CopyCount); ctx.Volumes.Reachable = true; ctx.Queue.NotifyAvailability(); await WaitUntil(() => ctx.Queue.Snapshot().All(j => j.Status == TransferStatus.Done)); Assert.True(File.Exists(Path.Combine(ctx.Dest, "a.txt"))); await ctx.Queue.StopAsync(CancellationToken.None); } [Fact] public async Task Auto_run_fires_only_after_the_destination_returns() { await using var ctx = await SyncHarness.CreateAsync(); ctx.Profile.AutoRun = true; await ctx.Sync.SaveAsync(ctx.Profile); await ctx.Sync.TryAutoRunAsync(); Assert.Empty(ctx.Queue.Snapshot()); ctx.Volumes.Reachable = false; await ctx.Sync.TryAutoRunAsync(); Assert.Empty(ctx.Queue.Snapshot()); ctx.Volumes.Reachable = true; await ctx.Queue.StartAsync(CancellationToken.None); await ctx.Sync.TryAutoRunAsync(); await WaitUntil(() => ctx.Queue.Snapshot().Any(j => j.Status == TransferStatus.Done)); Assert.True(File.Exists(Path.Combine(ctx.Dest, "a.txt"))); File.WriteAllText(Path.Combine(ctx.Root, "b.txt"), "b"); await ctx.Sync.TryAutoRunAsync(); Assert.DoesNotContain(ctx.Queue.Snapshot(), j => j.SourcePath.EndsWith("b.txt", StringComparison.OrdinalIgnoreCase)); await ctx.Queue.StopAsync(CancellationToken.None); } [Fact] public async Task Mirror_auto_run_is_ignored() { await using var ctx = await SyncHarness.CreateAsync(); ctx.Profile.Mode = SyncMode.Mirror; ctx.Profile.AutoRun = true; await ctx.Sync.SaveAsync(ctx.Profile); ctx.Volumes.Reachable = false; await ctx.Sync.TryAutoRunAsync(); ctx.Volumes.Reachable = true; await ctx.Sync.TryAutoRunAsync(); Assert.Empty(ctx.Queue.Snapshot()); } private static async Task WaitUntil(Func condition) { var limit = DateTime.UtcNow + TimeSpan.FromSeconds(4); while (!condition()) { if (DateTime.UtcNow > limit) { throw new TimeoutException("Condition was not met."); } await Task.Delay(20); } } private sealed class SyncHarness : IAsyncDisposable { public required FolderSyncService Sync { get; init; } public required TransferQueue Queue { get; init; } public required GateShell Shell { get; init; } public required ControlledVolumes Volumes { get; init; } public required SqliteIndexStore Store { get; init; } public required SyncProfile Profile { get; init; } public required string Root { get; init; } public required string Dest { get; init; } public static async Task CreateAsync() { var root = Path.Combine(Path.GetTempPath(), "ew-sync", Guid.NewGuid().ToString("N")); var source = Path.Combine(root, "src"); var dest = Path.Combine(root, "dest"); Directory.CreateDirectory(source); Directory.CreateDirectory(dest); File.WriteAllText(Path.Combine(source, "a.txt"), "a"); var store = new SqliteIndexStore(Path.Combine(root, "index.db"), NullLogger.Instance); await store.OpenAsync(); var shell = new GateShell(); var volumes = new ControlledVolumes(); var enumerator = new DiskEnum(); var queue = new TransferQueue( new NativeFileOperationExecutor(shell, enumerator), store, volumes, NullLogger.Instance); var mutations = new LocalIndexMutations(store); var ops = new FileOperationService(queue, shell, enumerator); var env = new SyncEnv(root); var sources = new SourceManager(store, volumes, env, new SystemClock(), NullLogger.Instance); var sync = new FolderSyncService( new FolderSyncPlanner(), store, mutations, sources, ops, volumes, enumerator, new NeverHydrate()); return new SyncHarness { Sync = sync, Queue = queue, Shell = shell, Volumes = volumes, Store = store, Profile = new SyncProfile { Name = "Test", SourcePath = source, DestPath = dest, Mode = SyncMode.CopyUpdate }, Root = source, Dest = dest }; } public async ValueTask DisposeAsync() { await Store.DisposeAsync(); var work = Path.GetDirectoryName(Root); try { if (work is not null) Directory.Delete(work, true); } catch { /* ignore */ } } } private sealed class SyncEnv(string dir) : IAppEnvironment { public string DataDirectory { get; } = dir; public string DatabasePath { get; } = Path.Combine(dir, "index.db"); public string LogDirectory { get; } = Path.Combine(dir, "logs"); } private sealed class NeverHydrate : IHydrationGuard { public bool WouldHydrateOnRead(FileSystemItem item) => false; public bool WouldHydrateOnRead(int attributes, CloudAvailability? availability) => false; public Task WouldHydrateOnReadAsync(string path, CancellationToken cancellationToken = default) => Task.FromResult(false); } }