Files
ClawdDotNet/tests/ClawdDotNet.Core.Tests/Storage/PathBoundaryTests.cs
T
RichardandClaude Opus 5.5 837a634a22 Stufe 4c (#764): Kommentare in C#, WebUI-Skripten, CSS und HTML auf Englisch
Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig
erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner
Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft.
Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch.

WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b
umbenannten Parameter angeglichen.

Dabei behoben:
- 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue"
  gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day.
- Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine
  CSS-Klasse gab - jetzt "success" wie laufende Instanzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:45:03 +02:00

153 lines
6.3 KiB
C#

using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tests.Infrastructure;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Storage;
/// <summary>
/// Until the Linux port the containment check compared with
/// <c>OrdinalIgnoreCase</c> — the Windows assumption that upper and lower case
/// fold together. Under Linux <c>Workspace</c> and <c>workspace</c> are two
/// directories, and a symbolic link can point out of the root directory
/// without the plain path calculation noticing.
///
/// The file system is deliberately not abstracted here: exactly the semantics this is
/// about would be hidden by an abstraction.
/// </summary>
public sealed class PathBoundaryTests : IDisposable
{
private readonly string _base;
private readonly string _root;
public PathBoundaryTests()
{
_base = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
_root = Path.Combine(_base, "Workspace");
Directory.CreateDirectory(_root);
}
public void Dispose()
{
try { Directory.Delete(_base, recursive: true); }
catch { /* Cleaning up is secondary */ }
}
// ═══════════════════════════════════════════════════════════
// Directory boundaries (the same on both platforms)
// ═══════════════════════════════════════════════════════════
[Fact]
public void Ein_Nachbarverzeichnis_mit_gleichem_Praefix_liegt_ausserhalb()
{
PathBoundary.IsInside(Path.Combine(_root + "-Backup", "geheim.txt"), _root)
.ShouldBeFalse();
}
[Fact]
public void Das_Wurzelverzeichnis_selbst_liegt_innerhalb()
{
PathBoundary.IsInside(_root, _root).ShouldBeTrue();
PathBoundary.IsInside(_root + Path.DirectorySeparatorChar, _root).ShouldBeTrue();
}
[Fact]
public void Ein_Unterverzeichnis_liegt_innerhalb()
{
PathBoundary.IsInside(Path.Combine(_root, "a", "b", "c.txt"), _root).ShouldBeTrue();
}
[Fact]
public void Punkt_Punkt_fuehrt_hinaus()
{
PathBoundary.IsInside(Path.Combine(_root, "..", "evil.txt"), _root).ShouldBeFalse();
}
// ═══════════════════════════════════════════════════════════
// Upper/lower case — here the platforms diverge
// ═══════════════════════════════════════════════════════════
[LinuxFact]
public void Unter_Linux_ist_ein_Verzeichnis_mit_anderer_Schreibweise_ein_anderes()
{
// "workspace" next to "Workspace": two real, separate directories.
var lower = Path.Combine(_base, "workspace");
Directory.CreateDirectory(lower);
PathBoundary.IsInside(Path.Combine(lower, "geheim.txt"), _root).ShouldBeFalse();
}
[WindowsFact]
public void Unter_Windows_meint_eine_andere_Schreibweise_dasselbe_Verzeichnis()
{
var lower = Path.Combine(_base, "workspace");
PathBoundary.IsInside(Path.Combine(lower, "datei.txt"), _root).ShouldBeTrue();
}
[Fact]
public void Der_Schluessel_fuer_Sperren_folgt_der_Plattform()
{
var upper = Path.Combine(_base, "Config.json");
var lower = Path.Combine(_base, "config.json");
var gleich = PathBoundary.CanonicalKey(upper) == PathBoundary.CanonicalKey(lower);
// Under Windows the same file — the same lock. Under Linux two files
// that must not share one.
gleich.ShouldBe(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
}
// ═══════════════════════════════════════════════════════════
// Symbolic links
// ═══════════════════════════════════════════════════════════
[LinuxFact]
public void Eine_Verknuepfung_aus_dem_Wurzelverzeichnis_heraus_wird_erkannt()
{
// The case the plain path calculation does not see: "Workspace/raus" points
// outside, but "Workspace/raus/beute.txt" lies lexically inside.
var draussen = Path.Combine(_base, "Draussen");
Directory.CreateDirectory(draussen);
Directory.CreateSymbolicLink(Path.Combine(_root, "raus"), draussen);
PathBoundary.IsInside(Path.Combine(_root, "raus", "beute.txt"), _root)
.ShouldBeFalse();
}
[LinuxFact]
public void Eine_Verknuepfung_innerhalb_des_Wurzelverzeichnisses_bleibt_erlaubt()
{
// Counter-check: whoever stays inside is not locked out.
var ziel = Path.Combine(_root, "echte-daten");
Directory.CreateDirectory(ziel);
Directory.CreateSymbolicLink(Path.Combine(_root, "abkuerzung"), ziel);
PathBoundary.IsInside(Path.Combine(_root, "abkuerzung", "bericht.md"), _root)
.ShouldBeTrue();
}
[LinuxFact]
public void Ein_Wurzelverzeichnis_hinter_einer_Verknuepfung_sperrt_sich_nicht_selbst_aus()
{
// If the workspace itself lies behind a link (common with /var → /private/var
// or mounted data directories), the resolution must apply on both sides
// — otherwise nothing matches any more after resolving.
var echt = Path.Combine(_base, "EchterOrt");
Directory.CreateDirectory(echt);
var ueber = Path.Combine(_base, "Verknuepft");
Directory.CreateSymbolicLink(ueber, echt);
PathBoundary.IsInside(Path.Combine(ueber, "datei.txt"), ueber).ShouldBeTrue();
PathBoundary.IsInside(Path.Combine(echt, "datei.txt"), ueber).ShouldBeTrue();
}
[Fact]
public void Nicht_existierende_Pfade_bleiben_pruefbar()
{
// When writing, the file does not exist yet. What does not exist cannot be a
// link — the resolution must not fail on that.
PathBoundary.IsInside(Path.Combine(_root, "gibt", "es", "noch", "nicht.txt"), _root)
.ShouldBeTrue();
}
}