Skip to content
Merged
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
Expand Up @@ -49,7 +49,7 @@ public async Task FindEntriesAsync_And_ListEntriesAsync_Should_Report_The_Worksp
await Assert.That(listed.Status).IsEqualTo(200);
await Assert.That(listed.IsError).IsFalse();
await Assert.That(listed.Location.Directory).IsEqualTo(found.Location.Directory);
await Assert.That(workspace.HasTextFile(listed.Location.Directory, RootFile, owner)).IsTrue();
await Assert.That(workspace.ReadTextFile(listed.Location.Directory, RootFile)).IsEqualTo(owner);
var rootFile = listed.Entries.Single(item => NormalizeSeparators(item.Path) == RootFile);
await Assert.That(rootFile.Type).IsEqualTo(FileSystemEntryType.File);
var childDirectory = listed.Entries.Single(
Expand Down Expand Up @@ -140,7 +140,7 @@ public async Task WriteFileAsync_Should_Write_Bytes_That_ReadFileAsync_Reads_Bac
var read = await client.FileSystem.ReadFileAsync(new FsReadRequest { Path = WriteTarget }, cancellationToken: cancellationToken);

await Assert.That(read.Content.ToArray()).IsEquivalentTo(bytes, CollectionOrdering.Matching);
await Assert.That(workspace.HasBytes(WriteTarget, bytes)).IsTrue();
await Assert.That(workspace.ReadBytes(WriteTarget)).IsEquivalentTo(bytes, CollectionOrdering.Matching);

Console.WriteLine(
"filesystem-live: write-status=" + Number(written.Status) +
Expand Down
17 changes: 17 additions & 0 deletions tests/OpenCode.Sdk.Tests/Harness/TestWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ public bool HasTextFile(string directory, string relativePath, string expectedCo
}
}

/// <summary>Reads the bytes of a file below the workspace, refusing paths that escape it.</summary>
public byte[] ReadBytes(string relativePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(relativePath);

return _fileSystem.File.ReadAllBytes(ResolvePathBelow(Path, relativePath));
}

/// <summary>Reads the contents below a reported directory, refusing paths that escape it.</summary>
public string ReadTextFile(string directory, string relativePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(directory);
ArgumentException.ThrowIfNullOrWhiteSpace(relativePath);

return _fileSystem.File.ReadAllText(ResolvePathBelow(directory, relativePath));
}

private string ResolvePathBelow(string root, string relativePath)
{
if (_fileSystem.Path.IsPathRooted(relativePath))
Expand Down
84 changes: 84 additions & 0 deletions tests/OpenCode.Sdk.Tests/Harness/TestWorkspaceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Globalization;
using Testably.Abstractions.Testing;
using Testably.Abstractions.Testing.Initializer;
using TUnit.Assertions.Enums;

namespace OpenCode.Sdk.Tests.Harness;

Expand Down Expand Up @@ -71,4 +74,85 @@ public async Task HasTextFile_Should_Distinguish_The_Owned_Marker_From_Wrong_Or_
await Assert.That(workspace.HasTextFile(workspace.Path, marker, "another-owner")).IsFalse();
await Assert.That(workspace.HasTextFile(workspace.Path, "missing-marker.txt", owner)).IsFalse();
}

[Test]
public async Task ReadBytes_Should_Return_The_Actual_Nested_File_Contents()
{
var fileSystem = new MockFileSystem();
using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath());
_ = workspace.WriteTextFile("nested/bytes.txt", "cab");

await Assert.That(workspace.ReadBytes("nested/bytes.txt"))
.IsEquivalentTo(new byte[] { 99, 97, 98 }, CollectionOrdering.Matching);
}

[Test]
public async Task ReadTextFile_Should_Return_The_Actual_Content_From_A_Reported_Directory()
{
var fileSystem = new MockFileSystem();
using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath());
var reported = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), "reported-project");
_ = fileSystem.Initialize().With(new FileDescription(
fileSystem.Path.Combine(reported, "nested", "owner.txt"), "another-owner"));

await Assert.That(workspace.ReadTextFile(reported, "nested/owner.txt")).IsEqualTo("another-owner");
}

