diff --git a/net-users-api.Tests/Controllers/UsersControllerTests.cs b/net-users-api.Tests/Controllers/UsersControllerTests.cs
new file mode 100644
index 0000000..12af45b
--- /dev/null
+++ b/net-users-api.Tests/Controllers/UsersControllerTests.cs
@@ -0,0 +1,253 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NetUsersApi.Controllers;
+using NetUsersApi.Models;
+using FluentAssertions;
+
+namespace NetUsersApi.Tests.Controllers;
+
+///
+/// Unit tests for UsersController
+///
+public class UsersControllerTests
+{
+ private readonly Mock> _mockLogger;
+
+ public UsersControllerTests()
+ {
+ _mockLogger = new Mock>();
+ }
+
+ #region GetUsers Tests
+
+ [Fact]
+ public void GetUsers_HappyPath_ReturnsAllUsers()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+
+ // Act
+ var result = controller.GetUsers();
+
+ // Assert
+ result.Should().NotBeNull();
+ var okResult = result.Result.Should().BeOfType().Subject;
+ var users = okResult.Value.Should().BeAssignableTo>().Subject;
+ users.Should().NotBeNull();
+ }
+
+ [Fact]
+ public void GetUsers_LogsInformation()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+
+ // Act
+ 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")),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+
+ #endregion
+
+ #region GetUser Tests
+
+ [Fact]
+ public void GetUser_ValidId_ReturnsUser()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var testId = "1";
+
+ // Act
+ var result = controller.GetUser(testId);
+
+ // Assert
+ result.Should().NotBeNull();
+ var okResult = result.Result.Should().BeOfType().Subject;
+ var user = okResult.Value.Should().BeOfType().Subject;
+ user.Id.Should().Be(testId);
+ }
+
+ [Fact]
+ public void GetUser_InvalidId_ReturnsNotFound()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var invalidId = "999";
+
+ // Act
+ var result = controller.GetUser(invalidId);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Result.Should().BeOfType();
+ }
+
+ [Theory]
+ [InlineData("")]
+ public void GetUser_EmptyId_ReturnsNotFound(string id)
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+
+ // Act
+ var result = controller.GetUser(id);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Result.Should().BeOfType();
+ }
+
+ #endregion
+
+ #region CreateUser Tests
+
+ [Fact]
+ public void CreateUser_HappyPath_CreatesAndReturns201()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var newUser = new UserProfile
+ {
+ Id = "100",
+ FullName = "Test User",
+ Emoji = "๐งช"
+ };
+
+ // Act
+ var result = controller.CreateUser(newUser);
+
+ // Assert
+ result.Should().NotBeNull();
+ var createdResult = result.Result.Should().BeOfType().Subject;
+ createdResult.StatusCode.Should().Be(201);
+ var returnedUser = createdResult.Value.Should().BeOfType().Subject;
+ returnedUser.Id.Should().Be(newUser.Id);
+ returnedUser.FullName.Should().Be(newUser.FullName);
+ returnedUser.Emoji.Should().Be(newUser.Emoji);
+ }
+
+ [Fact]
+ public void CreateUser_NullUser_ReturnsBadRequest()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+
+ // Act
+ var result = controller.CreateUser(null!);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Result.Should().BeOfType();
+ }
+
+ #endregion
+
+ #region UpdateUser Tests
+
+ [Fact]
+ public void UpdateUser_HappyPath_UpdatesExistingUser()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var existingId = "1";
+ var updatedUser = new UserProfile
+ {
+ Id = existingId,
+ FullName = "Updated Name",
+ Emoji = "๐"
+ };
+
+ // Act
+ var result = controller.UpdateUser(existingId, updatedUser);
+
+ // Assert
+ result.Should().NotBeNull();
+ var okResult = result.Result.Should().BeOfType().Subject;
+ var returnedUser = okResult.Value.Should().BeOfType().Subject;
+ returnedUser.FullName.Should().Be("Updated Name");
+ returnedUser.Emoji.Should().Be("๐");
+ }
+
+ [Fact]
+ public void UpdateUser_NonExistentUser_ReturnsNotFound()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var nonExistentId = "999";
+ var updatedUser = new UserProfile
+ {
+ Id = nonExistentId,
+ FullName = "Test User",
+ Emoji = "๐งช"
+ };
+
+ // Act
+ var result = controller.UpdateUser(nonExistentId, updatedUser);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Result.Should().BeOfType();
+ }
+
+ [Fact]
+ public void UpdateUser_NullUser_ReturnsBadRequest()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var existingId = "1";
+
+ // Act
+ var result = controller.UpdateUser(existingId, null!);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Result.Should().BeOfType();
+ }
+
+ #endregion
+
+ #region DeleteUser Tests
+
+ [Fact]
+ public void DeleteUser_HappyPath_DeletesAndReturnsNoContent()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var existingId = "2";
+
+ // Act
+ var result = controller.DeleteUser(existingId);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().BeOfType();
+ }
+
+ [Fact]
+ public void DeleteUser_NonExistentUser_ReturnsNotFound()
+ {
+ // Arrange
+ var controller = new UsersController(_mockLogger.Object);
+ var nonExistentId = "999";
+
+ // Act
+ var result = controller.DeleteUser(nonExistentId);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Should().BeOfType();
+ }
+
+ #endregion
+}
diff --git a/net-users-api.Tests/IntegrationTests/CustomWebApplicationFactory.cs b/net-users-api.Tests/IntegrationTests/CustomWebApplicationFactory.cs
new file mode 100644
index 0000000..85c28f3
--- /dev/null
+++ b/net-users-api.Tests/IntegrationTests/CustomWebApplicationFactory.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.Hosting;
+
+namespace NetUsersApi.Tests.IntegrationTests;
+
+///
+/// Custom WebApplicationFactory for integration testing
+/// Configures the test server environment
+///
+public class CustomWebApplicationFactory : WebApplicationFactory
+{
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.ConfigureServices(services =>
+ {
+ // Add any test-specific service configurations here if needed
+ });
+
+ builder.UseEnvironment("Testing");
+ }
+}
diff --git a/net-users-api.Tests/IntegrationTests/UsersApiIntegrationTests.cs b/net-users-api.Tests/IntegrationTests/UsersApiIntegrationTests.cs
new file mode 100644
index 0000000..f90ca30
--- /dev/null
+++ b/net-users-api.Tests/IntegrationTests/UsersApiIntegrationTests.cs
@@ -0,0 +1,297 @@
+using System.Net;
+using System.Net.Http.Json;
+using FluentAssertions;
+using NetUsersApi.Models;
+
+namespace NetUsersApi.Tests.IntegrationTests;
+
+///
+/// Integration tests for the Users API
+/// Tests the full HTTP request/response cycle
+///
+public class UsersApiIntegrationTests : IClassFixture
+{
+ private readonly HttpClient _client;
+ private readonly CustomWebApplicationFactory _factory;
+
+ public UsersApiIntegrationTests(CustomWebApplicationFactory factory)
+ {
+ _factory = factory;
+ _client = factory.CreateClient();
+ }
+
+ #region GET /api/v1/users Tests
+
+ [Fact]
+ public async Task GetUsers_ReturnsSuccessStatusCode()
+ {
+ // Arrange & Act
+ var response = await _client.GetAsync("/api/v1/users");
+
+ // Assert
+ response.EnsureSuccessStatusCode();
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task GetUsers_ReturnsJsonContentType()
+ {
+ // Arrange & Act
+ var response = await _client.GetAsync("/api/v1/users");
+
+ // Assert
+ response.Content.Headers.ContentType?.MediaType.Should().Be("application/json");
+ }
+
+ [Fact]
+ public async Task GetUsers_ReturnsUserList()
+ {
+ // Arrange & Act
+ var response = await _client.GetAsync("/api/v1/users");
+ var users = await response.Content.ReadFromJsonAsync>();
+
+ // Assert
+ users.Should().NotBeNull();
+ users.Should().HaveCountGreaterThan(0);
+ }
+
+ #endregion
+
+ #region GET /api/v1/users/{id} Tests
+
+ [Fact]
+ public async Task GetUser_ValidId_ReturnsUser()
+ {
+ // Arrange
+ var userId = "1";
+
+ // Act
+ var response = await _client.GetAsync($"/api/v1/users/{userId}");
+ var user = await response.Content.ReadFromJsonAsync();
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ user.Should().NotBeNull();
+ user!.Id.Should().Be(userId);
+ user.FullName.Should().NotBeNullOrEmpty();
+ user.Emoji.Should().NotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public async Task GetUser_InvalidId_ReturnsNotFound()
+ {
+ // Arrange
+ var invalidUserId = "999";
+
+ // Act
+ var response = await _client.GetAsync($"/api/v1/users/{invalidUserId}");
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ #endregion
+
+ #region POST /api/v1/users Tests
+
+ [Fact]
+ public async Task CreateUser_ValidUser_ReturnsCreated()
+ {
+ // Arrange
+ var newUser = new UserProfile
+ {
+ Id = $"test-{Guid.NewGuid()}",
+ FullName = "Integration Test User",
+ Emoji = "๐งช"
+ };
+
+ // Act
+ var response = await _client.PostAsJsonAsync("/api/v1/users", newUser);
+ var createdUser = await response.Content.ReadFromJsonAsync();
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.Created);
+ response.Headers.Location.Should().NotBeNull();
+ createdUser.Should().NotBeNull();
+ createdUser!.Id.Should().Be(newUser.Id);
+ createdUser.FullName.Should().Be(newUser.FullName);
+ createdUser.Emoji.Should().Be(newUser.Emoji);
+ }
+
+ [Fact]
+ public async Task CreateUser_ValidUser_CanBeRetrieved()
+ {
+ // Arrange
+ var newUser = new UserProfile
+ {
+ Id = $"test-{Guid.NewGuid()}",
+ FullName = "Retrievable User",
+ Emoji = "๐"
+ };
+
+ // Act
+ var createResponse = await _client.PostAsJsonAsync("/api/v1/users", newUser);
+ createResponse.EnsureSuccessStatusCode();
+
+ var getResponse = await _client.GetAsync($"/api/v1/users/{newUser.Id}");
+ var retrievedUser = await getResponse.Content.ReadFromJsonAsync();
+
+ // Assert
+ getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
+ retrievedUser.Should().NotBeNull();
+ retrievedUser!.Id.Should().Be(newUser.Id);
+ retrievedUser.FullName.Should().Be(newUser.FullName);
+ }
+
+ [Fact]
+ public async Task CreateUser_NullUser_ReturnsBadRequest()
+ {
+ // Arrange & Act
+ var response = await _client.PostAsJsonAsync("/api/v1/users", (UserProfile?)null);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ }
+
+ #endregion
+
+ #region PUT /api/v1/users/{id} Tests
+
+ [Fact]
+ public async Task UpdateUser_ExistingUser_ReturnsOk()
+ {
+ // Arrange - Create a user first
+ var userId = $"test-{Guid.NewGuid()}";
+ var originalUser = new UserProfile
+ {
+ Id = userId,
+ FullName = "Original Name",
+ Emoji = "๐"
+ };
+ await _client.PostAsJsonAsync("/api/v1/users", originalUser);
+
+ var updatedUser = new UserProfile
+ {
+ Id = userId,
+ FullName = "Updated Name",
+ Emoji = "๐"
+ };
+
+ // Act
+ var response = await _client.PutAsJsonAsync($"/api/v1/users/{userId}", updatedUser);
+ var returnedUser = await response.Content.ReadFromJsonAsync();
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ returnedUser.Should().NotBeNull();
+ returnedUser!.FullName.Should().Be("Updated Name");
+ returnedUser.Emoji.Should().Be("๐");
+ }
+
+ [Fact]
+ public async Task UpdateUser_NonExistentUser_ReturnsNotFound()
+ {
+ // Arrange
+ var nonExistentId = "nonexistent-999";
+ var updatedUser = new UserProfile
+ {
+ Id = nonExistentId,
+ FullName = "Test User",
+ Emoji = "๐งช"
+ };
+
+ // Act
+ var response = await _client.PutAsJsonAsync($"/api/v1/users/{nonExistentId}", updatedUser);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ [Fact]
+ public async Task UpdateUser_NullUser_ReturnsBadRequest()
+ {
+ // Arrange
+ var userId = "1";
+
+ // Act
+ var response = await _client.PutAsJsonAsync($"/api/v1/users/{userId}", (UserProfile?)null);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ }
+
+ #endregion
+
+ #region DELETE /api/v1/users/{id} Tests
+
+ [Fact]
+ public async Task DeleteUser_ExistingUser_ReturnsNoContent()
+ {
+ // Arrange - Create a user first
+ var userId = $"test-{Guid.NewGuid()}";
+ var userToDelete = new UserProfile
+ {
+ Id = userId,
+ FullName = "User To Delete",
+ Emoji = "๐๏ธ"
+ };
+ await _client.PostAsJsonAsync("/api/v1/users", userToDelete);
+
+ // Act
+ var response = await _client.DeleteAsync($"/api/v1/users/{userId}");
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.NoContent);
+
+ // Verify user is actually deleted
+ var getResponse = await _client.GetAsync($"/api/v1/users/{userId}");
+ getResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ [Fact]
+ public async Task DeleteUser_NonExistentUser_ReturnsNotFound()
+ {
+ // Arrange
+ var nonExistentId = "nonexistent-999";
+
+ // Act
+ var response = await _client.DeleteAsync($"/api/v1/users/{nonExistentId}");
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+ }
+
+ #endregion
+
+ #region Test Isolation Tests
+
+ [Fact]
+ public async Task MultipleRequests_DontInterfere()
+ {
+ // Arrange
+ var user1Id = $"test-{Guid.NewGuid()}";
+ var user2Id = $"test-{Guid.NewGuid()}";
+
+ var user1 = new UserProfile { Id = user1Id, FullName = "User 1", Emoji = "1๏ธโฃ" };
+ var user2 = new UserProfile { Id = user2Id, FullName = "User 2", Emoji = "2๏ธโฃ" };
+
+ // Act
+ await _client.PostAsJsonAsync("/api/v1/users", user1);
+ await _client.PostAsJsonAsync("/api/v1/users", user2);
+
+ var response1 = await _client.GetAsync($"/api/v1/users/{user1Id}");
+ var response2 = await _client.GetAsync($"/api/v1/users/{user2Id}");
+
+ var retrievedUser1 = await response1.Content.ReadFromJsonAsync();
+ var retrievedUser2 = await response2.Content.ReadFromJsonAsync();
+
+ // Assert
+ retrievedUser1.Should().NotBeNull();
+ retrievedUser2.Should().NotBeNull();
+ retrievedUser1!.Id.Should().Be(user1Id);
+ retrievedUser2!.Id.Should().Be(user2Id);
+ retrievedUser1.FullName.Should().NotBe(retrievedUser2.FullName);
+ }
+
+ #endregion
+}
diff --git a/net-users-api.Tests/PropertyTests/UserProfilePropertyTests.cs b/net-users-api.Tests/PropertyTests/UserProfilePropertyTests.cs
new file mode 100644
index 0000000..e8d8b91
--- /dev/null
+++ b/net-users-api.Tests/PropertyTests/UserProfilePropertyTests.cs
@@ -0,0 +1,203 @@
+using System.Text.Json;
+using FsCheck;
+using FsCheck.Xunit;
+using FluentAssertions;
+using NetUsersApi.Models;
+
+namespace NetUsersApi.Tests.PropertyTests;
+
+///
+/// Property-based tests for UserProfile
+/// Uses FsCheck to generate random test cases and verify invariants
+///
+public class UserProfilePropertyTests
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ ///
+ /// Property: UserProfile serialization/deserialization roundtrip should preserve all data
+ /// Runs 100 test cases with random inputs
+ ///
+ [Property(MaxTest = 100)]
+ public void UserProfile_SerializationRoundtrip_PreservesData(NonEmptyString id, NonEmptyString fullName, NonEmptyString emoji)
+ {
+ // Arrange
+ var original = new UserProfile
+ {
+ Id = id.Get,
+ FullName = fullName.Get,
+ Emoji = emoji.Get
+ };
+
+ // Act
+ var json = JsonSerializer.Serialize(original);
+ var deserialized = JsonSerializer.Deserialize(json, JsonOptions);
+
+ // Assert
+ deserialized.Should().NotBeNull();
+ deserialized!.Id.Should().Be(original.Id);
+ deserialized.FullName.Should().Be(original.FullName);
+ deserialized.Emoji.Should().Be(original.Emoji);
+ }
+
+ ///
+ /// Property: UserProfile properties should never be null after construction
+ /// Runs 100 test cases with random inputs
+ ///
+ [Property(MaxTest = 100)]
+ public void UserProfile_RequiredProperties_AreNeverNull(NonEmptyString id, NonEmptyString fullName, NonEmptyString emoji)
+ {
+ // Arrange & Act
+ var profile = new UserProfile
+ {
+ Id = id.Get,
+ FullName = fullName.Get,
+ Emoji = emoji.Get
+ };
+
+ // Assert
+ profile.Id.Should().NotBeNull();
+ profile.FullName.Should().NotBeNull();
+ profile.Emoji.Should().NotBeNull();
+ }
+
+ ///
+ /// Property: Two UserProfiles with the same ID should be considered equal in a dictionary context
+ /// Runs 100 test cases with random inputs
+ ///
+ [Property(MaxTest = 100)]
+ public void UserProfile_SameId_CanBeUsedAsDictionaryKey(NonEmptyString id, NonEmptyString name1, NonEmptyString name2, NonEmptyString emoji)
+ {
+ // Arrange
+ var profile1 = new UserProfile { Id = id.Get, FullName = name1.Get, Emoji = emoji.Get };
+ var profile2 = new UserProfile { Id = id.Get, FullName = name2.Get, Emoji = emoji.Get };
+
+ var dictionary = new Dictionary
+ {
+ [profile1.Id] = profile1
+ };
+
+ // Act
+ dictionary[profile2.Id] = profile2;
+
+ // Assert
+ dictionary.Should().ContainKey(id.Get);
+ dictionary[id.Get].FullName.Should().Be(name2.Get);
+ dictionary.Should().HaveCount(1);
+ }
+
+ ///
+ /// Property: List operations should maintain correct count
+ /// Runs 100 test cases with random inputs
+ ///
+ [Property(MaxTest = 100)]
+ public void UserProfileList_AddRemove_MaintainsCorrectCount(NonEmptyString id, NonEmptyString fullName, NonEmptyString emoji)
+ {
+ // Arrange
+ var users = new List();
+ var user = new UserProfile
+ {
+ Id = id.Get,
+ FullName = fullName.Get,
+ Emoji = emoji.Get
+ };
+
+ // Act
+ users.Add(user);
+ var countAfterAdd = users.Count;
+
+ users.Remove(user);
+ var countAfterRemove = users.Count;
+
+ // Assert
+ countAfterAdd.Should().Be(1);
+ countAfterRemove.Should().Be(0);
+ }
+
+ ///
+ /// Property: Filtering by ID should return at most one user per unique ID
+ /// Runs 100 test cases with random inputs
+ ///
+ [Property(MaxTest = 100)]
+ public void UserProfileList_FilterById_ReturnsExpectedCount(NonEmptyString id, NonEmptyString name, NonEmptyString emoji)
+ {
+ // Arrange
+ var userList = new List
+ {
+ new() { Id = id.Get, FullName = name.Get, Emoji = emoji.Get }
+ };
+
+ // Act
+ var filtered = userList.Where(u => u.Id == id.Get).ToList();
+
+ // Assert
+ filtered.Should().HaveCount(1);
+ filtered[0].Id.Should().Be(id.Get);
+ }
+
+ ///
+ /// Property: Finding and updating a user preserves the ID
+ /// Runs 100 test cases with random inputs
+ ///
+ [Property(MaxTest = 100)]
+ public void UserProfileUpdate_PreservesId(NonEmptyString id, NonEmptyString originalName, NonEmptyString updatedName, NonEmptyString emoji)
+ {
+ // Arrange
+ var users = new List
+ {
+ new() { Id = id.Get, FullName = originalName.Get, Emoji = emoji.Get }
+ };
+
+ var updatedUser = new UserProfile
+ {
+ Id = id.Get,
+ FullName = updatedName.Get,
+ Emoji = emoji.Get
+ };
+
+ // Act
+ var index = users.FindIndex(u => u.Id == id.Get);
+ if (index >= 0)
+ {
+ users[index] = updatedUser;
+ }
+
+ // Assert
+ var foundUser = users.FirstOrDefault(u => u.Id == id.Get);
+ foundUser.Should().NotBeNull();
+ foundUser!.Id.Should().Be(id.Get);
+ foundUser.FullName.Should().Be(updatedName.Get);
+ }
+
+ ///
+ /// Property: JSON deserialization handles property name case insensitivity
+ /// Runs 100 test cases with random inputs
+ /// Uses proper JSON serialization to handle special characters
+ ///
+ [Property(MaxTest = 100)]
+ public void UserProfile_JsonDeserialization_IsCaseInsensitive(NonEmptyString id, NonEmptyString fullName, NonEmptyString emoji)
+ {
+ // Arrange - Create UserProfile objects and serialize them with different property casing
+ var profile = new UserProfile
+ {
+ Id = id.Get,
+ FullName = fullName.Get,
+ Emoji = emoji.Get
+ };
+
+ // Act - Serialize with default casing
+ var json = JsonSerializer.Serialize(profile);
+
+ // Deserialize with case-insensitive options
+ var deserialized = JsonSerializer.Deserialize(json, JsonOptions);
+
+ // Assert
+ deserialized.Should().NotBeNull();
+ deserialized!.Id.Should().Be(id.Get);
+ deserialized.FullName.Should().Be(fullName.Get);
+ deserialized.Emoji.Should().Be(emoji.Get);
+ }
+}
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..f66d509
--- /dev/null
+++ b/net-users-api.Tests/net-users-api.Tests.csproj
@@ -0,0 +1,30 @@
+๏ปฟ
+
+
+ net9.0
+ net_users_api.Tests
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/net-users-api/Controllers/UsersController.cs b/net-users-api/Controllers/UsersController.cs
index 2622279..50e282c 100644
--- a/net-users-api/Controllers/UsersController.cs
+++ b/net-users-api/Controllers/UsersController.cs
@@ -100,15 +100,22 @@ public ActionResult UpdateUser(string id, [FromBody] UserProfile up
}
///
- /// Delete a user by ID TODO
+ /// Delete a user by ID
///
/// User ID
/// No content or 404 if not found
[HttpDelete("{id}")]
public IActionResult DeleteUser(string id)
{
- // Implement delete functionality here using Copilot Ask or Edit mode
- throw new NotImplementedException("DeleteUser functionality not yet implemented");
+ var user = _users.FirstOrDefault(u => u.Id == id);
+
+ if (user == null)
+ {
+ return NotFound(new { error = "User not found" });
+ }
+
+ _users.Remove(user);
+ return NoContent();
}
///
diff --git a/net-users-api/Program.cs b/net-users-api/Program.cs
index 9ebf4d5..0de6862 100644
--- a/net-users-api/Program.cs
+++ b/net-users-api/Program.cs
@@ -39,3 +39,6 @@
Console.WriteLine("Starting server on :8080");
app.Run();
+
+// Make Program class accessible for integration testing
+public partial class Program { }
diff --git a/net-users-demo.sln b/net-users-demo.sln
index e9bb8f5..131e197 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", "{A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}"
+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
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Debug|x64.Build.0 = Debug|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Debug|x86.Build.0 = Debug|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Release|x64.ActiveCfg = Release|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Release|x64.Build.0 = Release|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Release|x86.ActiveCfg = Release|Any CPU
+ {A2C0E5CB-3069-4064-BF5D-06FB7D75C4E0}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal