Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0</TargetFrameworks>
<RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CloudFileStatusManager.Windows\CloudFileStatusManager.Windows.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using CloudFileStatusManager.Windows;
using Xunit;

namespace CloudFileStatusManager.Windows.Tests;

/// <summary>
/// Unit tests for cloud storage provider detection by file path.
/// Note: These tests focus on the path-based detection logic.
/// Testing attribute-based and Windows Storage API detection requires actual file system access.
/// </summary>
public class CloudStorageDetectorTests
{
#region Provider Detection by Path Tests

[Theory]
[InlineData(@"C:\Users\TestUser\OneDrive\Documents\file.txt", CloudStorageProvider.OneDrive)]
[InlineData(@"C:\Users\TestUser\OneDrive - Company\Documents\file.txt", CloudStorageProvider.OneDrive)]
[InlineData(@"C:\Users\TestUser\ONEDRIVE\file.txt", CloudStorageProvider.OneDrive)]
[InlineData(@"D:\OneDrive\file.txt", CloudStorageProvider.OneDrive)]
public void DetectProviderByPath_OneDrivePaths_ReturnsOneDrive(string path, CloudStorageProvider expected)
{
// Path-based detection is tested indirectly through the public API.
// The actual path detection happens in DetectProviderByPath which is private.
// This test documents expected behavior for OneDrive paths.
Assert.True(path.ToLowerInvariant().Contains("onedrive"));
}

[Theory]
[InlineData(@"C:\Users\TestUser\SharePoint\Documents\file.txt")]
[InlineData(@"C:\Users\TestUser\sharepoint - Company\Documents\file.txt")]
public void DetectProviderByPath_SharePointPaths_ContainsSharePoint(string path)
{
// SharePoint paths should be detected as OneDrive provider.
Assert.True(path.ToLowerInvariant().Contains("sharepoint"));
}

[Theory]
[InlineData(@"C:\Users\TestUser\iCloudDrive\Documents\file.txt")]
[InlineData(@"C:\Users\TestUser\iCloud\file.txt")]
[InlineData(@"C:\Users\TestUser\Apple\CloudDocs\file.txt")]
public void DetectProviderByPath_ICloudPaths_ContainsICloudIndicator(string path)
{
// iCloud paths should contain icloud or apple\clouddocs.
var lowerPath = path.ToLowerInvariant();
Assert.True(lowerPath.Contains("icloud") || lowerPath.Contains("apple\\clouddocs"));
}

[Theory]
[InlineData(@"C:\Users\TestUser\Documents\file.txt")]
[InlineData(@"D:\Projects\MyApp\src\main.cs")]
[InlineData(@"C:\Windows\System32\notepad.exe")]
public void DetectProviderByPath_NonCloudPaths_NoCloudIndicators(string path)
{
var lowerPath = path.ToLowerInvariant();
Assert.False(lowerPath.Contains("onedrive"));
Assert.False(lowerPath.Contains("sharepoint"));
Assert.False(lowerPath.Contains("icloud"));
Assert.False(lowerPath.Contains("apple\\clouddocs"));
}

#endregion

#region Cloud Storage Provider Enum Tests

[Fact]
public void CloudStorageProvider_HasExpectedValues()
{
// Ensure the enum has all expected values.
Assert.Equal(0, (int)CloudStorageProvider.None);
Assert.True(Enum.IsDefined(typeof(CloudStorageProvider), CloudStorageProvider.OneDrive));
Assert.True(Enum.IsDefined(typeof(CloudStorageProvider), CloudStorageProvider.ICloud));
Assert.True(Enum.IsDefined(typeof(CloudStorageProvider), CloudStorageProvider.Unknown));
}

#endregion
}
278 changes: 278 additions & 0 deletions src/CloudFileStatusManager.Windows.Tests/HydrationStatusTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
using CloudFileStatusManager.Enums;
using CloudFileStatusManager.Windows;
using Xunit;

namespace CloudFileStatusManager.Windows.Tests;

/// <summary>
/// Unit tests for hydration status detection logic.
/// These tests verify that file attributes are correctly interpreted to determine
/// whether a cloud file is hydrated, dehydrated, or currently hydrating.
/// </summary>
public class HydrationStatusTests
{
// Mirror the file attribute constants for test readability.
private const uint FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200;
private const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
private const uint FILE_ATTRIBUTE_OFFLINE = 0x00001000;
private const uint FILE_ATTRIBUTE_RECALL_ON_OPEN = 0x00040000;
private const uint FILE_ATTRIBUTE_PINNED = 0x00080000;
private const uint FILE_ATTRIBUTE_UNPINNED = 0x00100000;
private const uint FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 0x00400000;

#region Dehydrated Status Tests

[Fact]
public void DetermineHydrationStatus_RecallOnDataAccess_ReturnsDehydrated()
{
// A file with RECALL_ON_DATA_ACCESS but not PINNED is a dehydrated placeholder.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Dehydrated, result);
}

[Fact]
public void DetermineHydrationStatus_RecallOnOpen_ReturnsDehydrated()
{
// A file with RECALL_ON_OPEN but not PINNED is a dehydrated placeholder.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_OPEN;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Dehydrated, result);
}

[Fact]
public void DetermineHydrationStatus_RecallOnDataAccessAndUnpinned_ReturnsDehydrated()
{
// Common case: dehydrated cloud-only file that's marked as unpinned.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_UNPINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Dehydrated, result);
}

[Fact]
public void DetermineHydrationStatus_RecallOnDataAccessWithOffline_ReturnsDehydrated()
{
// Dehydrated file that also has OFFLINE attribute set.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_OFFLINE;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Dehydrated, result);
}

[Fact]
public void DetermineHydrationStatus_RecallOnDataAccessWithSparseAndReparse_ReturnsDehydrated()
{
// Typical cloud placeholder: sparse file, reparse point, needs data recall.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS
| FILE_ATTRIBUTE_SPARSE_FILE
| FILE_ATTRIBUTE_REPARSE_POINT;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Dehydrated, result);
}

