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
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Expand All @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Validate Docker.DotNet version and gate Docker-driven tests when the daemon isn’t available.

Pin looks fine for net6.0, but please double-check the version against your CI images. Add a test category/assumption to skip Docker tests when the engine isn’t reachable to keep pipelines green on Windows/macOS agents without Docker.

Use this quick check to find tests already gated by category and confirm at least one suite depends on Docker:

If missing, wrap Docker-dependent fixtures/tests with [Category("Docker")] and add an environment/assumption check (e.g., try connecting via DockerClient and Assert.Ignore on failure).


🏁 Script executed:

#!/bin/bash
rg -nP '\[Category\("Docker"\)|DockerClient|Docker\.DotNet' -C2

Length of output: 647


Validate Docker.DotNet version and gate Docker-driven tests when the daemon isn’t available

It looks like you’ve correctly pinned Docker.DotNet to version 3.125.15 in your test project, but we didn’t find any existing test gating or assumptions around Docker availability. To prevent CI failures on agents without Docker (e.g., Windows/macOS hosted runners), please:

  • Verify the Docker.DotNet version
    Ensure v3.125.15 is compatible with the Docker engine version installed in your CI environments. If your images use an older or newer Docker API, you may need to bump or roll back this package reference in
    Frends.Files.Move/Frends.Files.Move.Tests/Frends.Files.Move.Tests.csproj (Line 11).

  • Annotate Docker-dependent tests
    Wrap any tests or fixtures that rely on Docker with a Docker-specific category. For NUnit, add:

    [Category("Docker")]
    public class MyDockerTests {}

    Or for MSTest:

    [TestCategory("Docker")]
    public class MyDockerTests {}
  • Skip tests when Docker isn’t reachable
    At the start of each Docker-dependent test or fixture setup, attempt a lightweight ping to the daemon and call the appropriate skip/ignore API on failure. For example (NUnit):

    var client = new DockerClientConfiguration().CreateClient();
    try
    {
        client.System.PingAsync().Wait();
    }
    catch
    {
        Assert.Ignore("Docker daemon is not available; skipping Docker tests.");
    }

    This will keep your pipelines green on agents that can’t run Docker.

Once these changes are in place, your suite will correctly isolate and skip Docker-driven tests when the engine isn’t available.

🤖 Prompt for AI Agents
In Frends.Files.Move/Frends.Files.Move.Tests/Frends.Files.Move.Tests.csproj
around line 11, you've pinned Docker.DotNet to v3.125.15 but haven't handled CI
agents without Docker; verify that v3.125.15 matches the Docker Engine API used
in your CI and update the PackageReference if incompatible, and then annotate
all Docker-dependent tests/fixtures with a Docker-specific test category (e.g.,
"Docker") and add a lightweight Docker daemon availability check in each Docker
test setup that performs a ping/health call and calls the test framework's
skip/ignore API when the daemon is unreachable so those tests are skipped on
hosts without Docker.

<PackageReference Include="MSTest.TestFramework" Version="3.8.3" />
<PackageReference Include="nunit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

_options = new Options
{
UseGivenUserCredentialsForRemoteConnections = true,
//UseGivenUserCredentialsForRemoteConnections = true,
UserName = $"{_domain}\\{_name}",
Password = _pwd
};
Comment on lines +41 to 44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

This test no longer exercises impersonation; set RemotePath to trigger credential validation.

With the old flag commented out, Files.Move may treat the operation as local-only and skip user/domain validation. Assign RemotePath accordingly so the test asserts the credential path.

         _options = new Options
         {
-            //UseGivenUserCredentialsForRemoteConnections = true,
+            RemotePath = RemotePathType.Source,
             UserName = $"{_domain}\\{_name}",
             Password = _pwd
         };

If the test intends to validate writing under impersonation, use RemotePathType.Target instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//UseGivenUserCredentialsForRemoteConnections = true,
UserName = $"{_domain}\\{_name}",
Password = _pwd
};
_options = new Options
{
RemotePath = RemotePathType.Source,
UserName = $"{_domain}\\{_name}",
Password = _pwd
};
🤖 Prompt for AI Agents
In Frends.Files.Move/Frends.Files.Move.Tests/ImpersonationTests.cs around lines
41–44, the test no longer triggers impersonation because
UseGivenUserCredentialsForRemoteConnections is commented out and the RemotePath
is not set for a remote operation; update the test to assign RemotePath to a
remote location (e.g. a UNC path) so credential validation runs, or set
RemotePathType to RemotePathType.Target to force remote credential use, and if
needed re-enable UseGivenUserCredentialsForRemoteConnections = true to ensure
the impersonation path is exercised.

