GitIntegrationDocClaimsTests.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. using LibGit2Sharp;
  2. using RackPeek.Domain.Git;
  3. using RackPeek.Domain.Git.UseCases;
  4. namespace Tests.Git;
  5. /// <summary>
  6. /// Holds tests that pin doc-only claims (UI strings, security promises,
  7. /// architectural statements) which don't fit cleanly inside one component
  8. /// test class.
  9. /// </summary>
  10. public sealed class GitIntegrationDocClaimsTests : IDisposable {
  11. private readonly string _tempDir;
  12. public GitIntegrationDocClaimsTests() {
  13. _tempDir = Path.Combine(
  14. Path.GetTempPath(),
  15. "rackpeek-doc-claims-tests",
  16. Guid.NewGuid().ToString());
  17. Directory.CreateDirectory(_tempDir);
  18. }
  19. public void Dispose() {
  20. try {
  21. if (Directory.Exists(_tempDir))
  22. Directory.Delete(_tempDir, true);
  23. }
  24. catch {
  25. // ignore cleanup issues
  26. }
  27. }
  28. [Fact]
  29. public async Task Token_Never_Touches_The_Config_Directory() {
  30. // Doc: "The token is read from the environment at container start; it
  31. // is not persisted in the YAML config." Stronger guarantee: it must
  32. // never appear in ANY file under the config directory, including the
  33. // .git folder, so a leaked backup never leaks the token.
  34. var token = $"sentinel-token-{Guid.NewGuid():N}";
  35. var configPath = Path.Combine(_tempDir, "config.yaml");
  36. await File.WriteAllTextAsync(configPath, "resources: []\n");
  37. var repo = new LibGit2GitRepository(
  38. _tempDir,
  39. new TokenCredentialsProvider("octocat", token));
  40. var addRemote = new AddRemoteUseCase(repo);
  41. await addRemote.ExecuteAsync("https://example.com/user/repo.git");
  42. var commit = new CommitAllUseCase(repo);
  43. await commit.ExecuteAsync("test commit");
  44. // Recursively scan everything under the config dir for the sentinel.
  45. foreach (var file in Directory.EnumerateFiles(_tempDir, "*", SearchOption.AllDirectories)) {
  46. var bytes = await File.ReadAllBytesAsync(file);
  47. // tokens are ASCII; cheaper than decoding every binary file as text
  48. var contents = System.Text.Encoding.ASCII.GetString(bytes);
  49. Assert.DoesNotContain(token, contents);
  50. }
  51. }
  52. [Fact]
  53. public void Writability_Warning_String_In_UI_Matches_Documented_Wording() {
  54. // Doc: 'If the indicator shows "Git configured but config directory is
  55. // not writable", the container can't create the .git/ folder...'.
  56. // Lock that exact wording so the doc and UI cannot drift out of sync.
  57. // The tests run from Tests/bin/Debug/net10.0; walk up to the repo root.
  58. var razorPath = LocateRepoFile("Shared.Rcl/Layout/GitStatusIndicator.razor");
  59. var contents = File.ReadAllText(razorPath);
  60. Assert.Contains(
  61. "Git configured but config directory is not writable",
  62. contents);
  63. }
  64. [Fact]
  65. public void Integration_Uses_LibGit2_And_Not_System_Git() {
  66. // Doc callout: "You do not need to install `git` in the container.
  67. // RackPeek uses the bundled libgit2 library to talk to remotes
  68. // directly over HTTPS." Verify the production implementation is
  69. // LibGit2Sharp-backed (not a process-shelling wrapper) so the doc
  70. // statement can't be invalidated by an accidental refactor.
  71. IGitRepository repo = new LibGit2GitRepository(
  72. _tempDir,
  73. new TokenCredentialsProvider("u", "t"));
  74. // Sanity: LibGit2Sharp must be reachable at all — if it weren't, the
  75. // line above would have thrown a DllNotFoundException for the native
  76. // libgit2 binary, which proves it ships with the assembly.
  77. Assert.True(repo.IsAvailable);
  78. // Pin: the production binding is the LibGit2Sharp-backed one. If
  79. // someone swaps it for a CLI-shelling implementation, this fails and
  80. // they're forced to revisit the "no system git needed" promise.
  81. Assert.IsType<LibGit2GitRepository>(repo);
  82. // And LibGit2Sharp itself must be loaded into the test process, which
  83. // proves the assembly ships with native binaries.
  84. Assert.NotNull(typeof(Repository).Assembly.Location);
  85. }
  86. private static string LocateRepoFile(string relativeFromRepoRoot) {
  87. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  88. while (dir is not null) {
  89. var candidate = Path.Combine(dir.FullName, relativeFromRepoRoot);
  90. if (File.Exists(candidate))
  91. return candidate;
  92. dir = dir.Parent;
  93. }
  94. throw new FileNotFoundException(
  95. $"Could not locate {relativeFromRepoRoot} walking up from {AppContext.BaseDirectory}");
  96. }
  97. }