diff --git a/src/Api/Data/StatusRepository.cs b/src/Api/Data/StatusRepository.cs index 71e6a08..2abecb1 100644 --- a/src/Api/Data/StatusRepository.cs +++ b/src/Api/Data/StatusRepository.cs @@ -80,9 +80,8 @@ public async Task>> GetAllAsync(CancellationToke .Limit(pageSize) .ToListAsync(cancellationToken); - return entities.Count > 0 - ? Result.Ok((Items: (IReadOnlyList)entities.Select(x => x.ToDto()).ToList(), Total: total)) - : Result.Fail<(IReadOnlyList Items, long Total)>("Statuses not found."); + IReadOnlyList items = entities.Select(x => x.ToDto()).ToList(); + return Result.Ok((items, total)); } /// diff --git a/tests/Api.Tests.Integration/Handlers/DeleteCategoryHandlerIntegrationTests.cs b/tests/Api.Tests.Integration/Handlers/DeleteCategoryHandlerIntegrationTests.cs new file mode 100644 index 0000000..50271cc --- /dev/null +++ b/tests/Api.Tests.Integration/Handlers/DeleteCategoryHandlerIntegrationTests.cs @@ -0,0 +1,127 @@ +// ======================================================= +// Copyright (c) 2026. All rights reserved. +// File Name : DeleteCategoryHandlerIntegrationTests.cs +// Company : mpaulosky +// Author : Matthew Paulosky +// Solution Name : IssueManager +// Project Name : Api.Tests.Integration +// ======================================================= + +namespace Integration.Handlers; + +/// +/// Integration tests for DeleteCategoryHandler (soft-delete via Archived) with a real MongoDB database. +/// +[Collection("CategoryIntegration")] +[ExcludeFromCodeCoverage] +public class DeleteCategoryHandlerIntegrationTests +{ + private readonly ICategoryRepository _repository; + private readonly DeleteCategoryHandler _handler; + + public DeleteCategoryHandlerIntegrationTests(MongoDbFixture fixture) + { + fixture.ThrowIfUnavailable(); + _repository = new CategoryRepository(fixture.ConnectionString, $"T{Guid.NewGuid():N}"); + _handler = new DeleteCategoryHandler(_repository); + } + + private static CategoryDto CreateTestCategoryDto(string name, string description = "Test description", bool archived = false) => + new(ObjectId.GenerateNewId(), name, description, DateTime.UtcNow, null, archived, UserDto.Empty); + + [Fact] + public async Task Handle_ValidCategory_SetsArchivedInDatabase() + { + // Arrange - Create a category + var category = CreateTestCategoryDto("Category to Delete", "This will be archived"); + var created = await _repository.CreateAsync(category, TestContext.Current.CancellationToken); + + var command = new DeleteCategoryCommand { Id = created.Value!.Id }; + + // Act + var result = await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + + // Verify Archived is set in the database + var getResult = await _repository.GetByIdAsync(created.Value.Id, TestContext.Current.CancellationToken); + getResult.Should().NotBeNull(); + getResult.Value?.Archived.Should().BeTrue(); + } + + [Fact] + public async Task Handle_NonExistentCategory_ReturnsNotFoundFailure() + { + // Arrange + var nonExistentId = ObjectId.GenerateNewId(); + var command = new DeleteCategoryCommand { Id = nonExistentId }; + + // Act + var result = await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(ResultErrorCode.NotFound); + } + + [Fact] + public async Task Handle_AlreadyArchivedCategory_IsIdempotent() + { + // Arrange - Create an already archived category + var archivedCategory = CreateTestCategoryDto("Already Archived", "Already archived", archived: true); + var created = await _repository.CreateAsync(archivedCategory, TestContext.Current.CancellationToken); + + var command = new DeleteCategoryCommand { Id = created.Value!.Id }; + + // Act - Delete already archived category (should be idempotent) + var result = await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert - Should still return true + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + + var dbCategoryResult = await _repository.GetByIdAsync(created.Value.Id, TestContext.Current.CancellationToken); + dbCategoryResult.Should().NotBeNull(); + dbCategoryResult.Value?.Archived.Should().BeTrue(); + } + + [Fact] + public async Task Handle_CategoryNotDeleted_RecordStillExists() + { + // Arrange - Create a category + var category = CreateTestCategoryDto("Category to Archive", "Should still exist in DB"); + var created = await _repository.CreateAsync(category, TestContext.Current.CancellationToken); + + var command = new DeleteCategoryCommand { Id = created.Value!.Id }; + + // Act - Soft delete + await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert - Record should still exist (soft delete) + var dbCategory = await _repository.GetByIdAsync(created.Value.Id, TestContext.Current.CancellationToken); + dbCategory.Should().NotBeNull(); + dbCategory.Value?.Id.Should().Be(created.Value.Id); + dbCategory.Value?.Archived.Should().BeTrue(); + } + + [Fact] + public async Task Handle_CreatedAndDeletedCategory_NotReturnedInList() + { + // Arrange - Create a category via repository + var category = CreateTestCategoryDto("Category for List Test", "Will be archived"); + var created = await _repository.CreateAsync(category, TestContext.Current.CancellationToken); + created.Value.Should().NotBeNull(); + + var command = new DeleteCategoryCommand { Id = created.Value!.Id }; + + // Act - Archive the category + await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert - GetAll (paginated) should exclude archived categories + var result = await _repository.GetAllAsync(1, 100, TestContext.Current.CancellationToken); + var allCategories = result.Value.Items; + allCategories.Should().NotContain(c => c.Id == created.Value.Id); + } +} diff --git a/tests/Api.Tests.Integration/Handlers/DeleteStatusHandlerIntegrationTests.cs b/tests/Api.Tests.Integration/Handlers/DeleteStatusHandlerIntegrationTests.cs new file mode 100644 index 0000000..af59cf3 --- /dev/null +++ b/tests/Api.Tests.Integration/Handlers/DeleteStatusHandlerIntegrationTests.cs @@ -0,0 +1,127 @@ +// ======================================================= +// Copyright (c) 2026. All rights reserved. +// File Name : DeleteStatusHandlerIntegrationTests.cs +// Company : mpaulosky +// Author : Matthew Paulosky +// Solution Name : IssueManager +// Project Name : Api.Tests.Integration +// ======================================================= + +namespace Integration.Handlers; + +/// +/// Integration tests for DeleteStatusHandler (soft-delete via Archived) with a real MongoDB database. +/// +[Collection("StatusIntegration")] +[ExcludeFromCodeCoverage] +public class DeleteStatusHandlerIntegrationTests +{ + private readonly IStatusRepository _repository; + private readonly DeleteStatusHandler _handler; + + public DeleteStatusHandlerIntegrationTests(MongoDbFixture fixture) + { + fixture.ThrowIfUnavailable(); + _repository = new StatusRepository(fixture.ConnectionString, $"T{Guid.NewGuid():N}"); + _handler = new DeleteStatusHandler(_repository); + } + + private static StatusDto CreateTestStatusDto(string name, string description = "Test description", bool archived = false) => + new(ObjectId.GenerateNewId(), name, description, DateTime.UtcNow, null, archived, UserDto.Empty); + + [Fact] + public async Task Handle_ValidStatus_SetsArchivedInDatabase() + { + // Arrange - Create a status + var status = CreateTestStatusDto("Status to Delete", "This will be archived"); + var created = await _repository.CreateAsync(status, TestContext.Current.CancellationToken); + + var command = new DeleteStatusCommand { Id = created.Value!.Id }; + + // Act + var result = await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + + // Verify Archived is set in the database + var getResult = await _repository.GetByIdAsync(created.Value.Id, TestContext.Current.CancellationToken); + getResult.Should().NotBeNull(); + getResult.Value?.Archived.Should().BeTrue(); + } + + [Fact] + public async Task Handle_NonExistentStatus_ReturnsNotFoundFailure() + { + // Arrange + var nonExistentId = ObjectId.GenerateNewId(); + var command = new DeleteStatusCommand { Id = nonExistentId }; + + // Act + var result = await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(ResultErrorCode.NotFound); + } + + [Fact] + public async Task Handle_AlreadyArchivedStatus_IsIdempotent() + { + // Arrange - Create an already archived status + var archivedStatus = CreateTestStatusDto("Already Archived", "Already archived", archived: true); + var created = await _repository.CreateAsync(archivedStatus, TestContext.Current.CancellationToken); + + var command = new DeleteStatusCommand { Id = created.Value!.Id }; + + // Act - Delete already archived status (should be idempotent) + var result = await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert - Should still return true + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + + var dbStatusResult = await _repository.GetByIdAsync(created.Value.Id, TestContext.Current.CancellationToken); + dbStatusResult.Should().NotBeNull(); + dbStatusResult.Value?.Archived.Should().BeTrue(); + } + + [Fact] + public async Task Handle_StatusNotDeleted_RecordStillExists() + { + // Arrange - Create a status + var status = CreateTestStatusDto("Status to Archive", "Should still exist in DB"); + var created = await _repository.CreateAsync(status, TestContext.Current.CancellationToken); + + var command = new DeleteStatusCommand { Id = created.Value!.Id }; + + // Act - Soft delete + await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert - Record should still exist (soft delete) + var dbStatus = await _repository.GetByIdAsync(created.Value.Id, TestContext.Current.CancellationToken); + dbStatus.Should().NotBeNull(); + dbStatus.Value?.Id.Should().Be(created.Value.Id); + dbStatus.Value?.Archived.Should().BeTrue(); + } + + [Fact] + public async Task Handle_CreatedAndDeletedStatus_NotReturnedInList() + { + // Arrange - Create a status via repository + var status = CreateTestStatusDto("Status for List Test", "Will be archived"); + var created = await _repository.CreateAsync(status, TestContext.Current.CancellationToken); + created.Value.Should().NotBeNull(); + + var command = new DeleteStatusCommand { Id = created.Value!.Id }; + + // Act - Archive the status + await _handler.Handle(command, TestContext.Current.CancellationToken); + + // Assert - GetAll (paginated) should exclude archived statuses + var result = await _repository.GetAllAsync(1, 100, TestContext.Current.CancellationToken); + var allStatuses = result.Value.Items; + allStatuses.Should().NotContain(s => s.Id == created.Value.Id); + } +} diff --git a/tests/Api.Tests.Unit/Endpoints/CategoryEndpointsTests.cs b/tests/Api.Tests.Unit/Endpoints/CategoryEndpointsTests.cs index 87fdb08..b92e039 100644 --- a/tests/Api.Tests.Unit/Endpoints/CategoryEndpointsTests.cs +++ b/tests/Api.Tests.Unit/Endpoints/CategoryEndpointsTests.cs @@ -181,4 +181,60 @@ public async Task UpdateCategory_WithoutAuthentication_ReturnsUnauthorized() response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } + [Fact] + public async Task DeleteCategory_WithValidId_ReturnsNoContent() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + var categoryDto = new CategoryDto( + categoryId, + "Test Category", + "Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + _factory.CategoryRepository + .GetByIdAsync(Arg.Any(), Arg.Any()) + .Returns(Result.Ok(categoryDto)); + _factory.CategoryRepository + .ArchiveAsync(Arg.Any(), Arg.Any()) + .Returns(Result.Ok()); + + // Act + var response = await _authenticatedClient.DeleteAsync($"/api/v1/categories/{categoryId}").ConfigureAwait(false); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + } + + [Fact] + public async Task DeleteCategory_WithoutAuthentication_ReturnsUnauthorized() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + + // Act + var response = await _client.DeleteAsync($"/api/v1/categories/{categoryId}").ConfigureAwait(false); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task DeleteCategory_NotFound_Returns404() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + _factory.CategoryRepository + .GetByIdAsync(Arg.Any(), Arg.Any()) + .Returns(Result.Fail("Not found")); + + // Act + var response = await _authenticatedClient.DeleteAsync($"/api/v1/categories/{categoryId}").ConfigureAwait(false); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + } diff --git a/tests/Api.Tests.Unit/Endpoints/StatusEndpointsTests.cs b/tests/Api.Tests.Unit/Endpoints/StatusEndpointsTests.cs index d62acb6..38cef52 100644 --- a/tests/Api.Tests.Unit/Endpoints/StatusEndpointsTests.cs +++ b/tests/Api.Tests.Unit/Endpoints/StatusEndpointsTests.cs @@ -181,4 +181,60 @@ public async Task UpdateStatus_WithoutAuthentication_ReturnsUnauthorized() response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } + [Fact] + public async Task DeleteStatus_WithValidId_ReturnsNoContent() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + var statusDto = new StatusDto( + statusId, + "Test Status", + "Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + _factory.StatusRepository + .GetByIdAsync(Arg.Any(), Arg.Any()) + .Returns(Result.Ok(statusDto)); + _factory.StatusRepository + .ArchiveAsync(Arg.Any(), Arg.Any()) + .Returns(Result.Ok()); + + // Act + var response = await _authenticatedClient.DeleteAsync($"/api/v1/statuses/{statusId}", TestContext.Current.CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + } + + [Fact] + public async Task DeleteStatus_WithoutAuthentication_ReturnsUnauthorized() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + + // Act + var response = await _client.DeleteAsync($"/api/v1/statuses/{statusId}", TestContext.Current.CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task DeleteStatus_NotFound_Returns404() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + _factory.StatusRepository + .GetByIdAsync(Arg.Any(), Arg.Any()) + .Returns(Result.Fail("Not found")); + + // Act + var response = await _authenticatedClient.DeleteAsync($"/api/v1/statuses/{statusId}", TestContext.Current.CancellationToken); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + } diff --git a/tests/Api.Tests.Unit/Extensions/ServiceCollectionExtensionsTests.cs b/tests/Api.Tests.Unit/Extensions/ServiceCollectionExtensionsTests.cs index 5e57208..308e88c 100644 --- a/tests/Api.Tests.Unit/Extensions/ServiceCollectionExtensionsTests.cs +++ b/tests/Api.Tests.Unit/Extensions/ServiceCollectionExtensionsTests.cs @@ -156,11 +156,13 @@ public void AddHandlers_RegistersAllHandlers() services.Should().Contain(sd => sd.ServiceType == typeof(GetCategoryHandler)); services.Should().Contain(sd => sd.ServiceType == typeof(ListCategoriesHandler)); services.Should().Contain(sd => sd.ServiceType == typeof(UpdateCategoryHandler)); + services.Should().Contain(sd => sd.ServiceType == typeof(DeleteCategoryHandler)); services.Should().Contain(sd => sd.ServiceType == typeof(CreateCommentHandler)); services.Should().Contain(sd => sd.ServiceType == typeof(GetCommentHandler)); services.Should().Contain(sd => sd.ServiceType == typeof(ListCommentsHandler)); services.Should().Contain(sd => sd.ServiceType == typeof(UpdateCommentHandler)); services.Should().Contain(sd => sd.ServiceType == typeof(DeleteCommentHandler)); + services.Should().Contain(sd => sd.ServiceType == typeof(DeleteStatusHandler)); } [Fact] diff --git a/tests/Api.Tests.Unit/Handlers/Categories/DeleteCategoryHandlerTests.cs b/tests/Api.Tests.Unit/Handlers/Categories/DeleteCategoryHandlerTests.cs new file mode 100644 index 0000000..ecbeda7 --- /dev/null +++ b/tests/Api.Tests.Unit/Handlers/Categories/DeleteCategoryHandlerTests.cs @@ -0,0 +1,182 @@ +// ======================================================= +// Copyright (c) 2026. All rights reserved. +// File Name : DeleteCategoryHandlerTests.cs +// Company : mpaulosky +// Author : Matthew Paulosky +// Solution Name : IssueManager +// Project Name : Api.Tests.Unit +// ======================================================= + +using Api.Data.Interfaces; + +namespace Api.Handlers.Categories; + +/// +/// Unit tests for DeleteCategoryHandler (soft-delete via Archived). +/// +[ExcludeFromCodeCoverage] +public class DeleteCategoryHandlerTests +{ + private readonly ICategoryRepository _repository; + private readonly DeleteCategoryHandler _handler; + + public DeleteCategoryHandlerTests() + { + _repository = Substitute.For(); + _handler = new DeleteCategoryHandler(_repository); + } + + [Fact] + public async Task Handle_ValidCategory_SetsIsArchivedToTrue() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + var category = new CategoryDto( + categoryId, + "Test Category", + "Test Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + var command = new DeleteCategoryCommand { Id = categoryId }; + + _repository.GetByIdAsync(categoryId, Arg.Any()) + .Returns(Result.Ok(category)); + + _repository.ArchiveAsync(categoryId, Arg.Any()) + .Returns(Result.Ok()); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + await _repository.Received(1).GetByIdAsync(categoryId, Arg.Any()); + await _repository.Received(1).ArchiveAsync(categoryId, Arg.Any()); + } + + [Fact] + public async Task Handle_NonExistentCategory_ReturnsNotFoundResult() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + var command = new DeleteCategoryCommand { Id = categoryId }; + + _repository.GetByIdAsync(categoryId, Arg.Any()) + .Returns(Result.Fail("Not found")); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(ResultErrorCode.NotFound); + } + + [Fact] + public async Task Handle_AlreadyArchivedCategory_IsIdempotent() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + var archivedCategory = new CategoryDto( + categoryId, + "Archived Category", + "Already archived", + DateTime.UtcNow, + null, + true, + UserDto.Empty); + + var command = new DeleteCategoryCommand { Id = categoryId }; + + _repository.GetByIdAsync(categoryId, Arg.Any()) + .Returns(Result.Ok(archivedCategory)); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + await _repository.Received(1).GetByIdAsync(categoryId, Arg.Any()); + await _repository.DidNotReceive().ArchiveAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Handle_EmptyId_ReturnsValidationFailure() + { + // Arrange + var command = new DeleteCategoryCommand { Id = ObjectId.Empty }; + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(ResultErrorCode.Validation); + } + + [Fact] + public async Task Handle_RepositoryArchiveFails_ReturnsFailure() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + var category = new CategoryDto( + categoryId, + "Test Category", + "Test Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + var command = new DeleteCategoryCommand { Id = categoryId }; + + _repository.GetByIdAsync(categoryId, Arg.Any()) + .Returns(Result.Ok(category)); + + _repository.ArchiveAsync(categoryId, Arg.Any()) + .Returns(Result.Fail("Archive failed")); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeFalse(); + result.Error.Should().Be("Archive failed"); + } + + [Fact] + public async Task Handle_ValidCategory_PassesCancellationToken() + { + // Arrange + var categoryId = ObjectId.GenerateNewId(); + var cancellationToken = new CancellationToken(); + var category = new CategoryDto( + categoryId, + "Test Category", + "Test Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + var command = new DeleteCategoryCommand { Id = categoryId }; + + _repository.GetByIdAsync(categoryId, Arg.Any()) + .Returns(Result.Ok(category)); + + _repository.ArchiveAsync(categoryId, Arg.Any()) + .Returns(Result.Ok()); + + // Act + await _handler.Handle(command, cancellationToken); + + // Assert + await _repository.Received(1).GetByIdAsync(categoryId, Arg.Any()); + await _repository.Received(1).ArchiveAsync(categoryId, Arg.Any()); + } +} diff --git a/tests/Api.Tests.Unit/Handlers/Statuses/DeleteStatusHandlerTests.cs b/tests/Api.Tests.Unit/Handlers/Statuses/DeleteStatusHandlerTests.cs new file mode 100644 index 0000000..de99822 --- /dev/null +++ b/tests/Api.Tests.Unit/Handlers/Statuses/DeleteStatusHandlerTests.cs @@ -0,0 +1,182 @@ +// ======================================================= +// Copyright (c) 2026. All rights reserved. +// File Name : DeleteStatusHandlerTests.cs +// Company : mpaulosky +// Author : Matthew Paulosky +// Solution Name : IssueManager +// Project Name : Api.Tests.Unit +// ======================================================= + +using Api.Data.Interfaces; + +namespace Api.Handlers.Statuses; + +/// +/// Unit tests for DeleteStatusHandler (soft-delete via Archived). +/// +[ExcludeFromCodeCoverage] +public class DeleteStatusHandlerTests +{ + private readonly IStatusRepository _repository; + private readonly DeleteStatusHandler _handler; + + public DeleteStatusHandlerTests() + { + _repository = Substitute.For(); + _handler = new DeleteStatusHandler(_repository); + } + + [Fact] + public async Task Handle_ValidStatus_SetsIsArchivedToTrue() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + var status = new StatusDto( + statusId, + "Test Status", + "Test Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + var command = new DeleteStatusCommand { Id = statusId }; + + _repository.GetByIdAsync(statusId, Arg.Any()) + .Returns(Result.Ok(status)); + + _repository.ArchiveAsync(statusId, Arg.Any()) + .Returns(Result.Ok()); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + await _repository.Received(1).GetByIdAsync(statusId, Arg.Any()); + await _repository.Received(1).ArchiveAsync(statusId, Arg.Any()); + } + + [Fact] + public async Task Handle_NonExistentStatus_ReturnsNotFoundResult() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + var command = new DeleteStatusCommand { Id = statusId }; + + _repository.GetByIdAsync(statusId, Arg.Any()) + .Returns(Result.Fail("Not found")); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(ResultErrorCode.NotFound); + } + + [Fact] + public async Task Handle_AlreadyArchivedStatus_IsIdempotent() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + var archivedStatus = new StatusDto( + statusId, + "Archived Status", + "Already archived", + DateTime.UtcNow, + null, + true, + UserDto.Empty); + + var command = new DeleteStatusCommand { Id = statusId }; + + _repository.GetByIdAsync(statusId, Arg.Any()) + .Returns(Result.Ok(archivedStatus)); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeTrue(); + result.Value.Should().BeTrue(); + await _repository.Received(1).GetByIdAsync(statusId, Arg.Any()); + await _repository.DidNotReceive().ArchiveAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Handle_EmptyId_ReturnsValidationFailure() + { + // Arrange + var command = new DeleteStatusCommand { Id = ObjectId.Empty }; + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(ResultErrorCode.Validation); + } + + [Fact] + public async Task Handle_RepositoryArchiveFails_ReturnsFailure() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + var status = new StatusDto( + statusId, + "Test Status", + "Test Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + var command = new DeleteStatusCommand { Id = statusId }; + + _repository.GetByIdAsync(statusId, Arg.Any()) + .Returns(Result.Ok(status)); + + _repository.ArchiveAsync(statusId, Arg.Any()) + .Returns(Result.Fail("Archive failed")); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Success.Should().BeFalse(); + result.Error.Should().Be("Archive failed"); + } + + [Fact] + public async Task Handle_ValidStatus_PassesCancellationToken() + { + // Arrange + var statusId = ObjectId.GenerateNewId(); + var cancellationToken = new CancellationToken(); + var status = new StatusDto( + statusId, + "Test Status", + "Test Description", + DateTime.UtcNow, + null, + false, + UserDto.Empty); + + var command = new DeleteStatusCommand { Id = statusId }; + + _repository.GetByIdAsync(statusId, Arg.Any()) + .Returns(Result.Ok(status)); + + _repository.ArchiveAsync(statusId, Arg.Any()) + .Returns(Result.Ok()); + + // Act + await _handler.Handle(command, cancellationToken); + + // Assert + await _repository.Received(1).GetByIdAsync(statusId, Arg.Any()); + await _repository.Received(1).ArchiveAsync(statusId, Arg.Any()); + } +}