Expand Down Expand Up @@ -80,12 +80,12 @@
{
var options = new Options
{
UseGivenUserCredentialsForRemoteConnections = true,
//UseGivenUserCredentialsForRemoteConnections = true,
UserName = "test",
Password = _pwd
};
Comment on lines +83 to 86
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Domain-less username test should also set RemotePath to ensure the expected ArgumentException.

Same issue as above. Without RemotePath, the validation might not run. Update the options and keep the assertion.

         var options = new Options
         {
-            //UseGivenUserCredentialsForRemoteConnections = true,
+            RemotePath = RemotePathType.Source,
             UserName = "test",
             Password = _pwd
         };

Also applies to: 88-90

🤖 Prompt for AI Agents
In Frends.Files.Move/Frends.Files.Move.Tests/ImpersonationTests.cs around lines
83-86 (and similarly for lines 88-90), the domain-less username test sets
UserName and Password but omits RemotePath so the validation that throws the
expected ArgumentException may not run; update the test options to include a
valid RemotePath value (e.g. a non-empty UNC or placeholder path) before
invoking the method and leave the existing assertion unchanged so the test
triggers and asserts the ArgumentException as intended.


var ex = Assert.ThrowsAsync<ArgumentException>(() => Files.Move(_input, options, default));
ClassicAssert.AreEqual($@"UserName field must be of format domain\username was: {options.UserName}", ex.Message);

Check warning on line 89 in Frends.Files.Move/Frends.Files.Move.Tests/ImpersonationTests.cs

View workflow job for this annotation

GitHub Actions / build / Build on windows-latest

Dereference of a possibly null reference.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
using Frends.Files.Move.Definitions;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Threading;

namespace Frends.Files.Move.Tests.Samba;

[TestFixture]
public class ImpersonationSambaDockerTests
{
private string _localSourceDir;
private string _localTargetDir;
private string _remoteDir;
private Options _options;
private bool _isDockerEnvironment;

[OneTimeSetUp]
public void OneTimeSetup()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ImpersonationDockerTests_" + Guid.NewGuid().ToString("N")[..8]);
_localSourceDir = Path.Combine(tempRoot, "Source");
_localTargetDir = Path.Combine(tempRoot, "Target");
Directory.CreateDirectory(_localSourceDir);
Directory.CreateDirectory(_localTargetDir);

WaitForDockerContainer("impersonation_test_samba", TimeSpan.FromMinutes(2));

var connectionMethods = new[]
{
@"\\localhost:1445\testshare", // Docker port mapping
@"\\127.0.0.1:1445\testshare", // Localhost IP with port
GetContainerDirectPath(), // Container IP fallback
@"\\host.docker.internal:1445\testshare" // Docker Desktop host
};

_isDockerEnvironment = false;
string lastError = "";

foreach (var path in connectionMethods)
{
if (string.IsNullOrEmpty(path)) continue;

try
{
if (TestSambaConnection(path, "testuser", "password123!", TimeSpan.FromSeconds(10)))
{
_remoteDir = path;
_isDockerEnvironment = true;
break;
}
}
catch (Exception ex)
{
lastError = ex.Message;
continue;
}
}

if (!_isDockerEnvironment)
{
Assert.Ignore("Docker Samba environment not available");
}

_options = new Options
{
UserName = @"WORKGROUP\testuser",
Password = "password123!",
CreateTargetDirectories = true,
IfTargetFileExists = FileExistsAction.Overwrite
};
}

private void WaitForDockerContainer(string containerName, TimeSpan timeout)
{
var stopwatch = Stopwatch.StartNew();

while (stopwatch.Elapsed < timeout)
{
try
{
var psi = new ProcessStartInfo("docker", $"exec {containerName} smbclient -L //localhost/ -U testuser%password123! --option=\"client min protocol=SMB2\"")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
Comment on lines +86 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Password visible in process arguments.

The password is passed directly in command-line arguments, which could be visible in process listings.

While this is test code, consider using environment variables or stdin for passing credentials to avoid exposing them in process arguments:

-                var psi = new ProcessStartInfo("docker", $"exec {containerName} smbclient -L //localhost/ -U testuser%password123! --option=\"client min protocol=SMB2\"")
+                // Consider using environment variables or stdin to pass credentials
+                var psi = new ProcessStartInfo("docker", $"exec {containerName} sh -c 'echo password123! | smbclient -L //localhost/ -U testuser --option=\"client min protocol=SMB2\"'")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var psi = new ProcessStartInfo("docker", $"exec {containerName} smbclient -L //localhost/ -U testuser%password123! --option=\"client min protocol=SMB2\"")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
// Consider using environment variables or stdin to pass credentials
var psi = new ProcessStartInfo("docker", $"exec {containerName} sh -c 'echo password123! | smbclient -L //localhost/ -U testuser --option=\"client min protocol=SMB2\"'")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};


using var process = Process.Start(psi);
if (process.WaitForExit(5000) && process.ExitCode == 0)

Check warning on line 95 in Frends.Files.Move/Frends.Files.Move.Tests/Samba/ImpersonationSambaDockerTests.cs

View workflow job for this annotation

GitHub Actions / build / Build on windows-latest

Dereference of a possibly null reference.
{
return;
}
}
catch { }

Console.WriteLine($"⏳ Waiting for container {containerName} to be ready... ({stopwatch.Elapsed.TotalSeconds:F0}s)");
Thread.Sleep(2000);
}
}

