From 3f9fba3cd5ecd59cadec2b82f508088fe099525f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:00:45 +0000 Subject: [PATCH 1/2] Initial plan From b0113aa1a71103a1ff816b0830f5e24bc30b65cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:10:17 +0000 Subject: [PATCH 2/2] Add comprehensive unit tests for User Profile REST API Co-authored-by: frye <98463+frye@users.noreply.github.com> --- .../Controllers/HomeControllerTests.cs | 52 +++ .../Controllers/UsersControllerTests.cs | 328 ++++++++++++++++++ .../Models/UserProfileTests.cs | 64 ++++ .../net-users-api.tests.csproj | 28 ++ net-users-demo.sln | 70 ++-- 5 files changed, 520 insertions(+), 22 deletions(-) create mode 100644 net-users-api.tests/Controllers/HomeControllerTests.cs create mode 100644 net-users-api.tests/Controllers/UsersControllerTests.cs create mode 100644 net-users-api.tests/Models/UserProfileTests.cs create mode 100644 net-users-api.tests/net-users-api.tests.csproj diff --git a/net-users-api.tests/Controllers/HomeControllerTests.cs b/net-users-api.tests/Controllers/HomeControllerTests.cs new file mode 100644 index 0000000..ac0d381 --- /dev/null +++ b/net-users-api.tests/Controllers/HomeControllerTests.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using NetUsersApi.Controllers; +using NetUsersApi.Models; +using FluentAssertions; + +namespace NetUsersApi.Tests.Controllers; + +public class HomeControllerTests +{ + private readonly Mock> _mockLogger; + private readonly HomeController _controller; + + public HomeControllerTests() + { + _mockLogger = new Mock>(); + _controller = new HomeController(_mockLogger.Object); + } + + [Fact] + public void Index_ReturnsViewResult_WithUserList() + { + // Act + var result = _controller.Index(); + + // Assert + result.Should().BeOfType(); + var viewResult = result as ViewResult; + viewResult.Model.Should().BeAssignableTo>(); + + var users = viewResult.Model as IEnumerable; + users.Should().NotBeNull(); + } + + [Fact] + public void Index_LogsInformationMessage() + { + // Act + var result = _controller.Index(); + + // Assert + _mockLogger.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("GET / endpoint called")), + null, + It.IsAny>()), + Times.Once); + } +} diff --git a/net-users-api.tests/Controllers/UsersControllerTests.cs b/net-users-api.tests/Controllers/UsersControllerTests.cs new file mode 100644 index 0000000..e6a1d3a --- /dev/null +++ b/net-users-api.tests/Controllers/UsersControllerTests.cs @@ -0,0 +1,328 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using NetUsersApi.Controllers; +using NetUsersApi.Models; +using FluentAssertions; + +namespace NetUsersApi.Tests.Controllers; + +public class UsersControllerTests +{ + private readonly Mock> _mockLogger; + private readonly UsersController _controller; + + public UsersControllerTests() + { + _mockLogger = new Mock>(); + _controller = new UsersController(_mockLogger.Object); + } + + #region GetUsers Tests + + [Fact] + public void GetUsers_ReturnsOkResult_WithListOfUsers() + { + // Act + var result = _controller.GetUsers(); + + // Assert + result.Result.Should().BeOfType(); + var okResult = result.Result as OkObjectResult; + okResult.Value.Should().BeAssignableTo>(); + + var users = okResult.Value as IEnumerable; + users.Should().NotBeNull(); + users.Should().NotBeEmpty(); + } + + [Fact] + public void GetUsers_LogsInformationMessage() + { + // Act + var result = _controller.GetUsers(); + + // Assert + _mockLogger.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("GET /api/v1/users endpoint called")), + null, + It.IsAny>()), + Times.Once); + } + + #endregion + + #region GetUser Tests + + [Fact] + public void GetUser_WithValidId_ReturnsOkResult_WithUser() + { + // Arrange + var validId = "1"; + + // Act + var result = _controller.GetUser(validId); + + // Assert + result.Result.Should().BeOfType(); + var okResult = result.Result as OkObjectResult; + okResult.Value.Should().BeOfType(); + + var user = okResult.Value as UserProfile; + user.Should().NotBeNull(); + user.Id.Should().Be(validId); + } + + [Fact] + public void GetUser_WithInvalidId_ReturnsNotFound() + { + // Arrange + var invalidId = "999"; + + // Act + var result = _controller.GetUser(invalidId); + + // Assert + result.Result.Should().BeOfType(); + var notFoundResult = result.Result as NotFoundObjectResult; + notFoundResult.Value.Should().NotBeNull(); + } + + [Fact] + public void GetUser_WithNonExistentId_ReturnsNotFoundWithErrorMessage() + { + // Arrange + var nonExistentId = "nonexistent"; + + // Act + var result = _controller.GetUser(nonExistentId); + + // Assert + result.Result.Should().BeOfType(); + var notFoundResult = result.Result as NotFoundObjectResult; + + var errorObject = notFoundResult.Value; + errorObject.Should().NotBeNull(); + var errorProperty = errorObject.GetType().GetProperty("error"); + errorProperty.Should().NotBeNull(); + errorProperty.GetValue(errorObject).Should().Be("User not found"); + } + + #endregion + + #region CreateUser Tests + + [Fact] + public void CreateUser_WithValidUser_ReturnsCreatedAtAction() + { + // Arrange + var newUser = new UserProfile + { + Id = "100", + FullName = "Test User", + Emoji = "๐Ÿงช" + }; + + // Act + var result = _controller.CreateUser(newUser); + + // Assert + result.Result.Should().BeOfType(); + var createdResult = result.Result as CreatedAtActionResult; + createdResult.ActionName.Should().Be(nameof(UsersController.GetUser)); + createdResult.RouteValues["id"].Should().Be(newUser.Id); + createdResult.Value.Should().Be(newUser); + } + + [Fact] + public void CreateUser_WithValidUser_AddsUserToList() + { + // Arrange + var newUser = new UserProfile + { + Id = "101", + FullName = "Another Test User", + Emoji = "๐ŸŽ‰" + }; + + // Act + _controller.CreateUser(newUser); + var getUserResult = _controller.GetUser(newUser.Id); + + // Assert + getUserResult.Result.Should().BeOfType(); + var okResult = getUserResult.Result as OkObjectResult; + var retrievedUser = okResult.Value as UserProfile; + retrievedUser.Should().NotBeNull(); + retrievedUser.Id.Should().Be(newUser.Id); + retrievedUser.FullName.Should().Be(newUser.FullName); + retrievedUser.Emoji.Should().Be(newUser.Emoji); + } + + [Fact] + public void CreateUser_WithNullUser_ReturnsBadRequest() + { + // Act + var result = _controller.CreateUser(null); + + // Assert + result.Result.Should().BeOfType(); + var badRequestResult = result.Result as BadRequestObjectResult; + badRequestResult.Value.Should().NotBeNull(); + } + + [Fact] + public void CreateUser_WithNullUser_ReturnsBadRequestWithErrorMessage() + { + // Act + var result = _controller.CreateUser(null); + + // Assert + result.Result.Should().BeOfType(); + var badRequestResult = result.Result as BadRequestObjectResult; + + var errorObject = badRequestResult.Value; + errorObject.Should().NotBeNull(); + var errorProperty = errorObject.GetType().GetProperty("error"); + errorProperty.Should().NotBeNull(); + errorProperty.GetValue(errorObject).Should().Be("Invalid user data"); + } + + #endregion + + #region UpdateUser Tests + + [Fact] + public void UpdateUser_WithValidIdAndUser_ReturnsOkResult_WithUpdatedUser() + { + // Arrange + var userId = "1"; + var updatedUser = new UserProfile + { + Id = "999", // This should be overwritten + FullName = "Updated Name", + Emoji = "๐Ÿ”„" + }; + + // Act + var result = _controller.UpdateUser(userId, updatedUser); + + // Assert + result.Result.Should().BeOfType(); + var okResult = result.Result as OkObjectResult; + var returnedUser = okResult.Value as UserProfile; + returnedUser.Should().NotBeNull(); + returnedUser.Id.Should().Be(userId); // ID should match the path parameter + returnedUser.FullName.Should().Be(updatedUser.FullName); + returnedUser.Emoji.Should().Be(updatedUser.Emoji); + } + + [Fact] + public void UpdateUser_EnsuresIdDoesNotChange() + { + // Arrange + var userId = "2"; + var updatedUser = new UserProfile + { + Id = "different-id", + FullName = "Updated Name", + Emoji = "๐Ÿ”„" + }; + + // Act + var result = _controller.UpdateUser(userId, updatedUser); + + // Assert + result.Result.Should().BeOfType(); + var okResult = result.Result as OkObjectResult; + var returnedUser = okResult.Value as UserProfile; + returnedUser.Id.Should().Be(userId); // ID should be the path parameter, not the body ID + } + + [Fact] + public void UpdateUser_WithNonExistentId_ReturnsNotFound() + { + // Arrange + var nonExistentId = "999"; + var updatedUser = new UserProfile + { + Id = nonExistentId, + FullName = "Updated Name", + Emoji = "๐Ÿ”„" + }; + + // Act + var result = _controller.UpdateUser(nonExistentId, updatedUser); + + // Assert + result.Result.Should().BeOfType(); + var notFoundResult = result.Result as NotFoundObjectResult; + notFoundResult.Value.Should().NotBeNull(); + } + + [Fact] + public void UpdateUser_WithNonExistentId_ReturnsNotFoundWithErrorMessage() + { + // Arrange + var nonExistentId = "nonexistent"; + var updatedUser = new UserProfile + { + Id = nonExistentId, + FullName = "Updated Name", + Emoji = "๐Ÿ”„" + }; + + // Act + var result = _controller.UpdateUser(nonExistentId, updatedUser); + + // Assert + result.Result.Should().BeOfType(); + var notFoundResult = result.Result as NotFoundObjectResult; + + var errorObject = notFoundResult.Value; + errorObject.Should().NotBeNull(); + var errorProperty = errorObject.GetType().GetProperty("error"); + errorProperty.Should().NotBeNull(); + errorProperty.GetValue(errorObject).Should().Be("User not found"); + } + + [Fact] + public void UpdateUser_WithNullUser_ReturnsBadRequest() + { + // Arrange + var userId = "1"; + + // Act + var result = _controller.UpdateUser(userId, null); + + // Assert + result.Result.Should().BeOfType(); + var badRequestResult = result.Result as BadRequestObjectResult; + badRequestResult.Value.Should().NotBeNull(); + } + + [Fact] + public void UpdateUser_WithNullUser_ReturnsBadRequestWithErrorMessage() + { + // Arrange + var userId = "1"; + + // Act + var result = _controller.UpdateUser(userId, null); + + // Assert + result.Result.Should().BeOfType(); + var badRequestResult = result.Result as BadRequestObjectResult; + + var errorObject = badRequestResult.Value; + errorObject.Should().NotBeNull(); + var errorProperty = errorObject.GetType().GetProperty("error"); + errorProperty.Should().NotBeNull(); + errorProperty.GetValue(errorObject).Should().Be("Invalid user data"); + } + + #endregion +} diff --git a/net-users-api.tests/Models/UserProfileTests.cs b/net-users-api.tests/Models/UserProfileTests.cs new file mode 100644 index 0000000..5d3785f --- /dev/null +++ b/net-users-api.tests/Models/UserProfileTests.cs @@ -0,0 +1,64 @@ +using NetUsersApi.Models; +using FluentAssertions; + +namespace NetUsersApi.Tests.Models; + +public class UserProfileTests +{ + [Fact] + public void UserProfile_CanBeCreated_WithRequiredProperties() + { + // Arrange & Act + var user = new UserProfile + { + Id = "1", + FullName = "John Doe", + Emoji = "๐Ÿ˜€" + }; + + // Assert + user.Should().NotBeNull(); + user.Id.Should().Be("1"); + user.FullName.Should().Be("John Doe"); + user.Emoji.Should().Be("๐Ÿ˜€"); + } + + [Fact] + public void UserProfile_Properties_CanBeModified() + { + // Arrange + var user = new UserProfile + { + Id = "1", + FullName = "John Doe", + Emoji = "๐Ÿ˜€" + }; + + // Act + user.FullName = "Jane Smith"; + user.Emoji = "๐Ÿš€"; + + // Assert + user.FullName.Should().Be("Jane Smith"); + user.Emoji.Should().Be("๐Ÿš€"); + user.Id.Should().Be("1"); // ID should remain unchanged + } + + [Fact] + public void UserProfile_Id_CanBeSet() + { + // Arrange + var user = new UserProfile + { + Id = "1", + FullName = "John Doe", + Emoji = "๐Ÿ˜€" + }; + + // Act + user.Id = "2"; + + // Assert + user.Id.Should().Be("2"); + } +} diff --git a/net-users-api.tests/net-users-api.tests.csproj b/net-users-api.tests/net-users-api.tests.csproj new file mode 100644 index 0000000..28183cb --- /dev/null +++ b/net-users-api.tests/net-users-api.tests.csproj @@ -0,0 +1,28 @@ +๏ปฟ + + + net9.0 + net_users_api.tests + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/net-users-demo.sln b/net-users-demo.sln index e9bb8f5..5b27b1e 100644 --- a/net-users-demo.sln +++ b/net-users-demo.sln @@ -1,22 +1,48 @@ -๏ปฟ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "net-users-api", "net-users-api\net-users-api.csproj", "{F2C0F837-02EA-42E5-BDC0-48C31DD4245D}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal +๏ปฟ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "net-users-api", "net-users-api\net-users-api.csproj", "{F2C0F837-02EA-42E5-BDC0-48C31DD4245D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "net-users-api.tests", "net-users-api.tests\net-users-api.tests.csproj", "{73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|x64.Build.0 = Debug|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Debug|x86.Build.0 = Debug|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|Any CPU.Build.0 = Release|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|x64.ActiveCfg = Release|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|x64.Build.0 = Release|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|x86.ActiveCfg = Release|Any CPU + {F2C0F837-02EA-42E5-BDC0-48C31DD4245D}.Release|x86.Build.0 = Release|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Debug|x64.Build.0 = Debug|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Debug|x86.Build.0 = Debug|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Release|Any CPU.Build.0 = Release|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Release|x64.ActiveCfg = Release|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Release|x64.Build.0 = Release|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Release|x86.ActiveCfg = Release|Any CPU + {73A0A8C1-986A-4B50-A4C7-9BEAD9DEAE2D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal