Keep official clients in charge of sync while Explorer can group locations, persist UI prefs, and list zip/rar/7z members without extracting them. Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
12 KiB
C#
262 lines
12 KiB
C#
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 Enrich_passes_through_when_provider_throws()
|
|
{
|
|
var registry = new StorageProviderRegistry([new ThrowingProvider()], NullLogger<StorageProviderRegistry>.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<StorageProviderRegistry>.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<StorageProviderRegistry>.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<StorageProviderRegistry>.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<StorageProviderRegistry>.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<ProviderPlace> 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<ProviderPlace> GetPlaces() => throw new InvalidOperationException("places failed");
|
|
public Task<IReadOnlyList<ProviderItemState>> GetItemStatesAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<ProviderItemState>>([]);
|
|
public Task<ProviderActionResult> TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new ProviderActionResult(ProviderActionStatus.Unsupported));
|
|
public Task<ProviderQuota?> TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<ProviderQuota?>(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<IReadOnlyList<ProviderItemState>> GetItemStatesAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken = default)
|
|
=> throw new InvalidOperationException("state failed");
|
|
public Task<ProviderActionResult> TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default)
|
|
=> throw new InvalidOperationException("invoke failed");
|
|
public Task<ProviderQuota?> 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<IReadOnlyList<ProviderItemState>> GetItemStatesAsync(IReadOnlyList<string> paths, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<IReadOnlyList<ProviderItemState>>(paths.Select(p => new ProviderItemState(
|
|
"onedrive", p, Plugin.Abstractions.CloudAvailability.OnlineOnly, 100, 8, true, true, "Online-only", null)).ToList());
|
|
public Task<ProviderActionResult> TryInvokeAsync(ProviderActionRequest request, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new ProviderActionResult(ProviderActionStatus.Succeeded));
|
|
public Task<ProviderQuota?> TryGetQuotaAsync(string rootPath, CancellationToken cancellationToken = default)
|
|
=> Task.FromResult<ProviderQuota?>(null);
|
|
}
|