private string GetContainerDirectPath()
{
try
{
var psi = new ProcessStartInfo("docker", "inspect impersonation_test_samba -f \"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\"")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using var process = Process.Start(psi);
if (process.WaitForExit(5000) && process.ExitCode == 0)

Check warning on line 120 in Frends.Files.Move/Frends.Files.Move.Tests/Samba/ImpersonationSambaDockerTests.cs

View workflow job for this annotation

GitHub Actions / build / Build on windows-latest

Dereference of a possibly null reference.
{
var ip = process.StandardOutput.ReadToEnd().Trim();
if (!string.IsNullOrEmpty(ip) && ip != "localhost")
{
return $@"\\{ip}\testshare";
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to get container IP: {ex.Message}");
}

return null;

Check warning on line 134 in Frends.Files.Move/Frends.Files.Move.Tests/Samba/ImpersonationSambaDockerTests.cs

View workflow job for this annotation

GitHub Actions / build / Build on windows-latest

Possible null reference return.
}

private bool TestSambaConnection(string uncPath, string username, string password, TimeSpan timeout)
{
var host = ExtractHostFromUncPath(uncPath);

var psi = new ProcessStartInfo("docker", $"exec impersonation_test_samba smbclient //{host}/testshare -U {username}%{password} -c dir")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using var process = Process.Start(psi);
return process.WaitForExit((int)timeout.TotalMilliseconds) && process.ExitCode == 0;

Check warning on line 150 in Frends.Files.Move/Frends.Files.Move.Tests/Samba/ImpersonationSambaDockerTests.cs

View workflow job for this annotation

GitHub Actions / build / Build on windows-latest

Dereference of a possibly null reference.
}

private string ExtractHostFromUncPath(string uncPath)
{
// Extract host
var parts = uncPath.TrimStart('\\').Split('\\');
return parts.Length > 0 ? parts[0] : "localhost";
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
var tempRoot = Path.GetDirectoryName(_localSourceDir);
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, true);
}
}

[SetUp]
public void Setup()
{
Helper.CreateTestFiles(_localSourceDir);
}

[TearDown]
public void TearDown()
{
if (Directory.Exists(_localTargetDir))
{
Helper.DeleteTestFolder(_localTargetDir);
Directory.CreateDirectory(_localTargetDir);
}

if (Directory.Exists(_localSourceDir))
{
Helper.DeleteTestFolder(_localSourceDir);
Directory.CreateDirectory(_localSourceDir);
}
try
{
CleanRemoteDirectory();
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Failed to clean remote directory: {ex.Message}");
}
}

private void CleanRemoteDirectory()
{
var psi = new ProcessStartInfo("docker", $"exec impersonation_test_samba find /mount/testshare -type f -delete")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using var process = Process.Start(psi);
process.WaitForExit(10000);

Check warning on line 211 in Frends.Files.Move/Frends.Files.Move.Tests/Samba/ImpersonationSambaDockerTests.cs

View workflow job for this annotation

GitHub Actions / build / Build on windows-latest

Dereference of a possibly null reference.
}

