using LibGit2Sharp;
using RackPeek.Domain.Git;
using RackPeek.Domain.Git.UseCases;
namespace Tests.Git;
///
/// Holds tests that pin doc-only claims (UI strings, security promises,
/// architectural statements) which don't fit cleanly inside one component
/// test class.
///
public sealed class GitIntegrationDocClaimsTests : IDisposable {
private readonly string _tempDir;
public GitIntegrationDocClaimsTests() {
_tempDir = Path.Combine(
Path.GetTempPath(),
"rackpeek-doc-claims-tests",
Guid.NewGuid().ToString());
Directory.CreateDirectory(_tempDir);
}
public void Dispose() {
try {
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, true);
}
catch {
// ignore cleanup issues
}
}
[Fact]
public async Task Token_Never_Touches_The_Config_Directory() {
// Doc: "The token is read from the environment at container start; it
// is not persisted in the YAML config." Stronger guarantee: it must
// never appear in ANY file under the config directory, including the
// .git folder, so a leaked backup never leaks the token.
var token = $"sentinel-token-{Guid.NewGuid():N}";
var configPath = Path.Combine(_tempDir, "config.yaml");
await File.WriteAllTextAsync(configPath, "resources: []\n");
var repo = new LibGit2GitRepository(
_tempDir,
new TokenCredentialsProvider("octocat", token));
var addRemote = new AddRemoteUseCase(repo);
await addRemote.ExecuteAsync("https://example.com/user/repo.git");
var commit = new CommitAllUseCase(repo);
await commit.ExecuteAsync("test commit");
// Recursively scan everything under the config dir for the sentinel.
foreach (var file in Directory.EnumerateFiles(_tempDir, "*", SearchOption.AllDirectories)) {
var bytes = await File.ReadAllBytesAsync(file);
// tokens are ASCII; cheaper than decoding every binary file as text
var contents = System.Text.Encoding.ASCII.GetString(bytes);
Assert.DoesNotContain(token, contents);
}
}
[Fact]
public void Writability_Warning_String_In_UI_Matches_Documented_Wording() {
// Doc: 'If the indicator shows "Git configured but config directory is
// not writable", the container can't create the .git/ folder...'.
// Lock that exact wording so the doc and UI cannot drift out of sync.
// The tests run from Tests/bin/Debug/net10.0; walk up to the repo root.
var razorPath = LocateRepoFile("Shared.Rcl/Layout/GitStatusIndicator.razor");
var contents = File.ReadAllText(razorPath);
Assert.Contains(
"Git configured but config directory is not writable",
contents);
}
[Fact]
public void Integration_Uses_LibGit2_And_Not_System_Git() {
// Doc callout: "You do not need to install `git` in the container.
// RackPeek uses the bundled libgit2 library to talk to remotes
// directly over HTTPS." Verify the production implementation is
// LibGit2Sharp-backed (not a process-shelling wrapper) so the doc
// statement can't be invalidated by an accidental refactor.
IGitRepository repo = new LibGit2GitRepository(
_tempDir,
new TokenCredentialsProvider("u", "t"));
// Sanity: LibGit2Sharp must be reachable at all — if it weren't, the
// line above would have thrown a DllNotFoundException for the native
// libgit2 binary, which proves it ships with the assembly.
Assert.True(repo.IsAvailable);
// Pin: the production binding is the LibGit2Sharp-backed one. If
// someone swaps it for a CLI-shelling implementation, this fails and
// they're forced to revisit the "no system git needed" promise.
Assert.IsType(repo);
// And LibGit2Sharp itself must be loaded into the test process, which
// proves the assembly ships with native binaries.
Assert.NotNull(typeof(Repository).Assembly.Location);
}
private static string LocateRepoFile(string relativeFromRepoRoot) {
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null) {
var candidate = Path.Combine(dir.FullName, relativeFromRepoRoot);
if (File.Exists(candidate))
return candidate;
dir = dir.Parent;
}
throw new FileNotFoundException(
$"Could not locate {relativeFromRepoRoot} walking up from {AppContext.BaseDirectory}");
}
}