[Test]
public async Task Reads_Should_Return_The_Wrong_Content_Rather_Than_An_Ownership_Verdict()
{
var fileSystem = new MockFileSystem();
using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath());
_ = workspace.WriteTextFile("marker.txt", "owned");
_ = workspace.WriteTextFile("marker.txt", "wrong");

await Assert.That(workspace.ReadTextFile(workspace.Path, "marker.txt")).IsEqualTo("wrong");
await Assert.That(workspace.ReadBytes("marker.txt"))
.IsEquivalentTo(new byte[] { 119, 114, 111, 110, 103 }, CollectionOrdering.Matching);
}

[Test]
public async Task Reads_Should_Propagate_A_Missing_File()
{
var fileSystem = new MockFileSystem();
using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath());

_ = await Assert.That(() => workspace.ReadBytes("missing.txt")).ThrowsExactly<FileNotFoundException>();
_ = await Assert.That(() => workspace.ReadTextFile(workspace.Path, "missing.txt")).ThrowsExactly<FileNotFoundException>();
}

[Test]
[Arguments("")]
[Arguments(" ")]
public async Task ReadTextFile_Should_Reject_A_Blank_Directory(string directory)
{
var fileSystem = new MockFileSystem();
using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath());

var exception = await Assert.That(() => workspace.ReadTextFile(directory, "marker.txt")).ThrowsExactly<ArgumentException>();

await Assert.That(exception!.ParamName).IsEqualTo("directory");
}

[Test]
[Arguments("")]
[Arguments(" ")]
[Arguments("/rooted-outside.txt")]
[Arguments("../traversal-outside.txt")]
[Arguments("../{0}-sibling/outside.txt")]
public async Task Reads_Should_Reject_A_Blank_Or_Escaping_Path(string pathTemplate)
{
var fileSystem = new MockFileSystem();
using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath());
var relativePath = string.Format(CultureInfo.InvariantCulture, pathTemplate, fileSystem.Path.GetFileName(workspace.Path));
_ = fileSystem.Initialize().With(
new FileDescription(fileSystem.Path.GetFullPath("/rooted-outside.txt"), "rooted content"),
new FileDescription(fileSystem.Path.GetFullPath(fileSystem.Path.Combine(workspace.Path, "../traversal-outside.txt")), "traversal content"),
new FileDescription(workspace.Path + "-sibling/outside.txt", "sibling content"));

var bytes = await Assert.That(() => workspace.ReadBytes(relativePath)).ThrowsExactly<ArgumentException>();
await Assert.That(bytes!.ParamName).IsEqualTo("relativePath");
var text = await Assert.That(() => workspace.ReadTextFile(workspace.Path, relativePath)).ThrowsExactly<ArgumentException>();
await Assert.That(text!.ParamName).IsEqualTo("relativePath");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ public async Task GetLocationAsync_And_ListProjectsAsync_Should_Report_The_Works
await Assert.That(current.IsError).IsFalse();
await Assert.That(string.IsNullOrWhiteSpace(current.ResolvedLocation.Project.Id)).IsFalse();
await Assert.That(current.ResolvedLocation.Project.Canonical).IsEqualTo(current.ResolvedLocation.Project.Directory);
await Assert.That(workspace.HasTextFile(current.ResolvedLocation.Project.Directory, OwnerFile, owner)).IsTrue();
await Assert.That(workspace.ReadTextFile(current.ResolvedLocation.Project.Directory, OwnerFile)).IsEqualTo(owner);

var listed = await client.Projects.ListProjectsAsync(cancellationToken: cancellationToken);

await Assert.That(listed.Status).IsEqualTo(200);
await Assert.That(listed.IsError).IsFalse();
var project = listed.Projects.Single(item => item.Id == current.ResolvedLocation.Project.Id);
await Assert.That(project.Canonical).IsEqualTo(current.ResolvedLocation.Project.Canonical);
await Assert.That(workspace.HasTextFile(project.Canonical, OwnerFile, owner)).IsTrue();
await Assert.That(workspace.ReadTextFile(project.Canonical, OwnerFile)).IsEqualTo(owner);

Console.WriteLine(
"projects-live: current=" + Number(current.Status) +
Expand Down
Loading