[Test]
public async Task RemoteToLocal_WithImpersonation_Success()
{
CreateRemoteTestFiles();

var input = new Input
{
Directory = _remoteDir,
Pattern = "*.txt",
TargetDirectory = _localTargetDir
};

_options.RemotePath = RemotePathType.Source;

var result = await Files.Move(input, _options, default);

ClassicAssert.AreEqual(5, result.Files.Count);

foreach (var file in result.Files)
{
ClassicAssert.IsTrue(File.Exists(file.TargetPath));

var content = await File.ReadAllTextAsync(file.TargetPath);
ClassicAssert.IsNotEmpty(content);

ClassicAssert.IsFalse(File.Exists(file.SourcePath));
}
}

[Test]
public async Task LocalToRemote_WithImpersonation_Success()
{
var input = new Input
{
Directory = _localSourceDir,
Pattern = "*.txt",
TargetDirectory = _remoteDir
};

_options.RemotePath = RemotePathType.Target;

var result = await Files.Move(input, _options, default);

ClassicAssert.AreEqual(5, result.Files.Count);

foreach (var file in result.Files)
{
ClassicAssert.IsTrue(File.Exists(file.TargetPath));
ClassicAssert.IsFalse(File.Exists(file.SourcePath));
}
}

private void CreateRemoteTestFiles()
{
// Use docker exec to create test files in the container
var commands = new[]
{
"echo 'Test1 content' > /mount/testshare/Test1.txt",
"echo 'Test2 content' > /mount/testshare/Test2.txt",
"echo 'Test3 content' > /mount/testshare/Test3.txt",
"echo 'Test4 content' > /mount/testshare/Test4.txt",
"echo 'Test5 content' > /mount/testshare/Test5.txt"
};

foreach (var command in commands)
{
var psi = new ProcessStartInfo("docker", $"exec impersonation_test_samba sh -c \"{command}\"")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using var process = Process.Start(psi);
process.WaitForExit(5000);
}
}
}
41 changes: 41 additions & 0 deletions Frends.Files.Move/Frends.Files.Move.Tests/Samba/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# docker-compose.yml
version: '3.8'
services:
samba:
image: dperson/samba:latest
container_name: impersonation_test_samba
hostname: samba-test
ports:
- "1445:445" # Map to port 1445 to avoid conflicts
volumes:
- samba_data:/mount/testshare # Use named volume instead of tmpfs
environment:
- USER=testuser;password123!
- SHARE=testshare;/mount/testshare;yes;no;no;testuser;testuser;testuser
- SMB=true
- NMBD=true
- WORKGROUP=WORKGROUP
command: >
sh -c "
/usr/bin/samba.sh -u 'testuser;password123!' -s 'testshare;/mount/testshare;yes;no;no;testuser' -p &&
chmod 777 /mount/testshare &&
chown testuser:testuser /mount/testshare &&
tail -f /dev/null
"
restart: unless-stopped
networks:
- test-net
healthcheck:
test: ["CMD", "smbclient", "-L", "//localhost/", "-U", "testuser%password123!", "--option=client min protocol=SMB2"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s

volumes:
samba_data:
driver: local

networks:
test-net:
driver: bridge
6 changes: 3 additions & 3 deletions Frends.Files.Move/Frends.Files.Move.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

_options = new Options
{
UseGivenUserCredentialsForRemoteConnections = false,
RemotePath = RemotePathType.None,
CreateTargetDirectories = false,
IfTargetFileExists = FileExistsAction.Throw,
PreserveDirectoryStructure = true,
Expand Down Expand Up @@ -59,7 +59,7 @@
{
var options = new Options
{
UseGivenUserCredentialsForRemoteConnections = false,
RemotePath = RemotePathType.None,
CreateTargetDirectories = false,
IfTargetFileExists = FileExistsAction.Throw,
PreserveDirectoryStructure = true
Expand All @@ -76,7 +76,7 @@
{
var options = new Options
{
UseGivenUserCredentialsForRemoteConnections = false,
RemotePath = RemotePathType.None,
CreateTargetDirectories = true,
IfTargetFileExists = FileExistsAction.Throw,
PreserveDirectoryStructure = true
Expand Down Expand Up @@ -134,7 +134,7 @@
};

var ex = Assert.ThrowsAsync<DirectoryNotFoundException>(() => Files.Move(input, _options, default));
ClassicAssert.AreEqual($"Directory does not exist or you do not have read access. Tried to access directory '{input.Directory}'", ex.Message);

Check warning on line 137 in Frends.Files.Move/Frends.Files.Move.Tests/UnitTests.cs

View workflow job for this annotation

GitHub Actions / build / Build on windows-latest

Dereference of a possibly null reference.
}

[Test]
Expand Down
Loading
Loading