using Explorer.Application; using Explorer.Domain; using Explorer.Plugin.Abstractions; using Explorer.Plugin.GoogleDrive; using Explorer.Plugin.Nextcloud; using Explorer.Plugin.OneDrive; using Microsoft.Extensions.Logging.Abstractions; namespace Explorer.Application.Tests; public class CloudProviderTests { [Fact] public async Task Quota_returns_null_when_provider_throws() { var registry = new StorageProviderRegistry([new QuotaThrowingProvider()], NullLogger.Instance); Assert.Null(await registry.TryGetQuotaAsync(@"C:\OneDrive")); } [Fact] public async Task Enrich_passes_through_when_provider_throws() { var registry = new StorageProviderRegistry([new ThrowingProvider()], NullLogger.Instance); var item = new FileSystemItem { FullPath = @"C:\a.txt", Name = "a.txt", SizeBytes = 10 }; var result = await registry.EnrichAsync([item]); Assert.Single(result); Assert.Null(result[0].Cloud); Assert.Equal("a.txt", result[0].Name); } [Fact] public async Task Enrich_maps_provider_state() { var registry = new StorageProviderRegistry([new StubProvider()], NullLogger.Instance); var item = new FileSystemItem { FullPath = @"C:\OneDrive\photo.jpg", Name = "photo.jpg", SizeBytes = 100 }; var result = await registry.EnrichAsync([item]); Assert.Equal(Explorer.Domain.CloudAvailability.OnlineOnly, result[0].Cloud?.Availability); Assert.Equal(8, result[0].AllocatedSizeBytes); Assert.True(result[0].Cloud?.MayHydrateOnRead); } [Fact] public void Hydration_guard_skips_online_only_and_recall_attributes() { var registry = new StorageProviderRegistry([], NullLogger.Instance); var guard = new HydrationGuard(registry); Assert.True(guard.WouldHydrateOnRead(AttributeFlags.RecallOnDataAccess, null)); Assert.True(guard.WouldHydrateOnRead(0, Explorer.Domain.CloudAvailability.OnlineOnly)); Assert.False(guard.WouldHydrateOnRead(0, Explorer.Domain.CloudAvailability.LocallyAvailable)); Assert.True(guard.WouldHydrateOnRead(new FileSystemItem { FullPath = @"C:\x", Name = "x", Cloud = new CloudPresence("onedrive", Explorer.Domain.CloudAvailability.OnlineOnly, 1, 0, true) })); } [Theory] [InlineData(OneDrivePlaceholderState.RecallOnDataAccess, null, Plugin.Abstractions.CloudAvailability.OnlineOnly)] [InlineData(OneDrivePlaceholderState.Pinned, null, Plugin.Abstractions.CloudAvailability.Pinned)] [InlineData(0, CloudFilesNativeStates.Invalid, Plugin.Abstractions.CloudAvailability.Error)] [InlineData(0, CloudFilesNativeStates.PlaceholderInSync, Plugin.Abstractions.CloudAvailability.LocallyAvailable)] public void OneDrive_maps_attributes_to_availability(int attributes, uint? cf, Plugin.Abstractions.CloudAvailability expected) { Assert.Equal(expected, OneDrivePlaceholderState.MapAvailability(attributes, cf)); var state = OneDrivePlaceholderState.FromLocalSignals("onedrive", @"C:\OneDrive\f", attributes, 0, 50, 4, cf); Assert.Equal(expected, state.Availability); if (expected == Plugin.Abstractions.CloudAvailability.OnlineOnly) { Assert.True(state.MayHydrateOnRead); } } [Fact] public void Disabled_provider_is_ignored() { var registry = new StorageProviderRegistry([new StubProvider()], NullLogger.Instance); registry.SetEnabled("onedrive", false); Assert.Null(registry.Find(@"C:\OneDrive\a")); Assert.False(registry.HasCapability(@"C:\OneDrive\a", ProviderCapability.Pin)); Assert.Empty(registry.GetPlaces()); } [Fact] public void GetPlaces_fail_open_when_provider_throws() { var registry = new StorageProviderRegistry([new ThrowingPlacesProvider()], NullLogger.Instance); Assert.Empty(registry.GetPlaces()); } [Theory] [InlineData("Personal", @"C:\Users\x\OneDrive", null, "Dominique", "Dominique - Personal")] [InlineData("Business1", @"C:\Users\x\OneDrive - Contoso", "Contoso", "Dominique", "Dominique - Contoso")] [InlineData("Business1", @"C:\Users\x\OneDrive - Contoso", null, null, "OneDrive - Contoso")] [InlineData("Personal", @"C:\Users\x\OneDrive", null, null, "OneDrive")] public void OneDrive_place_label(string key, string folder, string? display, string? user, string expected) => Assert.Equal(expected, OneDrivePlaceDiscovery.BuildLabel(key, folder, display, user)); [Fact] public void Cloud_place_store_merges_manual_without_duplicates() { var discovered = new ProviderPlace[] { new("onedrive", "Dominique - Personal", @"C:\Users\x\OneDrive") }; var manual = new ProviderPlace[] { new("onedrive", "Work", @"C:\Users\x\OneDrive - Contoso"), new("onedrive", "Dup", @"C:\Users\x\OneDrive") }; var merged = CloudPlaceStore.Merge(discovered, manual); Assert.Equal(2, merged.Count); Assert.Equal("Dominique - Personal", merged[0].DisplayName); Assert.Equal(@"C:\Users\x\OneDrive - Contoso", merged[1].Path); } [Fact] public void Cloud_path_is_under_is_prefix_safe() { Assert.True(CloudPath.IsUnder(@"F:\OneDrive - wyniger", @"F:\OneDrive - wyniger\Obsidian\Notes")); Assert.False(CloudPath.IsUnder(@"F:\OneDrive", @"F:\OneDrive - wyniger")); Assert.True(CloudPath.IsUnder(@"J:\", @"J:\My Drive\Photos")); Assert.Equal(@"J:\", CloudPath.NormalizePlace("j:")); Assert.Equal(@"J:\", CloudPath.NormalizePlace(@"J:\")); } [Fact] public void DriveFS_media_named_google_drive_becomes_a_place() { var places = DriveFsPreferenceReader.PlacesFromTables( [ new("Games", @"D:\", 4), new("Google Drive", @"J:\", 10), new("MP3", @"G:\", 4) ], [ new("My Drive", @"My Drive", @"J:\My Drive", true) ]); Assert.Single(places); Assert.Equal("googledrive", places[0].ProviderId); Assert.Equal("Google Drive", places[0].DisplayName); Assert.Equal(@"J:\", places[0].Path); } [Fact] public void DriveFS_mirrored_folder_becomes_a_place_when_no_mount() { var places = DriveFsPreferenceReader.PlacesFromTables( [new("Data", @"F:\", 4)], [new("Photos", @"Photos", @"F:\Google Photos", false)]); Assert.Single(places); Assert.Equal("Google Drive - Photos", places[0].DisplayName); Assert.Equal(@"F:\Google Photos", places[0].Path); } [Fact] public void DriveFS_local_preference_db_exposes_configured_mount() { if (!File.Exists(DriveFsPreferenceReader.DefaultDatabasePath)) { return; } var places = DriveFsPreferenceReader.ReadPlaces(); Assert.Contains(places, p => p.DisplayName == "Google Drive" && p.Path.Length >= 2); } [Theory] [InlineData(@"C:\Users\x\My Drive", "Google Drive")] [InlineData(@"G:\Shared drives", "Shared drives")] [InlineData(@"D:\Work", "Google Drive - Work")] public void Google_drive_place_label(string path, string expected) => Assert.Equal(expected, GoogleDrivePlaceDiscovery.BuildLabel(path, null)); [Fact] public void Nextcloud_parses_account_folders_from_cfg() { const string cfg = """ [Accounts] 0\url=https://cloud.example.com 0\displayName=Dominique Wyniger 0\Folders\1\localPath=F:/OneDrive - wyniger/Obsidian/Notes/ 0\Folders\1\targetPath=/Notizen/Notes 0\Folders\1\virtualFilesMode=off """; var places = NextcloudPlaceDiscovery.ParseCfg(cfg); Assert.Single(places); Assert.Equal("nextcloud", places[0].ProviderId); Assert.Equal(@"F:\OneDrive - wyniger\Obsidian\Notes", places[0].Path); Assert.Equal("Nextcloud - Notes", places[0].DisplayName); } [Fact] public void OneDrive_does_not_claim_google_drive_paths() { Assert.False(OneDriveStorageProvider.LooksLikeOneDrive(@"G:\My Drive\Photos")); Assert.True(GoogleDriveStorageProvider.LooksLikeGoogleDrive(@"G:\My Drive\Photos")); } [Fact] public void Discover_skips_missing_default_profile_folders() { var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var google = GoogleDrivePlaceDiscovery.Discover(); var nextcloud = NextcloudPlaceDiscovery.Discover(); AssertMissingGuess(google, Path.Combine(profile, "Google Drive")); AssertMissingGuess(google, Path.Combine(profile, "My Drive")); AssertMissingGuess(nextcloud, Path.Combine(profile, "Nextcloud")); AssertMissingGuess(nextcloud, Path.Combine(profile, "ownCloud")); } private static void AssertMissingGuess(IReadOnlyList places, string path) { if (Directory.Exists(path)) { return; } Assert.DoesNotContain(places, p => p.Path.Equals(path.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase)); } } file static class CloudFilesNativeStates { public const uint PlaceholderInSync = 0x1 | 0x8; public const uint Invalid = 0xFFFFFFFF; } file sealed class ThrowingPlacesProvider : IStorageProvider { public ProviderManifest Manifest { get; } = new("boom", "Boom", "1", ProviderIsolation.InProcess); public ProviderCapability GetCapabilities() => ProviderCapability.CloudState; public bool TryMatchRoot(string path) => false; public IReadOnlyList GetPlaces() => throw new InvalidOperationException("places failed"); public Task> GetItemStatesAsync(IReadOnlyList paths, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default) => Task.FromResult(new ProviderActionResult(ProviderActionStatus.Unsupported)); public Task TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default) => Task.FromResult(null); } file sealed class ThrowingProvider : IStorageProvider { public ProviderManifest Manifest { get; } = new("boom", "Boom", "1", ProviderIsolation.InProcess); public ProviderCapability GetCapabilities() => ProviderCapability.CloudState; public bool TryMatchRoot(string path) => throw new InvalidOperationException("match failed"); public Task> GetItemStatesAsync(IReadOnlyList paths, CancellationToken cancellationToken = default) => throw new InvalidOperationException("state failed"); public Task TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default) => throw new InvalidOperationException("invoke failed"); public Task TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default) => throw new InvalidOperationException("quota failed"); } file sealed class QuotaThrowingProvider : IStorageProvider { public ProviderManifest Manifest { get; } = new("onedrive", "OneDrive", "1", ProviderIsolation.InProcess); public ProviderCapability GetCapabilities() => ProviderCapability.Quota; public bool TryMatchRoot(string path) => path.Contains("OneDrive", StringComparison.OrdinalIgnoreCase); public Task> GetItemStatesAsync(IReadOnlyList paths, CancellationToken cancellationToken = default) => Task.FromResult>([]); public Task TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default) => Task.FromResult(new ProviderActionResult(ProviderActionStatus.Unsupported)); public Task TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default) => throw new InvalidOperationException("quota failed"); } file sealed class StubProvider : IStorageProvider { public ProviderManifest Manifest { get; } = new("onedrive", "OneDrive", "1", ProviderIsolation.InProcess); public ProviderCapability GetCapabilities() => ProviderCapability.CloudState | ProviderCapability.Pin | ProviderCapability.Dehydrate; public bool TryMatchRoot(string path) => path.Contains("OneDrive", StringComparison.OrdinalIgnoreCase); public Task> GetItemStatesAsync(IReadOnlyList paths, CancellationToken cancellationToken = default) => Task.FromResult>(paths.Select(p => new ProviderItemState( "onedrive", p, Plugin.Abstractions.CloudAvailability.OnlineOnly, 100, 8, true, true, "Online-only", null)).ToList()); public Task TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default) => Task.FromResult(new ProviderActionResult(ProviderActionStatus.Succeeded)); public Task TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default) => Task.FromResult(null); }