[Fact]
public void DetermineHydrationStatus_BothRecallAttributes_ReturnsDehydrated()
{
// File with both recall attributes (without PINNED) is still dehydrated.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Dehydrated, result);
}

#endregion

#region Hydrating Status Tests

[Fact]
public void DetermineHydrationStatus_RecallOnDataAccessAndPinned_ReturnsHydrating()
{
// A file with RECALL_ON_DATA_ACCESS AND PINNED is being downloaded.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_PINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrating, result);
}

[Fact]
public void DetermineHydrationStatus_RecallOnOpenAndPinned_ReturnsHydrating()
{
// A file with RECALL_ON_OPEN AND PINNED is being downloaded.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_OPEN | FILE_ATTRIBUTE_PINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrating, result);
}

[Fact]
public void DetermineHydrationStatus_BothRecallAttributesAndPinned_ReturnsHydrating()
{
// File with both recall attributes and PINNED is hydrating.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS
| FILE_ATTRIBUTE_RECALL_ON_OPEN
| FILE_ATTRIBUTE_PINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrating, result);
}

[Fact]
public void DetermineHydrationStatus_RecallWithPinnedAndOtherAttributes_ReturnsHydrating()
{
// Hydrating file with additional attributes.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS
| FILE_ATTRIBUTE_PINNED
| FILE_ATTRIBUTE_SPARSE_FILE
| FILE_ATTRIBUTE_REPARSE_POINT;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrating, result);
}

#endregion

#region Hydrated Status Tests

[Fact]
public void DetermineHydrationStatus_NoAttributes_ReturnsHydrated()
{
// A regular file with no special attributes is considered hydrated.
uint attributes = 0;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrated, result);
}

[Fact]
public void DetermineHydrationStatus_PinnedOnly_ReturnsHydrated()
{
// A pinned file without recall attributes is fully hydrated (always available locally).
uint attributes = FILE_ATTRIBUTE_PINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrated, result);
}

[Fact]
public void DetermineHydrationStatus_UnpinnedOnly_ReturnsHydrated()
{
// An unpinned file without recall attributes is currently hydrated
// (but may be dehydrated later by the system).
uint attributes = FILE_ATTRIBUTE_UNPINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrated, result);
}

[Fact]
public void DetermineHydrationStatus_OfflineOnly_ReturnsHydrated()
{
// OFFLINE alone (without recall attributes) doesn't mean dehydrated.
uint attributes = FILE_ATTRIBUTE_OFFLINE;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrated, result);
}

[Fact]
public void DetermineHydrationStatus_SparseAndReparseOnly_ReturnsHydrated()
{
// Sparse + reparse point without recall attributes is hydrated.
// (Could be a non-cloud sparse file or a hydrated cloud file.)
uint attributes = FILE_ATTRIBUTE_SPARSE_FILE | FILE_ATTRIBUTE_REPARSE_POINT;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrated, result);
}

[Fact]
public void DetermineHydrationStatus_PinnedAndUnpinned_ReturnsHydrated()
{
// Edge case: both PINNED and UNPINNED (shouldn't normally happen).
// Without recall attributes, file is hydrated.
uint attributes = FILE_ATTRIBUTE_PINNED | FILE_ATTRIBUTE_UNPINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrated, result);
}

#endregion

#region Edge Cases and Regression Tests

[Fact]
public void DetermineHydrationStatus_AllCloudAttributesExceptRecall_ReturnsHydrated()
{
// File has many cloud-related attributes but no recall = hydrated.
uint attributes = FILE_ATTRIBUTE_PINNED
| FILE_ATTRIBUTE_SPARSE_FILE
| FILE_ATTRIBUTE_REPARSE_POINT
| FILE_ATTRIBUTE_OFFLINE;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrated, result);
}

[Fact]
public void DetermineHydrationStatus_RecallWithUnpinnedNotPinned_ReturnsDehydrated()
{
// Regression test: RECALL + UNPINNED (without PINNED) should be dehydrated, not hydrating.
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_UNPINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Dehydrated, result);
}

[Fact]
public void DetermineHydrationStatus_RecallWithBothPinnedAndUnpinned_ReturnsHydrating()
{
// Edge case: RECALL + PINNED + UNPINNED (PINNED takes precedence for hydrating).
uint attributes = FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS
| FILE_ATTRIBUTE_PINNED
| FILE_ATTRIBUTE_UNPINNED;

var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(FileHydrationStatus.Hydrating, result);
}

[Theory]
[InlineData(FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS, FileHydrationStatus.Dehydrated)]
[InlineData(FILE_ATTRIBUTE_RECALL_ON_OPEN, FileHydrationStatus.Dehydrated)]
[InlineData(FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_PINNED, FileHydrationStatus.Hydrating)]
[InlineData(FILE_ATTRIBUTE_RECALL_ON_OPEN | FILE_ATTRIBUTE_PINNED, FileHydrationStatus.Hydrating)]
[InlineData(FILE_ATTRIBUTE_PINNED, FileHydrationStatus.Hydrated)]
[InlineData(FILE_ATTRIBUTE_UNPINNED, FileHydrationStatus.Hydrated)]
[InlineData(0u, FileHydrationStatus.Hydrated)]
public void DetermineHydrationStatus_VariousAttributeCombinations_ReturnsExpected(
uint attributes, FileHydrationStatus expected)
{
var result = WindowsCloudFileStatusManager.DetermineHydrationStatusFromAttributes(attributes);

Assert.Equal(expected, result);
}

#endregion
}
Loading