4
0

GitRepositoryAvailabilityTests.cs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. using LibGit2Sharp;
  2. using RackPeek.Domain.Git;
  3. using RackPeek.Domain.Git.UseCases;
  4. namespace Tests.Git;
  5. public sealed class GitRepositoryAvailabilityTests : IDisposable {
  6. private readonly string _tempDir;
  7. private readonly IGitCredentialsProvider _creds =
  8. new TokenCredentialsProvider("test-user", "test-token");
  9. public GitRepositoryAvailabilityTests() {
  10. _tempDir = Path.Combine(
  11. Path.GetTempPath(),
  12. "rackpeek-git-tests",
  13. Guid.NewGuid().ToString());
  14. Directory.CreateDirectory(_tempDir);
  15. File.WriteAllText(Path.Combine(_tempDir, "config.yaml"), "");
  16. }
  17. public void Dispose() {
  18. try {
  19. if (Directory.Exists(_tempDir))
  20. ForceDelete(_tempDir);
  21. }
  22. catch {
  23. // ignore cleanup issues
  24. }
  25. }
  26. private static void ForceDelete(string path) {
  27. foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) {
  28. File.SetAttributes(file, FileAttributes.Normal);
  29. }
  30. Directory.Delete(path, true);
  31. }
  32. [Fact]
  33. public void Fresh_Config_Directory_Becomes_Available_After_Construction() {
  34. // Reproduces the user's bug: GIT_TOKEN is set, container starts on a
  35. // fresh volume with only config.yaml. Without auto-init, IsAvailable
  36. // stays false and every git action returns "Git is not available."
  37. Assert.False(Repository.IsValid(_tempDir),
  38. "precondition: fresh dir should not be a git repo yet");
  39. var repo = new LibGit2GitRepository(_tempDir, _creds);
  40. Assert.True(repo.IsAvailable,
  41. "after construction with GIT_TOKEN configured, the repo should be initialised " +
  42. "so the user can immediately add a remote without an explicit Enable Git click");
  43. }
  44. [Fact]
  45. public async Task Add_Remote_Succeeds_On_Fresh_Config_Directory() {
  46. // End-to-end: simulates the user clicking Add Remote on a brand-new
  47. // install. Before the fix this returns "Git is not available." even
  48. // though GIT_TOKEN was set on the container.
  49. var repo = new LibGit2GitRepository(_tempDir, _creds);
  50. var useCase = new AddRemoteUseCase(repo);
  51. var error = await useCase.ExecuteAsync("https://example.com/user/repo.git");
  52. // The AddRemote fetch will fail (no real remote) but the failure
  53. // should be a network error, not "Git is not available."
  54. Assert.DoesNotContain("Git is not available", error ?? string.Empty);
  55. }
  56. [Fact]
  57. public void Existing_Repo_Is_Not_Reinitialised() {
  58. // Auto-init must be idempotent: pre-existing repos must keep their
  59. // history. Initialising twice would discard the user's commits.
  60. Repository.Init(_tempDir);
  61. using (var seed = new Repository(_tempDir)) {
  62. File.WriteAllText(Path.Combine(_tempDir, "seed.txt"), "seed");
  63. Commands.Stage(seed, "seed.txt");
  64. var sig = new Signature("seed", "seed@test", DateTimeOffset.UtcNow);
  65. seed.Commit("seed commit", sig, sig);
  66. }
  67. var repo = new LibGit2GitRepository(_tempDir, _creds);
  68. Assert.True(repo.IsAvailable);
  69. using var verify = new Repository(_tempDir);
  70. Assert.NotNull(verify.Head.Tip);
  71. Assert.Equal("seed commit", verify.Head.Tip.MessageShort);
  72. }
  73. [Fact]
  74. public void Missing_Directory_Stays_Unavailable() {
  75. // Edge case: directory doesn't exist (e.g. misconfigured volume).
  76. // We must not throw out of the constructor and we must not pretend
  77. // git is available.
  78. var missing = Path.Combine(_tempDir, "definitely-not-there");
  79. var repo = new LibGit2GitRepository(missing, _creds);
  80. Assert.False(repo.IsAvailable);
  81. }
  82. [Fact]
  83. public void Readonly_Config_Directory_Does_Not_Throw_From_Constructor() {
  84. // Doc promises the UI will surface "Git configured but config directory
  85. // is not writable" when init can't run — that requires the constructor
  86. // to swallow the init failure and set IsAvailable=false rather than
  87. // crashing the Blazor render that resolves the singleton.
  88. if (OperatingSystem.IsWindows())
  89. return; // chmod-style read-only doesn't translate cleanly
  90. var readOnly = Path.Combine(_tempDir, "readonly");
  91. Directory.CreateDirectory(readOnly);
  92. File.WriteAllText(Path.Combine(readOnly, "config.yaml"), "");
  93. // Strip write permission for owner + group + other (0555).
  94. File.SetUnixFileMode(readOnly,
  95. UnixFileMode.UserRead | UnixFileMode.UserExecute |
  96. UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
  97. UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
  98. try {
  99. var repo = new LibGit2GitRepository(readOnly, _creds);
  100. Assert.False(repo.IsAvailable,
  101. "constructor must report unavailable, not throw, so the UI can " +
  102. "show the documented writability warning");
  103. }
  104. finally {
  105. // Restore write perm so cleanup works.
  106. File.SetUnixFileMode(readOnly,
  107. UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
  108. }
  109. }
  110. [Fact]
  111. public async Task Add_Remote_Persists_The_Configured_Origin() {
  112. // The full happy path that was broken before the fix: construct a repo
  113. // against a fresh dir, add a remote, confirm it lives on disk so a
  114. // subsequent push/pull would find it.
  115. var repo = new LibGit2GitRepository(_tempDir, _creds);
  116. var useCase = new AddRemoteUseCase(repo);
  117. var url = "https://example.com/user/repo.git";
  118. await useCase.ExecuteAsync(url);
  119. using var verify = new Repository(_tempDir);
  120. Remote? origin = verify.Network.Remotes["origin"];
  121. Assert.NotNull(origin);
  122. Assert.Equal(url, origin!.Url);
  123. }
  124. [Fact]
  125. public void Existing_Repo_With_Remote_Is_Detected_As_Available() {
  126. // After a container restart we should pick up the previously
  127. // initialised repo (including its remote) without re-initialising
  128. // and without losing state.
  129. Repository.Init(_tempDir);
  130. using (var seed = new Repository(_tempDir)) {
  131. seed.Network.Remotes.Add("origin", "https://example.com/user/repo.git");
  132. }
  133. var repo = new LibGit2GitRepository(_tempDir, _creds);
  134. Assert.True(repo.IsAvailable);
  135. Assert.True(repo.HasRemote());
  136. }
  137. }