Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/Api/Data/StatusRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ public async Task<Result<IReadOnlyList<StatusDto>>> GetAllAsync(CancellationToke
.Limit(pageSize)
.ToListAsync(cancellationToken);

return entities.Count > 0
? Result.Ok((Items: (IReadOnlyList<StatusDto>)entities.Select(x => x.ToDto()).ToList(), Total: total))
: Result.Fail<(IReadOnlyList<StatusDto> Items, long Total)>("Statuses not found.");
IReadOnlyList<StatusDto> items = entities.Select(x => x.ToDto()).ToList();
return Result.Ok((items, total));
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -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
// =======================================================
Comment on lines +1 to +8

namespace Integration.Handlers;

/// <summary>
/// Integration tests for DeleteCategoryHandler (soft-delete via Archived) with a real MongoDB database.
/// </summary>
[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);
}
}
Original file line number Diff line number Diff line change
@@ -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
// =======================================================
Comment on lines +1 to +8

namespace Integration.Handlers;

/// <summary>
/// Integration tests for DeleteStatusHandler (soft-delete via Archived) with a real MongoDB database.
/// </summary>
[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);
}
}
56 changes: 56 additions & 0 deletions tests/Api.Tests.Unit/Endpoints/CategoryEndpointsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ObjectId>(), Arg.Any<CancellationToken>())
.Returns(Result<CategoryDto>.Ok(categoryDto));
_factory.CategoryRepository
.ArchiveAsync(Arg.Any<ObjectId>(), Arg.Any<CancellationToken>())
.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<ObjectId>(), Arg.Any<CancellationToken>())
.Returns(Result<CategoryDto>.Fail("Not found"));

// Act
var response = await _authenticatedClient.DeleteAsync($"/api/v1/categories/{categoryId}").ConfigureAwait(false);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}

}
56 changes: 56 additions & 0 deletions tests/Api.Tests.Unit/Endpoints/StatusEndpointsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ObjectId>(), Arg.Any<CancellationToken>())
.Returns(Result.Ok(statusDto));
_factory.StatusRepository
.ArchiveAsync(Arg.Any<ObjectId>(), Arg.Any<CancellationToken>())
.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<ObjectId>(), Arg.Any<CancellationToken>())
.Returns(Result<StatusDto>.Fail("Not found"));

// Act
var response = await _authenticatedClient.DeleteAsync($"/api/v1/statuses/{statusId}", TestContext.Current.CancellationToken);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}

}
Loading
Loading