From 0db606e45939bcc58d10aac02533e978190ae11b Mon Sep 17 00:00:00 2001 From: Hans Christian Winther-Sorensen Date: Sat, 21 Sep 2024 00:40:27 +0200 Subject: [PATCH] TEST-0006 Add more backend tests (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 💬 ## Motivation and Context 🥅 ## How has this been tested? 🧪 - [x] Local build ⚒️ - [x] Local tests 🧪 - [ ] (optional) Local run and endpoint tested in swagger 🚀 ## Screenshots (if appropriate) 💻 ## Types of changes 🌊 - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) ## Checklist ☑️ - [x] The pull request title starts with the jira case number (when applicable), e.g. "TEST-1234 Add some feature" - [x] The person responsible for following up on requested review changes has been assigned to the pull request - [x] My code follows the code style of this project. - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. ## Highly optional checks, only use these if you have a reason to do so ✔️ - [ ] This PR changes the database so I have added the *create-diagram* label to assist reviewers with a db diagram - [ ] This PR changes platform or backend and I need others to be able to test against these changes before merging to dev, so I have added the *deploy-azure* label to deploy before merging the PR ## Checklist for the approver ✅ - [ ] I've checked the files view for spelling issues, code quality warnings and similar - [ ] I've waited until all checks have passed (green check/without error) - [ ] I've checked that only the intended files are changed --- src/backend/.runsettings | 13 + src/backend/Backend.sln | 1 + src/backend/Backend.sln.DotSettings | 2 + .../WebApi/Controllers/BloggingController.cs | 4 +- .../Controllers/SendMessageController.cs | 9 +- src/backend/WebApi/Entities/BlogDto.cs | 2 +- src/backend/WebApi/Entities/PostDto.cs | 3 + .../WebApi/Filters/ValidateModelAttribute.cs | 3 +- src/backend/WebApi/Messaging/MessageSender.cs | 25 +- .../WebApi/Messaging/RabbitMqHelper.cs | 2 +- src/backend/WebApi/Program.cs | 23 +- .../WebApi/Repository/BloggingRepository.cs | 21 +- src/backend/WebApi/WebApi.csproj | 4 +- src/backend/WebApi/swagger.json | 2 +- .../DefineConstantsAttributeTests.cs | 39 +++ .../Controllers/BloggingControllerTests.cs | 194 +++++++++++++-- .../Controllers/SendMessageControllerTests.cs | 39 +++ .../Controllers/ServiceControllerTests.cs | 85 +++++++ .../Database/BloggingContextTests.cs | 229 ++++++++++++++++++ .../Database/MsSqlContainerTest.cs | 14 ++ .../Database/MsSqlDefaultConfiguration.cs | 10 + .../Entities/BaseDtoValidationTests.cs | 21 ++ .../WebApi.Tests/Entities/BlogDtoTests.cs | 39 +++ .../WebApi.Tests/Entities/PostDtoTests.cs | 47 ++++ .../Entities/VersionInformationTests.cs | 89 +++++++ tests/backend/WebApi.Tests/GlobalUsings.cs | 1 + .../WebApi.Tests/Messaging/RabbitMqTests.cs | 81 +++++++ tests/backend/WebApi.Tests/MockHelper.cs | 17 ++ tests/backend/WebApi.Tests/MockObjects.cs | 35 +++ .../backend/WebApi.Tests/WebApi.Tests.csproj | 46 ++-- tests/backend/WebApi.Tests/packages.lock.json | 125 +++++++--- 31 files changed, 1118 insertions(+), 107 deletions(-) create mode 100644 src/backend/.runsettings create mode 100644 tests/backend/WebApi.Tests/Attributes/DefineConstantsAttributeTests.cs create mode 100644 tests/backend/WebApi.Tests/Controllers/SendMessageControllerTests.cs create mode 100644 tests/backend/WebApi.Tests/Controllers/ServiceControllerTests.cs create mode 100644 tests/backend/WebApi.Tests/Database/BloggingContextTests.cs create mode 100644 tests/backend/WebApi.Tests/Database/MsSqlContainerTest.cs create mode 100644 tests/backend/WebApi.Tests/Database/MsSqlDefaultConfiguration.cs create mode 100644 tests/backend/WebApi.Tests/Entities/BaseDtoValidationTests.cs create mode 100644 tests/backend/WebApi.Tests/Entities/BlogDtoTests.cs create mode 100644 tests/backend/WebApi.Tests/Entities/PostDtoTests.cs create mode 100644 tests/backend/WebApi.Tests/Entities/VersionInformationTests.cs create mode 100644 tests/backend/WebApi.Tests/GlobalUsings.cs create mode 100644 tests/backend/WebApi.Tests/Messaging/RabbitMqTests.cs create mode 100644 tests/backend/WebApi.Tests/MockHelper.cs create mode 100644 tests/backend/WebApi.Tests/MockObjects.cs diff --git a/src/backend/.runsettings b/src/backend/.runsettings new file mode 100644 index 00000000..c4c4f365 --- /dev/null +++ b/src/backend/.runsettings @@ -0,0 +1,13 @@ + + + + + + + cobertura + "**/*Migrations/*.cs" + + + + + \ No newline at end of file diff --git a/src/backend/Backend.sln b/src/backend/Backend.sln index 9f7e29a9..180125b4 100644 --- a/src/backend/Backend.sln +++ b/src/backend/Backend.sln @@ -10,6 +10,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9EBA80A2-4F3D-4EBC-AC53-3D4DE1719443}" ProjectSection(SolutionItems) = preProject ..\..\.gitignore = ..\..\.gitignore + .runsettings = .runsettings ..\..\README.md = ..\..\README.md EndProjectSection EndProject diff --git a/src/backend/Backend.sln.DotSettings b/src/backend/Backend.sln.DotSettings index 5b023302..71fae751 100644 --- a/src/backend/Backend.sln.DotSettings +++ b/src/backend/Backend.sln.DotSettings @@ -2189,6 +2189,7 @@ True True True + True True True True @@ -2240,6 +2241,7 @@ True True True + True True True True diff --git a/src/backend/WebApi/Controllers/BloggingController.cs b/src/backend/WebApi/Controllers/BloggingController.cs index 2754a15e..3434d072 100644 --- a/src/backend/WebApi/Controllers/BloggingController.cs +++ b/src/backend/WebApi/Controllers/BloggingController.cs @@ -50,7 +50,7 @@ public async Task> GetBlog(int id, CancellationToken cance public async Task> PostBlog(BlogDto blog, CancellationToken cancellationToken) { logger.LogInformation("PostBlog was called"); - var blogEntry = await bloggingRepository.AddBlogAsync(blog, cancellationToken); + var blogEntry = await bloggingRepository.AddOrUpdateBlogAsync(blog, cancellationToken); if (blogEntry == null) return NotFound(); @@ -97,7 +97,7 @@ public async Task> GetPost(int id, CancellationToken cance public async Task> PostPost(PostDto post, CancellationToken cancellationToken) { logger.LogInformation("PostPost was called"); - var postEntry = await bloggingRepository.AddPostAsync(post, cancellationToken); + var postEntry = await bloggingRepository.AddOrUpdatePostAsync(post, cancellationToken); if (postEntry == null) return NotFound(); diff --git a/src/backend/WebApi/Controllers/SendMessageController.cs b/src/backend/WebApi/Controllers/SendMessageController.cs index 6da06670..8de718e1 100644 --- a/src/backend/WebApi/Controllers/SendMessageController.cs +++ b/src/backend/WebApi/Controllers/SendMessageController.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using Microsoft.AspNetCore.Mvc; +using WebApi.Entities; using WebApi.Messaging; namespace WebApi.Controllers; @@ -15,12 +16,16 @@ namespace WebApi.Controllers; /// The service used for sending messages. [ApiController] [Route("[controller]")] -public class SendMessageController(MessageSender messageSender) : ControllerBase +public class SendMessageController(IMessageSender messageSender) : ControllerBase { /// /// Sends a message using the MessageSender service. /// /// A string indicating the result of the message sending operation. [HttpGet] - public string Get() => messageSender.SendMessage(); + public Task>> Get() => + Task.FromResult>>(Ok(new GenericValue + { + Value = messageSender.SendMessage() + })); } \ No newline at end of file diff --git a/src/backend/WebApi/Entities/BlogDto.cs b/src/backend/WebApi/Entities/BlogDto.cs index 19526ef7..1fab98d0 100644 --- a/src/backend/WebApi/Entities/BlogDto.cs +++ b/src/backend/WebApi/Entities/BlogDto.cs @@ -23,7 +23,7 @@ public IEnumerable Validate(ValidationContext validationContex if (string.IsNullOrEmpty(Title)) yield return new ValidationResult($"{nameof(Title)} must be set"); - if (Title.Length > 1000) + if (Title.Length > 500) yield return new ValidationResult($"{nameof(Title)} is longer than the maximum amount of characters (500)"); if (string.IsNullOrEmpty(Url)) diff --git a/src/backend/WebApi/Entities/PostDto.cs b/src/backend/WebApi/Entities/PostDto.cs index 6377c09e..e079e75a 100644 --- a/src/backend/WebApi/Entities/PostDto.cs +++ b/src/backend/WebApi/Entities/PostDto.cs @@ -34,6 +34,9 @@ public IEnumerable Validate(ValidationContext validationContex if (Content.Length > 8000) yield return new ValidationResult($"{nameof(Content)} is longer than the maximum amount of characters (8000)"); + + if (BlogId == default) + yield return new ValidationResult($"{nameof(BlogId)} must be set"); } /// diff --git a/src/backend/WebApi/Filters/ValidateModelAttribute.cs b/src/backend/WebApi/Filters/ValidateModelAttribute.cs index 3acfc6f4..f9396fcb 100644 --- a/src/backend/WebApi/Filters/ValidateModelAttribute.cs +++ b/src/backend/WebApi/Filters/ValidateModelAttribute.cs @@ -4,7 +4,8 @@ namespace WebApi.Filters; /// -/// TODO: document +/// An action filter attribute that validates the model state before the action method is executed. +/// If the model state is invalid, it returns a BadRequestObjectResult with the model state errors. /// public class ValidateModelAttribute : ActionFilterAttribute { diff --git a/src/backend/WebApi/Messaging/MessageSender.cs b/src/backend/WebApi/Messaging/MessageSender.cs index 619f2326..03ce49d3 100644 --- a/src/backend/WebApi/Messaging/MessageSender.cs +++ b/src/backend/WebApi/Messaging/MessageSender.cs @@ -14,7 +14,7 @@ namespace WebApi.Messaging; /// context. /// Implements IDisposable to ensure that resources are released properly when the object is no longer needed. /// -public sealed class MessageSender : IDisposable +public sealed class MessageSender : IMessageSender, IDisposable { private static readonly ActivitySource ActivitySource = new(nameof(MessageSender)); private static readonly TextMapPropagator Propagator = Propagators.DefaultTextMapPropagator; @@ -44,10 +44,8 @@ public void Dispose() _connection.Dispose(); } - /// - /// Sends a message to a RabbitMQ queue, including propagating the OpenTelemetry trace context. - /// - /// A string representing the message that was sent. + /// + /// Including propagating the OpenTelemetry trace context. public string SendMessage() { try @@ -89,9 +87,9 @@ public string SendMessage() return body; } - catch (Exception ex) + catch (Exception exception) { - _logger.LogError(ex, "Message publishing failed."); + _logger.LogError(exception, "Message publishing failed."); throw; } } @@ -116,4 +114,17 @@ private void InjectTraceContextIntoBasicProperties(IBasicProperties props, strin _logger.LogError(ex, "Failed to inject trace context."); } } +} + +/// +/// Defines functionality to send messages to a RabbitMQ queue +/// See also: MessageSender +/// +public interface IMessageSender +{ + /// + /// Sends a message to a RabbitMQ queue + /// + /// A string representing the message that was sent. + public string SendMessage(); } \ No newline at end of file diff --git a/src/backend/WebApi/Messaging/RabbitMqHelper.cs b/src/backend/WebApi/Messaging/RabbitMqHelper.cs index f3056b99..221d30f1 100644 --- a/src/backend/WebApi/Messaging/RabbitMqHelper.cs +++ b/src/backend/WebApi/Messaging/RabbitMqHelper.cs @@ -32,9 +32,9 @@ public static class RabbitMqHelper private static readonly ConnectionFactory ConnectionFactory = new() { HostName = Environment.GetEnvironmentVariable("RABBITMQ_HOSTNAME") ?? "localhost", + Port = int.TryParse(Environment.GetEnvironmentVariable("RABBITMQ_PORT"), out var port) ? port : 5672, UserName = Environment.GetEnvironmentVariable("RABBITMQ_DEFAULT_USER") ?? "guest", Password = Environment.GetEnvironmentVariable("RABBITMQ_DEFAULT_PASS") ?? "guest", - Port = 5672, RequestedConnectionTimeout = TimeSpan.FromMilliseconds(3000) }; diff --git a/src/backend/WebApi/Program.cs b/src/backend/WebApi/Program.cs index 0aab8126..8bc6c8ea 100644 --- a/src/backend/WebApi/Program.cs +++ b/src/backend/WebApi/Program.cs @@ -32,7 +32,7 @@ const string serviceName = "Test.WebApi"; -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Logging.AddOpenTelemetry(static options => { @@ -104,16 +104,17 @@ var app = builder.Build(); -try -{ - using var serviceScope = app.Services.CreateScope(); - var bloggingContext = serviceScope.ServiceProvider.GetRequiredService(); - bloggingContext.Database.Migrate(); -} -catch (Exception exception) -{ - Console.WriteLine($"Migration exception: {exception.Message}"); -} +if (!app.Environment.IsEnvironment("Swagger") && !EF.IsDesignTime) + try + { + using var serviceScope = app.Services.CreateScope(); + var bloggingContext = serviceScope.ServiceProvider.GetRequiredService(); + bloggingContext.Database.Migrate(); + } + catch (Exception exception) + { + Console.WriteLine($"Migration exception: {exception.Message}"); + } if (app.Environment.IsDevelopment()) { diff --git a/src/backend/WebApi/Repository/BloggingRepository.cs b/src/backend/WebApi/Repository/BloggingRepository.cs index b1b941e3..006eed00 100644 --- a/src/backend/WebApi/Repository/BloggingRepository.cs +++ b/src/backend/WebApi/Repository/BloggingRepository.cs @@ -37,7 +37,7 @@ public interface IBloggingRepository /// The blog to add. /// A token to cancel the asynchronous operation. /// A task that represents the asynchronous operation. The task result contains the added . - Task AddBlogAsync(BlogDto blog, CancellationToken cancellationToken); + Task AddOrUpdateBlogAsync(BlogDto blog, CancellationToken cancellationToken); /// /// Lists all posts asynchronously. @@ -67,7 +67,7 @@ public interface IBloggingRepository /// The post to add. /// A token to cancel the asynchronous operation. /// A task that represents the asynchronous operation. The task result contains the added . - Task AddPostAsync(PostDto post, CancellationToken cancellationToken); + Task AddOrUpdatePostAsync(PostDto post, CancellationToken cancellationToken); } /// @@ -91,7 +91,7 @@ public class BloggingRepository(BloggingContext bloggingContext) : IBloggingRepo } /// - public async Task AddBlogAsync(BlogDto blog, CancellationToken cancellationToken) + public async Task AddOrUpdateBlogAsync(BlogDto blog, CancellationToken cancellationToken) { if (blog.BlogId != 0) { @@ -104,6 +104,7 @@ public class BloggingRepository(BloggingContext bloggingContext) : IBloggingRepo return null; // Update + blogEntity.Title = blog.Title; blogEntity.Url = blog.Url; await bloggingContext.SaveChangesAsync(cancellationToken); @@ -122,14 +123,10 @@ public class BloggingRepository(BloggingContext bloggingContext) : IBloggingRepo } /// - public async Task> ListPostsAsync(int blogId, CancellationToken cancellationToken) - { - var posts = await bloggingContext.Posts - .Where(p => p.BlogId == blogId) - .ToListAsync(cancellationToken); - - return posts.Select(PostDto.FromEntity); - } + public async Task> ListPostsAsync(int blogId, CancellationToken cancellationToken) => + PostDto.FromEntity(await bloggingContext.Posts + .Where(p => p.BlogId == blogId) + .ToListAsync(cancellationToken)); /// public async Task GetPostAsync(int id, CancellationToken cancellationToken) @@ -143,7 +140,7 @@ public async Task> ListPostsAsync(int blogId, CancellationT } /// - public async Task AddPostAsync(PostDto post, CancellationToken cancellationToken) + public async Task AddOrUpdatePostAsync(PostDto post, CancellationToken cancellationToken) { if (post.PostId != 0) { diff --git a/src/backend/WebApi/WebApi.csproj b/src/backend/WebApi/WebApi.csproj index 5caeb57f..ddc08d56 100644 --- a/src/backend/WebApi/WebApi.csproj +++ b/src/backend/WebApi/WebApi.csproj @@ -37,14 +37,14 @@ - + - + diff --git a/src/backend/WebApi/swagger.json b/src/backend/WebApi/swagger.json index d02c8ec8..e8b72b88 100644 --- a/src/backend/WebApi/swagger.json +++ b/src/backend/WebApi/swagger.json @@ -254,7 +254,7 @@ "content": { "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/StringGenericValue" } } } diff --git a/tests/backend/WebApi.Tests/Attributes/DefineConstantsAttributeTests.cs b/tests/backend/WebApi.Tests/Attributes/DefineConstantsAttributeTests.cs new file mode 100644 index 00000000..9e63b5db --- /dev/null +++ b/tests/backend/WebApi.Tests/Attributes/DefineConstantsAttributeTests.cs @@ -0,0 +1,39 @@ +using WebApi.Attributes; + +namespace WebApi.Tests.Attributes; + +[TestFixture] +public class DefineConstantsAttributeTests +{ + [Test] + public void AttributeInstantiation_WithConstantsString_IsNotNull() + { + // Arrange + const string constantsString = "\"CONST1;CONST2;CONST3\""; + + // Act + var attribute = new DefineConstantsAttribute(constantsString); + + // Assert + Assert.That(attribute, Is.Not.Null); + } + + [Test] + public void ConstantsProperty_WithValidConstantsString_ParsesCorrectly() + { + // Arrange + const string constantsString = "\"CONST1;CONST2;CONST3\""; + var expectedConstants = new[] + { + "CONST1", "CONST2", "CONST3" + }; + + var attribute = new DefineConstantsAttribute(constantsString); + + // Act + var actualConstants = attribute.Constants; + + // Assert + Assert.That(expectedConstants, Is.EqualTo(actualConstants)); + } +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs b/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs index 1e366709..32f8fd24 100644 --- a/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs +++ b/tests/backend/WebApi.Tests/Controllers/BloggingControllerTests.cs @@ -27,16 +27,8 @@ public async Task GetBlogs_ReturnsListOfBlogs() // Arrange var blogs = new List { - new() - { - Url = "url1", - Title = "title1" - }, - new() - { - Url = "url2", - Title = "title2" - } + MockBlog, + MockBlogFactory(2) }; _bloggingRepositoryMock.Setup(static repo => repo.ListBlogsAsync(It.IsAny())) @@ -58,30 +50,192 @@ public async Task GetBlogs_ReturnsListOfBlogs() public async Task GetBlog_ValidId_ReturnsBlog() { // Arrange - var blog = new BlogDto + _bloggingRepositoryMock.Setup(static repo => repo.GetBlogAsync(1, It.IsAny())) + .ReturnsAsync(MockBlog); + + // Act + var result = await _controller.GetBlog(1, CancellationToken.None); + + // Assert + Assert.That(result, Is.InstanceOf>()); + var returnedBlog = result.Value; + Assert.That(returnedBlog, Is.Not.Null); + Assert.Multiple(() => { - BlogId = 1, - Title = "Test Blog", - Url = "test://url" - }; + Assert.That(returnedBlog.BlogId, Is.EqualTo(MockBlog.BlogId)); + Assert.That(returnedBlog.Title, Is.EqualTo(MockBlog.Title)); + Assert.That(returnedBlog.Url, Is.EqualTo(MockBlog.Url)); + }); + } + [Test] + public async Task GetBlog_InvalidId_ReturnsNotFound() + { + // Arrange _bloggingRepositoryMock.Setup(static repo => repo.GetBlogAsync(1, It.IsAny())) - .ReturnsAsync(blog); + .ReturnsAsync((BlogDto?) null); // Act var result = await _controller.GetBlog(1, CancellationToken.None); + // Assert + Assert.That(result.Result, Is.InstanceOf()); + } + + [Test] + public async Task PostBlog_NoId_StoresBlog() + { + // Arrange + var blog = MockBlog with + { + BlogId = 0 + }; + + var blogId = 1; + + _bloggingRepositoryMock.Setup(static repo => repo.AddOrUpdateBlogAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(MockBlog with + { + BlogId = blogId + }); + + // Act + var result = await _controller.PostBlog(blog, + CancellationToken.None); + // Assert Assert.That(result, Is.InstanceOf>()); var returnedBlog = result.Value; Assert.That(returnedBlog, Is.Not.Null); Assert.Multiple(() => { - Assert.That(returnedBlog.BlogId, Is.EqualTo(blog.BlogId)); - Assert.That(returnedBlog.Title, Is.EqualTo(blog.Title)); - Assert.That(returnedBlog.Url, Is.EqualTo(blog.Url)); + Assert.That(returnedBlog.BlogId, Is.EqualTo(blogId)); + Assert.That(returnedBlog.Title, Is.EqualTo(MockBlog.Title)); + Assert.That(returnedBlog.Url, Is.EqualTo(MockBlog.Url)); }); } - // TODO: Implement tests for the remaining actions using a similar pattern. + [Test] + public async Task PostBlog_InvalidId_ReturnsNotFound() + { + // Arrange + _bloggingRepositoryMock.Setup(static repo => repo.AddOrUpdateBlogAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((BlogDto?) null); + + // Act + var result = await _controller.PostBlog(MockBlog, CancellationToken.None); + + // Assert + Assert.That(result.Result, Is.InstanceOf()); + } + + [Test] + public async Task GetPosts_ReturnsListOfPosts() + { + // Arrange + var posts = new List + { + MockPost, + MockPostFactory(2) + }; + + _bloggingRepositoryMock.Setup(static repo => repo.ListPostsAsync(1, It.IsAny())) + .ReturnsAsync(posts); + + // Act + var result = await _controller.GetPosts(1, CancellationToken.None); + + // Assert + Assert.That(result, Is.InstanceOf>>()); + var okResult = result.Result as OkObjectResult; + Assert.That(okResult, Is.Not.Null); + var returnedPosts = okResult.Value as IEnumerable; + Assert.That(returnedPosts, Is.Not.Null); + Assert.That(returnedPosts.Count(), Is.EqualTo(2)); + } + + [Test] + public async Task GetPost_ValidId_ReturnsPost() + { + // Arrange + _bloggingRepositoryMock.Setup(static repo => repo.GetPostAsync(1, It.IsAny())) + .ReturnsAsync(MockPost); + + // Act + var result = await _controller.GetPost(1, CancellationToken.None); + + // Assert + Assert.That(result, Is.InstanceOf>()); + var returnedPost = result.Value; + Assert.That(returnedPost, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(returnedPost.PostId, Is.EqualTo(MockPost.PostId)); + Assert.That(returnedPost.Title, Is.EqualTo(MockPost.Title)); + Assert.That(returnedPost.BlogId, Is.EqualTo(MockPost.BlogId)); + Assert.That(returnedPost.Content, Is.EqualTo(MockPost.Content)); + }); + } + + [Test] + public async Task GetPost_InvalidId_ReturnsNotFound() + { + // Arrange + _bloggingRepositoryMock.Setup(static repo => repo.GetPostAsync(1, It.IsAny())) + .ReturnsAsync((PostDto?) null); + + // Act + var result = await _controller.GetPost(1, CancellationToken.None); + + // Assert + Assert.That(result.Result, Is.InstanceOf()); + } + + [Test] + public async Task PostPost_NoId_StoresPost() + { + // Arrange + var post = MockPost with + { + PostId = 0 + }; + + var postId = 1; + + _bloggingRepositoryMock.Setup(static repo => repo.AddOrUpdatePostAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(MockPost with + { + PostId = postId + }); + + // Act + var result = await _controller.PostPost(post, + CancellationToken.None); + + // Assert + Assert.That(result, Is.InstanceOf>()); + var returnedPost = result.Value; + Assert.That(returnedPost, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(returnedPost.PostId, Is.EqualTo(postId)); + Assert.That(returnedPost.Title, Is.EqualTo(MockPost.Title)); + Assert.That(returnedPost.BlogId, Is.EqualTo(MockPost.BlogId)); + Assert.That(returnedPost.Content, Is.EqualTo(MockPost.Content)); + }); + } + + [Test] + public async Task PostPost_InvalidId_ReturnsNotFound() + { + // Arrange + _bloggingRepositoryMock.Setup(static repo => repo.AddOrUpdatePostAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((PostDto?) null); + + // Act + var result = await _controller.PostPost(MockPost, CancellationToken.None); + + // Assert + Assert.That(result.Result, Is.InstanceOf()); + } } \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Controllers/SendMessageControllerTests.cs b/tests/backend/WebApi.Tests/Controllers/SendMessageControllerTests.cs new file mode 100644 index 00000000..151e5e79 --- /dev/null +++ b/tests/backend/WebApi.Tests/Controllers/SendMessageControllerTests.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using Moq; +using WebApi.Controllers; +using WebApi.Entities; +using WebApi.Messaging; + +namespace WebApi.Tests.Controllers; + +[TestFixture] +public class SendMessageControllerTests +{ + [SetUp] + public void SetUp() + { + _messageSenderMock = new Mock(); + _controller = new SendMessageController(_messageSenderMock.Object); + } + private Mock _messageSenderMock; + private SendMessageController _controller; + + [Test] + public async Task Get_ReturnsOkResult() + { + // Arrange + _messageSenderMock.Setup(static messageSender => messageSender.SendMessage()) + .Returns("Message sent"); + + // Act + var result = await _controller.Get(); + + // Assert + Assert.That(result, Is.InstanceOf>>()); + Assert.That(result.Result, Is.InstanceOf()); + var okResult = (OkObjectResult) result.Result; + Assert.That(okResult.Value, Is.Not.Null); + Assert.That(okResult.Value, Is.AssignableFrom>()); + Assert.That(okResult.Value is GenericValue {Value: "Message sent"}); + } +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Controllers/ServiceControllerTests.cs b/tests/backend/WebApi.Tests/Controllers/ServiceControllerTests.cs new file mode 100644 index 00000000..48a723b6 --- /dev/null +++ b/tests/backend/WebApi.Tests/Controllers/ServiceControllerTests.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using WebApi.Controllers; +using WebApi.Entities; + +namespace WebApi.Tests.Controllers; + +[TestFixture] +public class ServiceControllerTests +{ + [SetUp] + public void SetUp() + { + _loggerMock = new Mock>(); + _controller = new ServiceController(_loggerMock.Object); + } + private Mock> _loggerMock; + private ServiceController _controller; + + [Test] + public async Task Version_ReturnsOkResult() + { + // Act + var result = await _controller.Version(); + + // Assert + Assert.That(result, Is.InstanceOf>()); + Assert.That(result.Result, Is.InstanceOf()); + } + + [Test] + public async Task Version_ReturnsVersionWithCorrectProperties() + { + // Act + var result = await _controller.Version(); + + // Assert + Assert.That(result.Result, Is.Not.Null); + Assert.That(result.Result, Is.AssignableFrom()); + var okResult = (OkObjectResult) result.Result; + Assert.That(okResult.Value, Is.Not.Null); + Assert.That(okResult.Value, Is.AssignableFrom()); + var version = okResult.Value as VersionInformation; + Assert.That(version, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(version.Version, Is.InstanceOf()); + Assert.That(version.Constants, Is.InstanceOf()); + Assert.That(version.EnvironmentName, Is.InstanceOf()); + Assert.That(version.InformationalVersion, Is.InstanceOf()); + }); + } + + [Test] + public async Task Ping_ReturnsOkResult() + { + // Act + var result = await _controller.Ping(); + + // Assert + Assert.That(result, Is.InstanceOf>>()); + Assert.That(result.Result, Is.InstanceOf()); + var okResult = (OkObjectResult) result.Result; + Assert.That(okResult.Value, Is.Not.Null); + Assert.That(okResult.Value, Is.AssignableFrom>()); + Assert.That(okResult.Value is GenericValue {Value: "Ok"}); + } + + [Test] + public async Task Ping_LogsInformationMessage() + { + // Act + await _controller.Ping(); + + // Assert + _loggerMock.Verify(static logger => logger.Log( + It.Is(static logLevel => logLevel == LogLevel.Information), + It.Is(static eventId => eventId.Id == 0), + It.Is(static (@object, type) => @object.ToString() == "Ping was called" && type.Name == "FormattedLogValues"), + It.IsAny(), + It.IsAny>()), + Times.Once); + } +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Database/BloggingContextTests.cs b/tests/backend/WebApi.Tests/Database/BloggingContextTests.cs new file mode 100644 index 00000000..d546a969 --- /dev/null +++ b/tests/backend/WebApi.Tests/Database/BloggingContextTests.cs @@ -0,0 +1,229 @@ +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using WebApi.Database; +using WebApi.Repository; + +namespace WebApi.Tests.Database; + +[TestFixture] +public class BloggingContextTests +{ + [OneTimeSetUp] + public async Task SetUp() + { + _msSqlContainer = new MsSqlDefaultConfiguration(); + await _msSqlContainer.InitializeAsync(); + + _connectionString = _msSqlContainer.MsSqlContainer.GetConnectionString(); + var options = new DbContextOptionsBuilder() + .UseSqlServer(_connectionString) + .Options; + + _context = new BloggingContext(options); + } + + [OneTimeTearDown] + public async Task Cleanup() + { + await _msSqlContainer.DisposeAsync(); + await _context.DisposeAsync(); + } + + private BloggingContext _context; + private MsSqlDefaultConfiguration _msSqlContainer; + private string _connectionString; + + [Test] + [Order(1)] + public void ConnectionState_ReturnsOpen() + { + // Given + using DbConnection connection = new SqlConnection(_msSqlContainer.MsSqlContainer.GetConnectionString()); + + // When + connection.Open(); + + // Then + Assert.That(connection.State, Is.EqualTo(ConnectionState.Open)); + } + + [Test] + [Order(2)] + public async Task BloggingContextMigrate_CreatesTablesAndCanStoreData() + { + // Act + await _context.Database.MigrateAsync(); + + var blogEntity = await _context.Blogs.AddAsync(new Blog + { + BlogId = 0, + Title = "test blog", + Url = "test://url" + }); + + await _context.SaveChangesAsync(); + + await _context.Posts.AddAsync(new Post + { + PostId = 0, + BlogId = blogEntity.Entity.BlogId, + Title = "test post", + Content = "test content" + }); + + await _context.SaveChangesAsync(); + + // Assert + Assert.That(_context.Blogs.Count(), Is.EqualTo(1)); + var blog = _context.Blogs.First(); + Assert.That(blog.Posts, Has.Count.EqualTo(1)); + var post = blog.Posts[0]; + + Assert.Multiple(() => + { + Assert.That(blog.Title, Is.EqualTo("test blog")); + Assert.That(blog.Url, Is.EqualTo("test://url")); + Assert.That(post.Title, Is.EqualTo("test post")); + Assert.That(post.Content, Is.EqualTo("test content")); + Assert.That(post.Blog, Is.Not.Null); + }); + + Assert.That(post.Blog.BlogId, Is.EqualTo(blog.BlogId)); + } + + [Test] + [Order(3)] + public async Task BloggingRepository_ReturnsSeededBlogsAndPosts() + { + // Arrange + var bloggingRepository = new BloggingRepository(_context); + + // Act and Assert + var blogs = (await bloggingRepository.ListBlogsAsync(CancellationToken.None)).ToList(); + Assert.That(blogs, Has.Count.EqualTo(1)); + var blog = blogs[0]; + + var blogAgain = await bloggingRepository.GetBlogAsync(blog.BlogId, CancellationToken.None); + Assert.That(blogAgain, Is.Not.Null); + + var posts = (await bloggingRepository.ListPostsAsync(blog.BlogId, + CancellationToken.None)).ToList(); + + Assert.That(posts, Has.Count.EqualTo(1)); + var post = posts[0]; + var postAgain = await bloggingRepository.GetPostAsync(post.PostId, CancellationToken.None); + Assert.That(postAgain, Is.Not.Null); + + Assert.That(await bloggingRepository.GetBlogAsync(100, CancellationToken.None), Is.Null); + Assert.That(await bloggingRepository.GetPostAsync(100, CancellationToken.None), Is.Null); + } + + [Test] + [Order(4)] + public async Task BloggingRepository_CanAddBlog() + { + // Arrange + var bloggingRepository = new BloggingRepository(_context); + + // Act + var blog = await bloggingRepository.AddOrUpdateBlogAsync(MockBlog with + { + BlogId = 0 + }, + CancellationToken.None); + + // Assert + Assert.That(blog, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(blog.BlogId, Is.Not.Zero); + Assert.That(blog.Title, Is.EqualTo(MockBlog.Title)); + Assert.That(blog.Url, Is.EqualTo(MockBlog.Url)); + }); + } + + [Test] + [Order(5)] + public async Task BloggingRepository_CanAddPost() + { + // Arrange + var bloggingRepository = new BloggingRepository(_context); + var blog = (await bloggingRepository.ListBlogsAsync(CancellationToken.None)).First(); + + // Act + var post = await bloggingRepository.AddOrUpdatePostAsync(MockPost with + { + BlogId = blog.BlogId, + PostId = 0 + }, + CancellationToken.None); + + // Assert + Assert.That(post, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(post.PostId, Is.Not.Zero); + Assert.That(post.Title, Is.EqualTo(MockPost.Title)); + Assert.That(post.Content, Is.EqualTo(MockPost.Content)); + }); + } + + [Test] + [Order(6)] + public async Task BloggingRepository_CanUpdateBlogs() + { + // Arrange + var bloggingRepository = new BloggingRepository(_context); + var blog = (await bloggingRepository.ListBlogsAsync(CancellationToken.None)).First(); + + // Act + var blogResult = await bloggingRepository.AddOrUpdateBlogAsync(blog with + { + Title = "changed" + }, + CancellationToken.None); + + var blogDoesNotExistResult = await bloggingRepository.AddOrUpdateBlogAsync(blog with + { + BlogId = 100, + Title = "changed" + }, + CancellationToken.None); + + // Assert + Assert.That(blogResult, Is.Not.Null); + Assert.That(blogResult.Title, Is.EqualTo("changed")); + Assert.That(blogDoesNotExistResult, Is.Null); + } + + [Test] + [Order(7)] + public async Task BloggingRepository_CanUpdatePosts() + { + // Arrange + var bloggingRepository = new BloggingRepository(_context); + var blog = (await bloggingRepository.ListBlogsAsync(CancellationToken.None)).First(); + var post = (await bloggingRepository.ListPostsAsync(blog.BlogId, CancellationToken.None)).First(); + + // Act + var postResult = await bloggingRepository.AddOrUpdatePostAsync(post with + { + Title = "changed" + }, + CancellationToken.None); + + var postDoesNotExistResult = await bloggingRepository.AddOrUpdatePostAsync(post with + { + PostId = 100, + Title = "changed" + }, + CancellationToken.None); + + // Assert + Assert.That(postResult, Is.Not.Null); + Assert.That(postResult.Title, Is.EqualTo("changed")); + Assert.That(postDoesNotExistResult, Is.Null); + } +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Database/MsSqlContainerTest.cs b/tests/backend/WebApi.Tests/Database/MsSqlContainerTest.cs new file mode 100644 index 00000000..cd4d5efb --- /dev/null +++ b/tests/backend/WebApi.Tests/Database/MsSqlContainerTest.cs @@ -0,0 +1,14 @@ +using Testcontainers.MsSql; + +namespace WebApi.Tests.Database; + +/// +/// MsSql test container base class +/// +/// +public class MsSqlContainerTest(MsSqlContainer msSqlContainer) : IAsyncDisposable +{ + public readonly MsSqlContainer MsSqlContainer = msSqlContainer; + public async ValueTask DisposeAsync() => await MsSqlContainer.DisposeAsync(); + public Task InitializeAsync() => MsSqlContainer.StartAsync(); +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Database/MsSqlDefaultConfiguration.cs b/tests/backend/WebApi.Tests/Database/MsSqlDefaultConfiguration.cs new file mode 100644 index 00000000..26070502 --- /dev/null +++ b/tests/backend/WebApi.Tests/Database/MsSqlDefaultConfiguration.cs @@ -0,0 +1,10 @@ +using Testcontainers.MsSql; + +namespace WebApi.Tests.Database; + +#pragma warning disable S2094 // Classes should not be empty +public sealed class MsSqlDefaultConfiguration() : MsSqlContainerTest(new MsSqlBuilder() + // TODO: Temporary fix for this issue, can be removed later https://github.com/testcontainers/testcontainers-dotnet/issues/1264 + .WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") + .Build()); +#pragma warning restore S2094 // Classes should not be empty \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Entities/BaseDtoValidationTests.cs b/tests/backend/WebApi.Tests/Entities/BaseDtoValidationTests.cs new file mode 100644 index 00000000..85d1fcab --- /dev/null +++ b/tests/backend/WebApi.Tests/Entities/BaseDtoValidationTests.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace WebApi.Tests.Entities; + +public class BaseDtoValidationTests + where T : IValidatableObject +{ + protected static void Validate_ReturnsValidationError(T dto, string expectedErrorMessage) + { + // Arrange + var validationContext = new ValidationContext(dto); + + // Act + var results = dto.Validate(validationContext) + .ToList(); + + // Assert + Assert.That(results, Has.Count.EqualTo(1)); + Assert.That(results[0].ErrorMessage, Is.EqualTo(expectedErrorMessage)); + } +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Entities/BlogDtoTests.cs b/tests/backend/WebApi.Tests/Entities/BlogDtoTests.cs new file mode 100644 index 00000000..08cd0384 --- /dev/null +++ b/tests/backend/WebApi.Tests/Entities/BlogDtoTests.cs @@ -0,0 +1,39 @@ +using WebApi.Entities; + +namespace WebApi.Tests.Entities; + +[TestFixture] +public class BlogDtoTests : BaseDtoValidationTests +{ + [Test] + public void Validate_WhenTitleIsEmpty_ReturnsValidationError() => + Validate_ReturnsValidationError(MockBlog with + { + Title = string.Empty + }, + "Title must be set"); + + [Test] + public void Validate_WhenTitleIsTooLong_ReturnsValidationError() => + Validate_ReturnsValidationError(MockBlog with + { + Title = new string('a', 501) + }, + "Title is longer than the maximum amount of characters (500)"); + + [Test] + public void Validate_WhenUrlIsEmpty_ReturnsValidationError() => + Validate_ReturnsValidationError(MockBlog with + { + Url = string.Empty + }, + "Url must be set"); + + [Test] + public void Validate_WhenUrlIsTooLong_ReturnsValidationError() => + Validate_ReturnsValidationError(MockBlog with + { + Url = new string('a', 1001) + }, + "Url is longer than the maximum amount of characters (1000)"); +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Entities/PostDtoTests.cs b/tests/backend/WebApi.Tests/Entities/PostDtoTests.cs new file mode 100644 index 00000000..dc949679 --- /dev/null +++ b/tests/backend/WebApi.Tests/Entities/PostDtoTests.cs @@ -0,0 +1,47 @@ +using WebApi.Entities; + +namespace WebApi.Tests.Entities; + +[TestFixture] +public class PostDtoTests : BaseDtoValidationTests +{ + [Test] + public void Validate_WhenTitleIsEmpty_ReturnsValidationError() => + Validate_ReturnsValidationError(MockPost with + { + Title = string.Empty + }, + "Title must be set"); + + [Test] + public void Validate_WhenTitleIsTooLong_ReturnsValidationError() => + Validate_ReturnsValidationError(MockPost with + { + Title = new string('a', 2001) + }, + "Title is longer than the maximum amount of characters (2000)"); + + [Test] + public void Validate_WhenContentIsEmpty_ReturnsValidationError() => + Validate_ReturnsValidationError(MockPost with + { + Content = string.Empty + }, + "Content must be set"); + + [Test] + public void Validate_WhenContentIsTooLong_ReturnsValidationError() => + Validate_ReturnsValidationError(MockPost with + { + Content = new string('a', 8001) + }, + "Content is longer than the maximum amount of characters (8000)"); + + [Test] + public void Validate_WhenBlogIdIsZero_ReturnsValidationError() => + Validate_ReturnsValidationError(MockPost with + { + BlogId = 0 + }, + "BlogId must be set"); +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Entities/VersionInformationTests.cs b/tests/backend/WebApi.Tests/Entities/VersionInformationTests.cs new file mode 100644 index 00000000..ffa05ba0 --- /dev/null +++ b/tests/backend/WebApi.Tests/Entities/VersionInformationTests.cs @@ -0,0 +1,89 @@ +using System.Reflection; +using System.Reflection.Emit; +using WebApi.Attributes; +using WebApi.Entities; + +namespace WebApi.Tests.Entities; + +[TestFixture] +public class VersionInformationTests +{ + private static Assembly CreateFakeAssembly(params object[] constructorArgs) + where T : Attribute + { + var type = typeof(T); + var constructor = type.GetConstructor([typeof(string)]) ?? throw new InvalidOperationException($"Could not get constructor for {type.Name}"); + + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"{type.Name}.FakeAssembly"), + AssemblyBuilderAccess.RunAndCollect, + [ + new CustomAttributeBuilder(constructor, + constructorArgs) + ]) ?? throw new InvalidOperationException($"Failed to build assembly for {type.Name}"); + + var assemblyFake = assemblyBuilder.Modules.First() + .Assembly; + + return assemblyFake; + } + + [Test] + public void ConstantsProperty_ExtractsConstantsCorrectly() + { + // Arrange + var expectedConstants = new[] + { + "DEBUG", "NET8" + }; + + var assemblyFake = CreateFakeAssembly("\"DEBUG;NET8\""); + + // Act + var versionInformation = new VersionInformation(assemblyFake); + + // Assert + Assert.That(versionInformation.Constants, Is.EqualTo(expectedConstants)); + } + + [Test] + public void VersionProperty_ExtractsVersionCorrectly() + { + // Arrange + const string expectedVersion = "1.0.0"; + var assemblyFake = CreateFakeAssembly(expectedVersion); + + // Act + var versionInformation = new VersionInformation(assemblyFake); + + // Assert + Assert.That(versionInformation.Version, Is.EqualTo(expectedVersion)); + } + + [Test] + public void InformationalVersionProperty_ExtractsInformationalVersionCorrectly() + { + // Arrange + const string expectedInformationalVersion = "1.0.0-dev"; + var assemblyFake = CreateFakeAssembly(expectedInformationalVersion); + + // Act + var versionInformation = new VersionInformation(assemblyFake); + + // Assert + Assert.That(versionInformation.InformationalVersion, Is.EqualTo(expectedInformationalVersion)); + } + + [Test] + public void EnvironmentNameProperty_FallbacksToUnknownWhenNoEnvironmentVariablesSet() + { + // Arrange + Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", null); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null); + + // Act + var versionInformation = new VersionInformation(typeof(VersionInformationTests).Assembly); + + // Assert + Assert.That(versionInformation.EnvironmentName, Is.EqualTo("Unknown")); + } +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/GlobalUsings.cs b/tests/backend/WebApi.Tests/GlobalUsings.cs new file mode 100644 index 00000000..1384ee49 --- /dev/null +++ b/tests/backend/WebApi.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using static WebApi.Tests.MockObjects; \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/Messaging/RabbitMqTests.cs b/tests/backend/WebApi.Tests/Messaging/RabbitMqTests.cs new file mode 100644 index 00000000..edc1ccce --- /dev/null +++ b/tests/backend/WebApi.Tests/Messaging/RabbitMqTests.cs @@ -0,0 +1,81 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Moq; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Testcontainers.RabbitMq; +using WebApi.Messaging; + +namespace WebApi.Tests.Messaging; + +[TestFixture] +public class RabbitMqTests +{ + [OneTimeSetUp] + public async Task InitializeAsync() + { + await _rabbitMqContainer.StartAsync(); + _unitTestActivity = new Activity(nameof(RabbitMqTests)); + } + + [OneTimeTearDown] + public async Task DisposeAsync() + { + await _rabbitMqContainer.DisposeAsync(); + _unitTestActivity.Stop(); + _unitTestActivity.Dispose(); + } + + private readonly RabbitMqContainer _rabbitMqContainer = new RabbitMqBuilder().Build(); + private Activity _unitTestActivity; + + [Test] + [Order(1)] + public void IsOpen_ReturnsTrue() + { + // Given + var connectionFactory = new ConnectionFactory + { + Uri = new Uri(_rabbitMqContainer.GetConnectionString()) + }; + + // When + using var connection = connectionFactory.CreateConnection(); + + // Then + Assert.That(connection.IsOpen); + } + + [Test] + [Order(2)] + public void MessageSender_SendsMessage() + { + // Given + var connectionFactory = new ConnectionFactory + { + Uri = new Uri(_rabbitMqContainer.GetConnectionString()) + }; + + Environment.SetEnvironmentVariable("RABBITMQ_HOSTNAME", connectionFactory.HostName); + Environment.SetEnvironmentVariable("RABBITMQ_PORT", connectionFactory.Port.ToString()); + Environment.SetEnvironmentVariable("RABBITMQ_DEFAULT_USER", connectionFactory.UserName); + Environment.SetEnvironmentVariable("RABBITMQ_DEFAULT_PASS", connectionFactory.Password); + var messageSenderLogger = new Mock>(); + using var messageSender = new MessageSender(messageSenderLogger.Object); + var messageReceiverLogger = new Mock>(); + using var messageReceiver = new MessageReceiver(messageReceiverLogger.Object); + + // When + messageSender.SendMessage(); + messageReceiver.ReceiveMessage(new BasicDeliverEventArgs()); + + // Then + messageSenderLogger.VerifyLog(LogLevel.Information, Times.Once(), "Message sent:"); + + messageSenderLogger.VerifyLog(LogLevel.Error, Times.Never(), ".*"); + + messageReceiverLogger.VerifyLog(LogLevel.Information, Times.Once(), "Message received:"); + + messageReceiverLogger.VerifyLog(LogLevel.Error, Times.Never(), ".*"); + } +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/MockHelper.cs b/tests/backend/WebApi.Tests/MockHelper.cs new file mode 100644 index 00000000..68983c91 --- /dev/null +++ b/tests/backend/WebApi.Tests/MockHelper.cs @@ -0,0 +1,17 @@ +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Moq; + +namespace WebApi.Tests; + +internal static class MockHelper +{ + public static void VerifyLog(this Mock> loggerMock, LogLevel level, Times times, string? regex = null) => + loggerMock.Verify(logger => logger.Log( + level, + It.IsAny(), + It.Is((x, y) => regex == null || Regex.IsMatch(x.ToString() ?? string.Empty, regex)), + It.IsAny(), + It.IsAny>()), + times); +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/MockObjects.cs b/tests/backend/WebApi.Tests/MockObjects.cs new file mode 100644 index 00000000..d59c609c --- /dev/null +++ b/tests/backend/WebApi.Tests/MockObjects.cs @@ -0,0 +1,35 @@ +using WebApi.Entities; + +namespace WebApi.Tests; + +internal static class MockObjects +{ + internal static readonly BlogDto MockBlog = new() + { + BlogId = 1, + Title = "Test blog", + Url = "test://url" + }; + + internal static readonly PostDto MockPost = new() + { + BlogId = 1, + PostId = 1, + Title = "Test post", + Content = "Test content" + }; + + internal static BlogDto MockBlogFactory(int id) => + MockBlog with + { + BlogId = id, + Title = $"Blog entry {id}" + }; + + internal static PostDto MockPostFactory(int id) => + MockPost with + { + PostId = id, + Title = $"Post entry {id}" + }; +} \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/WebApi.Tests.csproj b/tests/backend/WebApi.Tests/WebApi.Tests.csproj index 33e66337..26c380e5 100644 --- a/tests/backend/WebApi.Tests/WebApi.Tests.csproj +++ b/tests/backend/WebApi.Tests/WebApi.Tests.csproj @@ -1,26 +1,32 @@ - - false - true - + + false + true + enable + - - - - - - - - - + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + - - - + + + - - - + + + - + \ No newline at end of file diff --git a/tests/backend/WebApi.Tests/packages.lock.json b/tests/backend/WebApi.Tests/packages.lock.json index 6005fb44..3b179019 100644 --- a/tests/backend/WebApi.Tests/packages.lock.json +++ b/tests/backend/WebApi.Tests/packages.lock.json @@ -10,23 +10,23 @@ }, "Microsoft.AspNetCore.Mvc.Testing": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "dh7J7O5ZbNix3tpRi5CTAD89yFD8jl374B42/tD5sp3MFXA0KSaDMm+3XU0EqE2l8sVJlV2//FnDkK1LCcXuCA==", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "iYJ0tw9dOMNVJ/8VbtTYvS4INGelLShllPzO/jA/UIcKZmjz7Mum43Os3/gDaXdcSHt/d1LlvE4vh8zYwQ+UiQ==", "dependencies": { - "Microsoft.AspNetCore.TestHost": "8.0.7", + "Microsoft.AspNetCore.TestHost": "8.0.8", "Microsoft.Extensions.DependencyModel": "8.0.1", "Microsoft.Extensions.Hosting": "8.0.0" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.10.0, )", - "resolved": "17.10.0", - "contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==", + "requested": "[17.11.1, )", + "resolved": "17.11.1", + "contentHash": "U3Ty4BaGoEu+T2bwSko9tWqWUOU16WzSFkq6U8zve75oRBMSLTBdMAZrVNNz1Tq12aCdDom9fcOcM9QZaFHqFg==", "dependencies": { - "Microsoft.CodeCoverage": "17.10.0", - "Microsoft.TestPlatform.TestHost": "17.10.0" + "Microsoft.CodeCoverage": "17.11.1", + "Microsoft.TestPlatform.TestHost": "17.11.1" } }, "Moq": { @@ -40,21 +40,39 @@ }, "NUnit": { "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "MT/DpAhjtiytzhTgTqIhBuWx4y26PKfDepYUHUM+5uv4TsryHC2jwFo5e6NhWkApCm/G6kZ80dRjdJFuAxq3rg==" + "requested": "[4.2.2, )", + "resolved": "4.2.2", + "contentHash": "mon0OPko28yZ/foVXrhiUvq1LReaGsBdziumyyYGxV/pOE4q92fuYeN+AF+gEU5pCjzykcdBt5l7xobTaiBjsg==" }, "NUnit.Analyzers": { "type": "Direct", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "4fJojPkzdoa4nB2+p6U+fITvPnVvwWSnsmiJ/Dl30xqiL3oxNbYvfeSLVd91hOmEjoUqSwN3Z7j1aFedjqWbUA==" + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "Ki4p1XrmnYil64HE9VkJSuo6KBr8vg7Mut43atYvItDp3HwIcRY5hi+n5o8GaX1IBCwYLaJgW5ZfZuBVPYXEkg==" }, "NUnit3TestAdapter": { "type": "Direct", - "requested": "[4.5.0, )", - "resolved": "4.5.0", - "contentHash": "s8JpqTe9bI2f49Pfr3dFRfoVSuFQyraTj68c3XXjIS/MRGvvkLnrg6RLqnTjdShX+AdFUCCU/4Xex58AdUfs6A==" + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "R7e1+a4vuV/YS+ItfL7f//rG+JBvVeVLX4mHzFEZo4W1qEKl8Zz27AqvQSAqo+BtIzUCo4aAJMYa56VXS4hudw==" + }, + "Testcontainers.MsSql": { + "type": "Direct", + "requested": "[3.10.0, )", + "resolved": "3.10.0", + "contentHash": "8FDr/j1RUN7sCDJnInXT19llmuDUXy4zB7uY9sjYXnBX6LduIamFimOkrwFY2BhzxTwvnVjiDw9acDBPQjQqvQ==", + "dependencies": { + "Testcontainers": "3.10.0" + } + }, + "Testcontainers.RabbitMq": { + "type": "Direct", + "requested": "[3.10.0, )", + "resolved": "3.10.0", + "contentHash": "ih6Pbx68k4Q4qxJQWB5pRIidiXLHhY970N/CDXe1vKYUn9gkz1ZzJe4cTeqHdHKEYmrsNtWlB0u52sawMlKeBw==", + "dependencies": { + "Testcontainers": "3.10.0" + } }, "Azure.Core": { "type": "Transitive", @@ -92,6 +110,24 @@ "System.Diagnostics.EventLog": "6.0.0" } }, + "Docker.DotNet": { + "type": "Transitive", + "resolved": "3.125.15", + "contentHash": "XN8FKxVv8Mjmwu104/Hl9lM61pLY675s70gzwSj8KR5pwblo8HfWLcCuinh9kYsqujBkMH4HVRCEcRuU6al4BQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "System.Buffers": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Docker.DotNet.X509": { + "type": "Transitive", + "resolved": "3.125.15", + "contentHash": "ONQN7ImrL3tHStUUCCPHwrFFQVpIpE+7L6jaDAMwSF+yTEmeWBmRARQZDRuvfj/+WtB8RR0oTW0tT3qQMSyHOw==", + "dependencies": { + "Docker.DotNet": "3.125.15" + } + }, "Google.Protobuf": { "type": "Transitive", "resolved": "3.22.5", @@ -132,8 +168,8 @@ }, "Microsoft.AspNetCore.TestHost": { "type": "Transitive", - "resolved": "8.0.7", - "contentHash": "Q+LAum9DPXAMRzZXQ8QcT1B3DiEopw1Agc8yb7wOOgNQvyoCEG2kaA3kiHWCPR+GMcvzPQ79n/em8eYaLbCcAw==", + "resolved": "8.0.8", + "contentHash": "mQSMZMA72IK/N79HgLn7tCCkN+stEq6yhq0vr6xfw2wvcfjAV2R6JFUYGUDHmWVUxTjDWjQX+Yrd5S9vQKnPLA==", "dependencies": { "System.IO.Pipelines": "8.0.0" } @@ -145,8 +181,8 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA==" + "resolved": "17.11.1", + "contentHash": "nPJqrcA5iX+Y0kqoT3a+pD/8lrW/V7ayqnEJQsTonSoPz59J8bmoQhcSN4G8+UJ64Hkuf0zuxnfuj2lkHOq4cA==" }, "Microsoft.CSharp": { "type": "Transitive", @@ -625,18 +661,18 @@ }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==", + "resolved": "17.11.1", + "contentHash": "E2jZqAU6JeWEVsyOEOrSW1o1bpHLgb25ypvKNB/moBXPVsFYBPd/Jwi7OrYahG50J83LfHzezYI+GaEkpAotiA==", "dependencies": { "System.Reflection.Metadata": "1.6.0" } }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "17.10.0", - "contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==", + "resolved": "17.11.1", + "contentHash": "DnG+GOqJXO/CkoqlJWeDFTgPhqD/V6VqUIL3vINizCWZ3X+HshCtbbyDdSHQQEjrc2Sl/K3yaxX6s+5LFEdYuw==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.10.0", + "Microsoft.TestPlatform.ObjectModel": "17.11.1", "Newtonsoft.Json": "13.0.1" } }, @@ -753,6 +789,24 @@ "System.Threading.Channels": "7.0.0" } }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SSH.NET": { + "type": "Transitive", + "resolved": "2023.0.0", + "contentHash": "g+3VDUrYhm0sqSxmlQFgRFrmBxhQvVh4pfn4pqjkX7WXE3tTjt1tIsOtjuz3mz/5s8gFFQVRydwCJ7Ohs54sJA==", + "dependencies": { + "SshNet.Security.Cryptography": "[1.3.0]" + } + }, + "SshNet.Security.Cryptography": { + "type": "Transitive", + "resolved": "1.3.0", + "contentHash": "5pBIXRjcSO/amY8WztpmNOhaaCNHY/B6CcYDI7FSTgqSyo/ZUojlLiKcsl+YGbxQuLX439qIkMfP0PHqxqJi/Q==" + }, "Swashbuckle.AspNetCore": { "type": "Transitive", "resolved": "6.6.2", @@ -790,6 +844,11 @@ "resolved": "6.6.2", "contentHash": "mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==" }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "6.0.1", @@ -972,6 +1031,18 @@ "System.Drawing.Common": "6.0.0" } }, + "Testcontainers": { + "type": "Transitive", + "resolved": "3.10.0", + "contentHash": "4oFyiUPCOM3s/sKDnIcOJZIn664d/8+fPvODDlfbb0QAfQqHlqjc2kIoFOLAt3oJRZP9/FJtTvcNvp9j7h4UBA==", + "dependencies": { + "Docker.DotNet": "3.125.15", + "Docker.DotNet.X509": "3.125.15", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", + "SSH.NET": "2023.0.0", + "SharpZipLib": "1.4.2" + } + }, "webapi": { "type": "Project", "dependencies": {