From ebecd065b6b658514f29f0cead0a25328204ce84 Mon Sep 17 00:00:00 2001 From: Vichea Date: Fri, 13 Sep 2024 15:11:17 -0500 Subject: [PATCH] Add Service Test --- src/SearchBugs.Domain/Git/GitErrors.cs | 2 + .../SearchBugs.Infrastructure.csproj | 5 + .../Services/DataEncryptionService.cs | 17 ++ .../Services/GitHttpService.cs | 50 ++--- .../Services/GitRepositoryService.cs | 15 +- .../Data/Options.cs | 28 +++ ...SearchBugs.Infrastructure.UnitTests.csproj | 22 ++- .../ServiceTest/DataEncryptionServiceTest.cs | 96 ++++++++++ .../ServiceTest/GitHttpServiceTest.cs | 176 ++++++++++++++++++ .../ServiceTest/GitRepositoryServiceTest.cs | 97 ++++++++++ 10 files changed, 476 insertions(+), 32 deletions(-) create mode 100644 test/SearchBugs.Infrastructure.UnitTests/Data/Options.cs create mode 100644 test/SearchBugs.Infrastructure.UnitTests/ServiceTest/DataEncryptionServiceTest.cs create mode 100644 test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitHttpServiceTest.cs create mode 100644 test/SearchBugs.Infrastructure.UnitTests/ServiceTest/GitRepositoryServiceTest.cs 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); + } +}