308 lines
12 KiB
C#
308 lines
12 KiB
C#
using Explorer.Application;
|
|
using Explorer.Domain;
|
|
using Explorer.Domain.Abstractions;
|
|
using Explorer.Storage.Sqlite;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace Explorer.Application.Tests;
|
|
|
|
public class SourceManagerTests
|
|
{
|
|
[Fact]
|
|
public async Task Removable_drive_keeps_stable_key_when_letter_changes()
|
|
{
|
|
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
|
|
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
|
await store.OpenAsync();
|
|
var volumes = new FakeVolumes();
|
|
volumes.Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Removable,
|
|
VolumeGuid = @"\\?\Volume{same}\",
|
|
RootPath = @"E:\",
|
|
DisplayName = "Stick E",
|
|
VolumeSerial = 9,
|
|
CapacityBytes = 64
|
|
}
|
|
];
|
|
var env = new FakeEnv(Path.GetDirectoryName(db)!);
|
|
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
await mgr.InitializeAsync();
|
|
var first = (await store.Sources.GetAllAsync()).Single();
|
|
var key = first.StableKey;
|
|
|
|
volumes.Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Removable,
|
|
VolumeGuid = @"\\?\Volume{same}\",
|
|
RootPath = @"G:\",
|
|
DisplayName = "Stick G",
|
|
VolumeSerial = 9,
|
|
CapacityBytes = 64
|
|
}
|
|
];
|
|
await mgr.RefreshOnlineStateAsync();
|
|
var again = (await store.Sources.GetAllAsync()).Single();
|
|
Assert.Equal(key, again.StableKey);
|
|
Assert.Equal(@"G:\", again.LastRootPath);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Ensure_for_mapped_letter_creates_source()
|
|
{
|
|
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
|
|
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
|
await store.OpenAsync();
|
|
var volumes = new FakeVolumes
|
|
{
|
|
Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Smb,
|
|
RootPath = @"Z:\",
|
|
DisplayName = "Z: (Network)",
|
|
Filesystem = "SMB"
|
|
}
|
|
]
|
|
};
|
|
var env = new FakeEnv(Path.GetDirectoryName(db)!);
|
|
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
volumes.Online = [];
|
|
await mgr.InitializeAsync();
|
|
volumes.Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Smb,
|
|
RootPath = @"Z:\",
|
|
DisplayName = "Z: (Network)",
|
|
Filesystem = "SMB"
|
|
}
|
|
];
|
|
var source = await mgr.EnsureForPathAsync(@"Z:\Photos");
|
|
Assert.NotNull(source);
|
|
Assert.Equal(@"Z:\", source!.LastRootPath);
|
|
var found = await mgr.FindByPathAsync(@"Z:\");
|
|
Assert.Equal(source.Id, found!.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Forget_removes_disconnected_source_and_index_rows()
|
|
{
|
|
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
|
|
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
|
await store.OpenAsync();
|
|
var volumes = new FakeVolumes
|
|
{
|
|
Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Smb,
|
|
RootPath = @"Z:\",
|
|
DisplayName = "Z: (Network)",
|
|
Filesystem = "SMB"
|
|
}
|
|
]
|
|
};
|
|
var env = new FakeEnv(Path.GetDirectoryName(db)!);
|
|
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
await mgr.InitializeAsync();
|
|
var source = await mgr.EnsureForPathAsync(@"Z:\");
|
|
Assert.NotNull(source);
|
|
await store.Entries.UpsertAsync(new IndexEntry
|
|
{
|
|
SourceId = source.Id,
|
|
Name = "Photos",
|
|
NameNorm = "photos",
|
|
IsDirectory = true,
|
|
PathRel = "Photos",
|
|
LastSeenUtc = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
Assert.False(mgr.CanForget(source));
|
|
Assert.False(await mgr.ForgetDisconnectedAsync(@"Z:\"));
|
|
|
|
volumes.Online = [];
|
|
await mgr.RefreshOnlineStateAsync();
|
|
source = (await store.Sources.GetAllAsync()).Single();
|
|
Assert.True(mgr.CanForget(source));
|
|
Assert.True(await mgr.ForgetDisconnectedAsync(@"Z:\"));
|
|
Assert.Empty(await store.Sources.GetAllAsync());
|
|
Assert.Null(await store.Entries.GetRootAsync(source.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Stale_scanning_status_clears_when_no_job_is_running()
|
|
{
|
|
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
|
|
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
|
await store.OpenAsync();
|
|
var volumes = new FakeVolumes
|
|
{
|
|
Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.NtfsLocal,
|
|
RootPath = @"D:\",
|
|
DisplayName = "Games (D:)",
|
|
VolumeSerial = 42,
|
|
CapacityBytes = 1000
|
|
}
|
|
]
|
|
};
|
|
var env = new FakeEnv(Path.GetDirectoryName(db)!);
|
|
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
await mgr.InitializeAsync();
|
|
var source = (await store.Sources.GetAllAsync()).Single();
|
|
await store.Sources.UpdateStatusAsync(source.Id, SourceStatus.Scanning, null);
|
|
await store.Sources.UpdateIndexedAsync(source.Id, DateTimeOffset.UtcNow, 1);
|
|
|
|
await mgr.RefreshOnlineStateAsync();
|
|
source = (await store.Sources.GetAllAsync()).Single();
|
|
Assert.Equal(SourceStatus.Online, source.Status);
|
|
Assert.True(source.IsIndexed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Forget_removes_unc_from_recents()
|
|
{
|
|
var dir = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"));
|
|
var db = Path.Combine(dir, "index.db");
|
|
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
|
var volumes = new FakeVolumes();
|
|
var env = new FakeEnv(dir);
|
|
File.WriteAllLines(Path.Combine(dir, "recents.txt"), [@"\\old-server\share"]);
|
|
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
await mgr.InitializeAsync();
|
|
Assert.Single(await store.Sources.GetAllAsync());
|
|
Assert.True(await mgr.ForgetDisconnectedAsync(@"\\old-server\share"));
|
|
Assert.Empty(await store.Sources.GetAllAsync());
|
|
Assert.Empty(File.ReadAllLines(Path.Combine(dir, "recents.txt")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Forget_then_rediscover_same_volume_guid()
|
|
{
|
|
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
|
|
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
|
await store.OpenAsync();
|
|
var guid = @"\\?\Volume{phase2}\";
|
|
var volumes = new FakeVolumes
|
|
{
|
|
Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Removable,
|
|
VolumeGuid = guid,
|
|
RootPath = @"E:\",
|
|
DisplayName = "Stick",
|
|
VolumeSerial = 9,
|
|
CapacityBytes = 64
|
|
}
|
|
]
|
|
};
|
|
var env = new FakeEnv(Path.GetDirectoryName(db)!);
|
|
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
await mgr.InitializeAsync();
|
|
Assert.Equal(guid, (await store.Sources.GetAllAsync()).Single().VolumeGuid);
|
|
|
|
volumes.Online = [];
|
|
await mgr.RefreshOnlineStateAsync();
|
|
Assert.True(await mgr.ForgetDisconnectedAsync(@"E:\"));
|
|
Assert.Empty(await store.Sources.GetAllAsync());
|
|
|
|
volumes.Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Removable,
|
|
VolumeGuid = guid,
|
|
RootPath = @"F:\",
|
|
DisplayName = "Stick",
|
|
VolumeSerial = 9,
|
|
CapacityBytes = 64
|
|
}
|
|
];
|
|
await mgr.RefreshOnlineStateAsync();
|
|
var restored = Assert.Single(await store.Sources.GetAllAsync());
|
|
Assert.Equal(guid, restored.VolumeGuid);
|
|
Assert.Equal(@"F:\", restored.LastRootPath);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mapped_network_drive_is_not_auto_added_until_imported()
|
|
{
|
|
var db = Path.Combine(Path.GetTempPath(), "ew-app", Guid.NewGuid().ToString("N"), "index.db");
|
|
await using var store = new SqliteIndexStore(db, NullLogger<SqliteIndexStore>.Instance);
|
|
await store.OpenAsync();
|
|
var volumes = new FakeVolumes
|
|
{
|
|
Online =
|
|
[
|
|
new VolumeFingerprint
|
|
{
|
|
Kind = SourceKind.Smb,
|
|
RootPath = @"Z:\",
|
|
DisplayName = "Z: (Network)",
|
|
Filesystem = "SMB"
|
|
}
|
|
]
|
|
};
|
|
var env = new FakeEnv(Path.GetDirectoryName(db)!);
|
|
var mgr = new SourceManager(store, volumes, env, new SystemClock(), NullLogger<SourceManager>.Instance);
|
|
await mgr.InitializeAsync();
|
|
Assert.Empty(await store.Sources.GetAllAsync());
|
|
var untracked = Assert.Single(await mgr.ListUntrackedOnlineVolumesAsync());
|
|
Assert.Equal(@"Z:\", untracked.RootPath);
|
|
|
|
var imported = await mgr.EnsureForPathAsync(@"Z:\");
|
|
Assert.NotNull(imported);
|
|
Assert.False(imported!.IsIndexed);
|
|
Assert.Empty(await mgr.ListUntrackedOnlineVolumesAsync());
|
|
Assert.Equal(imported.Id, (await store.Sources.GetAllAsync()).Single().Id);
|
|
}
|
|
}
|
|
|
|
file sealed class FakeVolumes : IVolumeService
|
|
{
|
|
public List<VolumeFingerprint> Online { get; set; } = [];
|
|
public IReadOnlyList<VolumeFingerprint> EnumerateOnlineVolumes() => Online;
|
|
public VolumeFingerprint? Probe(string path)
|
|
{
|
|
var root = Path.GetPathRoot(path)?.TrimEnd('\\');
|
|
return Online.FirstOrDefault(v =>
|
|
v.RootPath.TrimEnd('\\').Equals(root, StringComparison.OrdinalIgnoreCase)
|
|
|| v.RootPath.Equals(path, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
public bool IsPathReachable(string path) => Online.Any(v => path.StartsWith(v.RootPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase));
|
|
public VolumeSpace GetSpace(string path)
|
|
{
|
|
var fp = Probe(path);
|
|
return new VolumeSpace(fp?.CapacityBytes, fp?.FreeBytes);
|
|
}
|
|
}
|
|
|
|
file sealed class FakeEnv : IAppEnvironment
|
|
{
|
|
public FakeEnv(string dir)
|
|
{
|
|
DataDirectory = dir;
|
|
Directory.CreateDirectory(dir);
|
|
DatabasePath = Path.Combine(dir, "index.db");
|
|
LogDirectory = Path.Combine(dir, "logs");
|
|
Directory.CreateDirectory(LogDirectory);
|
|
}
|
|
|
|
public string DataDirectory { get; }
|
|
public string DatabasePath { get; }
|
|
public string LogDirectory { get; }
|
|
}
|