dev/SboxMcp.Tests/PathJailTests.cs

Unit tests for PathJail.Resolve, verifying that file system paths are correctly allowed or rejected relative to a specified root. Tests cover normal relative and absolute paths inside the root, attempts to escape with .. and different drives, empty input, prefix-sibling edge case, and allowing the root itself.

using SboxMcp.Server;
using Xunit;

namespace SboxMcp.Tests;

public class PathJailTests
{
	const string Root = @"C:\project";

	[Theory]
	[InlineData( "Assets/models/crate.vmdl" )]
	[InlineData( @"Code\Player.cs" )]
	[InlineData( "file.txt" )]
	[InlineData( @"C:\project\Editor\Tool.cs" )]
	public void Allows_paths_inside_root( string path )
	{
		var resolved = PathJail.Resolve( Root, path );
		Assert.StartsWith( Root, resolved, StringComparison.OrdinalIgnoreCase );
	}

	[Theory]
	[InlineData( @"..\outside.txt" )]
	[InlineData( @"..\..\Windows\System32\drivers\etc\hosts" )]
	[InlineData( @"Code\..\..\outside.txt" )]
	[InlineData( @"C:\Windows\System32\cmd.exe" )]
	[InlineData( @"D:\other\place.txt" )]
	[InlineData( @"Assets/../../escape.txt" )]
	public void Blocks_paths_outside_root( string path )
	{
		Assert.Throws<UnauthorizedAccessException>( () => PathJail.Resolve( Root, path ) );
	}

	[Fact]
	public void Blocks_empty_path()
	{
		Assert.Throws<ArgumentException>( () => PathJail.Resolve( Root, "  " ) );
	}

	[Fact]
	public void Sneaky_prefix_sibling_is_blocked()
	{
		// C:\project-evil shares the prefix "C:\project" but is outside
		Assert.Throws<UnauthorizedAccessException>( () => PathJail.Resolve( Root, @"C:\project-evil\file.txt" ) );
	}

	[Fact]
	public void Root_itself_is_allowed()
	{
		Assert.Equal( Root, PathJail.Resolve( Root, @"C:\project" ), ignoreCase: true );
	}
}