diff --git a/src/SearchBugs.Domain/Git/GitErrors.cs b/src/SearchBugs.Domain/Git/GitErrors.cs
index cd199ed..7c7dbab 100644
--- a/src/SearchBugs.Domain/Git/GitErrors.cs
+++ b/src/SearchBugs.Domain/Git/GitErrors.cs
@@ -11,4 +11,6 @@ public static class GitErrors
public static Error BranchNotFound = new Error("Git.BranchNotFound", "Branch not found.");
public static Error CommitNotFound = new Error("Git.CommitNotFound", "Commit not found.");
+
+ public static Error RepositoryNotFound = new Error("Git.RepositoryNotFound", "Repository not found.");
}
diff --git a/src/SearchBugs.Infrastructure/SearchBugs.Infrastructure.csproj b/src/SearchBugs.Infrastructure/SearchBugs.Infrastructure.csproj
index 707297e..06fa104 100644
--- a/src/SearchBugs.Infrastructure/SearchBugs.Infrastructure.csproj
+++ b/src/SearchBugs.Infrastructure/SearchBugs.Infrastructure.csproj
@@ -19,4 +19,9 @@
+
+
+ <_Parameter1>SearchBugs.Infrastructure.UnitTests
+
+
diff --git a/src/SearchBugs.Infrastructure/Services/DataEncryptionService.cs b/src/SearchBugs.Infrastructure/Services/DataEncryptionService.cs
index 6186ee5..8fa701f 100644
--- a/src/SearchBugs.Infrastructure/Services/DataEncryptionService.cs
+++ b/src/SearchBugs.Infrastructure/Services/DataEncryptionService.cs
@@ -8,6 +8,14 @@ internal sealed class DataEncryptionService : IDataEncryptionService
{
public string Encrypt(string plainText, string key)
{
+ if (string.IsNullOrEmpty(plainText))
+ {
+ throw new ArgumentNullException(nameof(plainText));
+ }
+ if (string.IsNullOrEmpty(key))
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key);
@@ -31,6 +39,15 @@ public string Encrypt(string plainText, string key)
public string Decrypt(string cipherText, string key)
{
+ if (string.IsNullOrEmpty(cipherText))
+ {
+ throw new ArgumentNullException(nameof(cipherText));
+ }
+
+ if (string.IsNullOrEmpty(key))
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
using (Aes aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(key);
diff --git a/src/SearchBugs.Infrastructure/Services/GitHttpService.cs b/src/SearchBugs.Infrastructure/Services/GitHttpService.cs
index ca914dc..c823490 100644
--- a/src/SearchBugs.Infrastructure/Services/GitHttpService.cs
+++ b/src/SearchBugs.Infrastructure/Services/GitHttpService.cs
@@ -22,20 +22,22 @@ public GitHttpService(IOptions gitOptions, IHttpContextAccessor http
public async Task Handle(string repositoryName, CancellationToken cancellationToken = default)
{
- var gitPath = Path.Combine(_gitOptions.BasePath, repositoryName);
-
- using var process = new Process();
- process.StartInfo = new ProcessStartInfo
+ try
{
- FileName = "git",
- Arguments = "http-backend --stateless-rpc --advertise-refs",
- RedirectStandardInput = true,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false,
- CreateNoWindow = true,
- WorkingDirectory = gitPath,
- EnvironmentVariables =
+ var gitPath = Path.Combine(_gitOptions.BasePath, repositoryName);
+
+ using var process = new Process();
+ process.StartInfo = new ProcessStartInfo
+ {
+ FileName = "git",
+ Arguments = "http-backend --stateless-rpc --advertise-refs",
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = gitPath,
+ EnvironmentVariables =
{
{ "GIT_HTTP_EXPORT_ALL", "1" },
{ "HTTP_GIT_PROTOCOL", _httpContext.Request.Headers["Git-Protocol"] },
@@ -51,17 +53,23 @@ public async Task Handle(string repositoryName, CancellationToken cancellationTo
{ "GIT_COMMITTER_NAME", _httpContext.User.Identity?.Name },
{ "GIT_COMMITTER_EMAIL", "TODO: some email" },
},
- };
- process.Start();
+ };
+ process.Start();
- var pipeWriter = PipeWriter.Create(process.StandardInput.BaseStream);
- await _httpContext.Request.BodyReader.CopyToAsync(pipeWriter, cancellationToken);
+ var pipeWriter = PipeWriter.Create(process.StandardInput.BaseStream);
+ await _httpContext.Request.BodyReader.CopyToAsync(pipeWriter, cancellationToken);
- var pipeReader = PipeReader.Create(process.StandardOutput.BaseStream);
- await ReadResponse(pipeReader, cancellationToken);
+ var pipeReader = PipeReader.Create(process.StandardOutput.BaseStream);
+ await ReadResponse(pipeReader, cancellationToken);
- await pipeReader.CopyToAsync(_httpContext.Response.BodyWriter, cancellationToken);
- await pipeReader.CompleteAsync();
+ await pipeReader.CopyToAsync(_httpContext.Response.BodyWriter, cancellationToken);
+ await pipeReader.CompleteAsync();
+ }
+ catch (Exception ex)
+ {
+ _httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
+ await _httpContext.Response.WriteAsync(ex.Message);
+ }
}
private async Task ReadResponse(PipeReader pipeReader, CancellationToken cancellationToken)
diff --git a/src/SearchBugs.Infrastructure/Services/GitRepositoryService.cs b/src/SearchBugs.Infrastructure/Services/GitRepositoryService.cs
index e4ec694..24eebf8 100644
--- a/src/SearchBugs.Infrastructure/Services/GitRepositoryService.cs
+++ b/src/SearchBugs.Infrastructure/Services/GitRepositoryService.cs
@@ -20,13 +20,13 @@ public GitRepositoryService(IOptions gitOptions)
public Result> ListTree(string commitSha, string repoPath)
{
var _repoPath = Path.Combine(_basePath, repoPath);
- using (var repo = new Repository(_basePath))
+ if (!Directory.Exists(_repoPath)) return Result.Failure>(GitErrors.RepositoryNotFound);
+ using (var repo = new Repository(_repoPath))
{
var commit = repo.Lookup(commitSha) ?? repo.Head.Tip;
+ if (commit == null) return Result.Failure>(GitErrors.InvalidCommitPath);
var tree = commit.Tree;
- if (tree == null) return Result.Failure>(GitErrors.InvalidCommitPath);
-
return tree.Select(entry => new GitTreeItem
{
Path = entry.Path,
@@ -36,14 +36,15 @@ public Result> ListTree(string commitSha, string repoPa
}
}
- public Result GetFileContent(string commitSha, string filePath)
+ public Result GetFileContent(string repoPath, string commitSha, string filePath)
{
- var _repoPath = Path.Combine(_basePath, filePath);
+ var _repoPath = Path.Combine(_basePath, repoPath);
using (var repo = new Repository(_repoPath))
{
- var commit = repo.Lookup(commitSha) ?? repo.Head.Tip;
- var blob = commit[filePath]?.Target as Blob;
+ var commit = repo.Lookup(commitSha);
+ if (commit == null) return Result.Failure(GitErrors.InvalidCommitPath);
+ var blob = commit[filePath]?.Target as Blob;
if (blob == null) return Result.Failure(GitErrors.FileNotFound);
return blob.GetContentText();
diff --git a/test/SearchBugs.Infrastructure.UnitTests/Data/Options.cs b/test/SearchBugs.Infrastructure.UnitTests/Data/Options.cs
new file mode 100644
index 0000000..82be99f
--- /dev/null
+++ b/test/SearchBugs.Infrastructure.UnitTests/Data/Options.cs
@@ -0,0 +1,28 @@
+using Microsoft.Extensions.Options;
+using SearchBugs.Infrastructure.Options;
+
+namespace SearchBugs.Infrastructure.UnitTests.Data;
+
+public class OptionsTest : IOptions, IDisposable
+{
+ public GitOptions Value => new GitOptions
+ {
+ BasePath = Path.Combine(Directory.GetCurrentDirectory(), "Repositories")
+ };
+
+ public OptionsTest()
+ {
+ if (!Directory.Exists(Value.BasePath))
+ {
+ Directory.CreateDirectory(Value.BasePath);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(Value.BasePath))
+ {
+ Directory.Delete(Value.BasePath, true);
+ }
+ }
+}
diff --git a/test/SearchBugs.Infrastructure.UnitTests/SearchBugs.Infrastructure.UnitTests.csproj b/test/SearchBugs.Infrastructure.UnitTests/SearchBugs.Infrastructure.UnitTests.csproj
index 9c5b30a..8ed0fd8 100644
--- a/test/SearchBugs.Infrastructure.UnitTests/SearchBugs.Infrastructure.UnitTests.csproj
+++ b/test/SearchBugs.Infrastructure.UnitTests/SearchBugs.Infrastructure.UnitTests.csproj
@@ -10,10 +10,24 @@
-
-
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
diff --git a/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/DataEncryptionServiceTest.cs b/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/DataEncryptionServiceTest.cs
new file mode 100644
index 0000000..e8d1095
--- /dev/null
+++ b/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/DataEncryptionServiceTest.cs
@@ -0,0 +1,96 @@
+using FluentAssertions;
+using SearchBugs.Infrastructure.Services;
+
+namespace SearchBugs.Infrastructure.UnitTests.ServiceTest;
+
+public class DataEncryptionServiceTest
+{
+ [Fact]
+ public void Encrypt_WhenCalled_ReturnEncryptedString()
+ {
+ // Arrange
+ var service = new DataEncryptionService();
+ var plainText = "Hello World";
+ var _32ByteKey = "XdhXLy^{8Pzs~O!Jm*MJLg^NA)4;(44m";
+
+ // Act
+ var encryptedText = service.Encrypt(plainText, _32ByteKey);
+
+ // Assert
+ encryptedText.Should().NotBe(plainText);
+ encryptedText.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public void Decrypt_WhenCalled_ReturnDecryptedString()
+ {
+ // Arrange
+ var service = new DataEncryptionService();
+ var plainText = "Hello World";
+ var _32ByteKey = "XdhXLy^{8Pzs~O!Jm*MJLg^NA)4;(44m";
+
+ // Act
+ var encryptedText = service.Encrypt(plainText, _32ByteKey);
+ var decryptedText = service.Decrypt(encryptedText, _32ByteKey);
+
+ // Assert
+ decryptedText.Should().Be(plainText);
+ }
+
+ [Fact]
+ public void Encrypt_WhenPlainTextIsNull_ThrowArgumentNullException()
+ {
+ // Arrange
+ var service = new DataEncryptionService();
+ var _32ByteKey = "XdhXLy^{8Pzs~O!Jm*MJLg^NA)4;(44m";
+
+ // Act
+ Action act = () => service.Encrypt(null, _32ByteKey);
+
+ // Assert
+ act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'plainText')");
+ }
+
+
+ [Fact]
+ public void Encrypt_WhenKeyIsNull_ThrowArgumentNullException()
+ {
+ // Arrange
+ var service = new DataEncryptionService();
+ var plainText = "Hello World";
+
+ // Act
+ Action act = () => service.Encrypt(plainText, null);
+
+ // Assert
+ act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'key')");
+ }
+
+ [Fact]
+ public void Decrypt_WhenCipherTextIsNull_ThrowArgumentNullException()
+ {
+ // Arrange
+ var service = new DataEncryptionService();
+ var _32ByteKey = "XdhXLy^{8Pzs~O!Jm*MJLg^NA)4;(44m";
+
+ // Act
+ Action act = () => service.Decrypt(null, _32ByteKey);
+
+ // Assert
+ act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'cipherText')");
+ }
+
+ [Fact]
+ public void Decrypt_WhenKeyIsNull_ThrowArgumentNullException()
+ {
+ // Arrange
+ var service = new DataEncryptionService();
+ var plainText = "Hello World";
+
+ // Act
+ Action act = () => service.Decrypt(plainText, null);
+
+ // Assert
+ act.Should().Throw().WithMessage("Value cannot be null. (Parameter 'key')");
+ }
+}
diff --git a/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitHttpServiceTest.cs b/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitHttpServiceTest.cs
new file mode 100644
index 0000000..bf76276
--- /dev/null
+++ b/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitHttpServiceTest.cs
@@ -0,0 +1,176 @@
+using FluentAssertions;
+using LibGit2Sharp;
+using Microsoft.AspNetCore.Http;
+using SearchBugs.Infrastructure.Options;
+using SearchBugs.Infrastructure.Services;
+using SearchBugs.Infrastructure.UnitTests.Data;
+using System.Security.Claims;
+
+namespace SearchBugs.Infrastructure.UnitTests.ServiceTest;
+
+public class GitHttpServiceTest
+{
+ private readonly GitOptions _gitOptions;
+ private readonly HttpContextAccessor _httpContextAccessor;
+
+ public GitHttpServiceTest()
+ {
+ _gitOptions = new OptionsTest().Value;
+ _httpContextAccessor = new HttpContextAccessor
+ {
+ HttpContext = new DefaultHttpContext()
+ };
+ }
+
+ private string GetOrCreateRepository(string repositoryName)
+ {
+ var repoPath = Path.Combine(_gitOptions.BasePath, repositoryName);
+ if (!Directory.Exists(repoPath))
+ {
+ Repository.Init(repoPath);
+ var repo = new Repository(repoPath);
+ var filePath = Path.Combine(repo.Info.WorkingDirectory, "test.txt");
+ var content = "Hello World";
+ File.WriteAllText(filePath, content);
+ repo.Index.Add("test.txt");
+ var signature = new Signature("Vichea Nath", "test@gmail.com", DateTimeOffset.Now);
+
+ if (repo.Head.Tip == null)
+ {
+ repo.Commit("Initial commit", signature, signature);
+ }
+ }
+
+ return repoPath;
+ }
+
+ [Fact]
+ public async Task Handle_GitClone_Success()
+ {
+ // Arrange
+ var service = new GitHttpService(new OptionsTest(), _httpContextAccessor);
+ var repositoryName = "test-repo";
+ // create Test repository
+ var repoPath = GetOrCreateRepository(repositoryName);
+
+ var request = new DefaultHttpContext().Request;
+ request.Headers["Git-Protocol"] = "http";
+ request.Method = HttpMethods.Post;
+ request.RouteValues["path"] = "git-upload-pack";
+ request.QueryString = new QueryString("?service=git-upload-pack");
+ request.ContentType = "application/x-git-upload-pack-request";
+ request.ContentLength = 0;
+ request.Headers.ContentEncoding = "gzip";
+ // mock authenticated user
+
+ request.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
+ {
+ new Claim(ClaimTypes.Name, "test-user")
+ }));
+
+ // Act
+ Func act = async () => await service.Handle(repositoryName);
+
+ // Assert
+ await act.Should().NotThrowAsync();
+ request.HttpContext.Response.StatusCode.Should().Be(StatusCodes.Status200OK);
+ }
+
+ [Fact]
+ public async Task Handle_GitClone_Fail()
+ {
+ // Arrange
+ var service = new GitHttpService(new OptionsTest(), _httpContextAccessor);
+ var repositoryName = "test-repo";
+ // create Test repository
+ var repoPath = GetOrCreateRepository(repositoryName);
+
+ var request = new DefaultHttpContext().Request;
+ request.Headers["Git-Protocol"] = "http";
+ request.Method = HttpMethods.Post;
+ request.RouteValues["path"] = "git-upload-pack";
+ request.QueryString = new QueryString("?service=git-upload-pack");
+ request.ContentType = "application/x-git-upload-pack-request";
+ request.ContentLength = 0;
+ request.Headers.ContentEncoding = "gzip";
+ // mock authenticated user
+
+ request.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
+ {
+ new Claim(ClaimTypes.Name, "test-user")
+ }));
+
+ // Act
+ Func act = async () => await service.Handle("invalid-repo");
+
+ // Assert
+ await act.Should().NotThrowAsync();
+ _httpContextAccessor.HttpContext.Response.StatusCode.Should().Be(StatusCodes.Status500InternalServerError);
+ _httpContextAccessor.HttpContext.Response.Body.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task Handle_GitPush_Success_ShouldReturn200OK()
+ {
+ // Arrange
+ var service = new GitHttpService(new OptionsTest(), _httpContextAccessor);
+ var repositoryName = "test-repo";
+ // create Test repository
+ var repoPath = GetOrCreateRepository(repositoryName);
+
+ var request = new DefaultHttpContext().Request;
+ request.Headers["Git-Protocol"] = "http";
+ request.Method = HttpMethods.Post;
+ request.RouteValues["path"] = "git-receive-pack";
+ request.QueryString = new QueryString("?service=git-receive-pack");
+ request.ContentType = "application/x-git-receive-pack-request";
+ request.ContentLength = 0;
+ request.Headers.ContentEncoding = "gzip";
+ // mock authenticated user
+
+ request.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
+ {
+ new Claim(ClaimTypes.Name, "test-user")
+ }));
+
+ // Act
+ Func act = async () => await service.Handle(repositoryName);
+
+ // Assert
+ await act.Should().NotThrowAsync();
+ request.HttpContext.Response.StatusCode.Should().Be(StatusCodes.Status200OK);
+ }
+
+ [Fact]
+ public async Task Handle_GitPush_Fail_ShouldReturn500InternalServerError()
+ {
+ // Arrange
+ var service = new GitHttpService(new OptionsTest(), _httpContextAccessor);
+ var repositoryName = "test-repo";
+ // create Test repository
+ var repoPath = GetOrCreateRepository(repositoryName);
+
+ var request = new DefaultHttpContext().Request;
+ request.Headers["Git-Protocol"] = "http";
+ request.Method = HttpMethods.Post;
+ request.RouteValues["path"] = "git-receive-pack";
+ request.QueryString = new QueryString("?service=git-receive-pack");
+ request.ContentType = "application/x-git-receive-pack-request";
+ request.ContentLength = 0;
+ request.Headers.ContentEncoding = "gzip";
+ // mock authenticated user
+
+ request.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
+ {
+ new Claim(ClaimTypes.Name, "test-user")
+ }));
+
+ // Act
+ Func act = async () => await service.Handle("invalid-repo");
+
+ // Assert
+ await act.Should().NotThrowAsync();
+ _httpContextAccessor.HttpContext.Response.StatusCode.Should().Be(StatusCodes.Status500InternalServerError);
+ _httpContextAccessor.HttpContext.Response.Body.Should().NotBeNull();
+ }
+}
diff --git a/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitRepositoryServiceTest.cs b/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitRepositoryServiceTest.cs
new file mode 100644
index 0000000..4a2ef77
--- /dev/null
+++ b/test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitRepositoryServiceTest.cs
@@ -0,0 +1,97 @@
+using FluentAssertions;
+using LibGit2Sharp;
+using SearchBugs.Domain.Git;
+using SearchBugs.Infrastructure.Options;
+using SearchBugs.Infrastructure.Services;
+using SearchBugs.Infrastructure.UnitTests.Data;
+
+namespace SearchBugs.Infrastructure.UnitTests.ServiceTest;
+
+public class GitRepositoryServiceTest
+{
+ private readonly GitOptions _gitOptions;
+
+ public GitRepositoryServiceTest()
+ {
+ _gitOptions = new OptionsTest().Value;
+ }
+
+ private string GetSetupNewRepository(string repositoryName)
+ {
+ var repoPath = Path.Combine(_gitOptions.BasePath, repositoryName);
+ if (!Directory.Exists(repoPath))
+ {
+ Repository.Init(repoPath);
+ var repo = new Repository(repoPath);
+ var filePath = Path.Combine(repo.Info.WorkingDirectory, "test.txt");
+ var content = "Hello World";
+ File.WriteAllText(filePath, content);
+ repo.Index.Add("test.txt");
+ var signature = new Signature("Vichea Nath", "test@gmail.com", DateTimeOffset.Now);
+
+ repo.Commit("Initial commit", signature, signature);
+ }
+
+ return repoPath;
+ }
+
+ [Fact]
+ public void ListTree_WhenCalled_ReturnGitTreeItems()
+ {
+ // Arrange
+ var service = new GitRepositoryService(new OptionsTest());
+ var repositoryName = "test-repo";
+ var filePath = "test.txt";
+ var repoPath = GetSetupNewRepository(repositoryName);
+ var commitSha = new Repository(repoPath).Commits.First().Sha;
+
+ // Act
+ var result = service.ListTree(commitSha, repositoryName);
+
+ // Assert
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().NotBeEmpty();
+ result.Value.Should().HaveCount(1);
+ result.Value.First().Path.Should().Be(filePath);
+ result.Value.First().Name.Should().Be("test.txt");
+ result.Value.First().Type.Should().Be("Blob");
+ }
+
+ [Fact]
+ public void ListTree_WhenCalledWithInvalidRepositoryName_ReturnError()
+ {
+ // Arrange
+ var service = new GitRepositoryService(new OptionsTest());
+
+ var repositoryName = "invalid-repo";
+ var commitSha = "invalid-commit-sha";
+ var repo = GetSetupNewRepository("test-repo");
+
+
+ // Act
+ var result = service.ListTree(commitSha, repositoryName);
+
+ // Assert
+ result.IsFailure.Should().BeTrue();
+ result.Error.Should().Be(GitErrors.RepositoryNotFound);
+ }
+
+ [Fact]
+ public void GetFileContent_WhenCalled_ReturnFileContent()
+ {
+ // Arrange
+ var service = new GitRepositoryService(new OptionsTest());
+ var repositoryName = "test-repo";
+ var repoPath = GetSetupNewRepository(repositoryName);
+ var commitSha = new Repository(repoPath).Commits.First().Sha;
+ var content = "Hello World";
+
+ // Act
+ var result = service.GetFileContent(repositoryName, commitSha, "test.txt");
+
+
+ // Assert
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be(content);
+ }
+}