KoogleApp/test/Koogle.Tests/Unit/Services/BookingCategoryServiceTests.cs

414 lines
13 KiB
C#

using AutoMapper;
using Koogle.Application.DTOs;
using Koogle.Application.Services;
using Koogle.Domain.Entities;
using Koogle.Domain.Enums;
using Koogle.Domain.Interfaces;
using Moq;
using FluentAssertions;
using Koogle.Application.Interfaces;
namespace Koogle.Tests.Unit.Services;
/// <summary>
/// Unit tests for BookingCategoryService.
/// </summary>
public class BookingCategoryServiceTests
{
private readonly Mock<IBookingCategoryRepository> _repositoryMock;
private readonly Mock<ICurrentClubContext> _clubContextMock;
private readonly Mock<IMapper> _mapperMock;
private readonly BookingCategoryService _sut;
private readonly Guid _clubId = Guid.NewGuid();
public BookingCategoryServiceTests()
{
_repositoryMock = new Mock<IBookingCategoryRepository>();
_clubContextMock = new Mock<ICurrentClubContext>();
_mapperMock = new Mock<IMapper>();
_clubContextMock.Setup(c => c.ClubId).Returns(_clubId);
SetupMapper();
_sut = new BookingCategoryService(
_repositoryMock.Object,
_clubContextMock.Object,
_mapperMock.Object);
}
private void SetupMapper()
{
_mapperMock.Setup(m => m.Map<BookingCategoryDto>(It.IsAny<BookingCategory>()))
.Returns((BookingCategory c) => new BookingCategoryDto
{
Id = c.Id,
Name = c.Name,
Description = c.Description,
CategoryType = c.CategoryType,
IsSystemCategory = c.IsSystemCategory,
Color = c.Color,
Icon = c.Icon,
IsActive = c.IsActive
});
_mapperMock.Setup(m => m.Map<List<BookingCategoryDto>>(It.IsAny<List<BookingCategory>>()))
.Returns((List<BookingCategory> cats) => cats.Select(c => new BookingCategoryDto
{
Id = c.Id,
Name = c.Name,
Description = c.Description,
CategoryType = c.CategoryType,
IsSystemCategory = c.IsSystemCategory,
Color = c.Color,
Icon = c.Icon,
IsActive = c.IsActive
}).ToList());
}
#region GetAllAsync Tests
[Fact]
public async Task GetAllAsync_ReturnsEmptyList_WhenNoCategoriesExist()
{
// Arrange
_repositoryMock.Setup(r => r.GetByClubIdAsync(_clubId, false, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
// Act
var result = await _sut.GetAllAsync();
// Assert
result.Should().BeEmpty();
}
[Fact]
public async Task GetAllAsync_ReturnsAllCategories_WhenCategoriesExist()
{
// Arrange
var categories = new List<BookingCategory>
{
CreateCategory("Spielstrafe"),
CreateCategory("Mitgliedsbeitrag"),
CreateCategory("Sonstige Einnahmen")
};
_repositoryMock.Setup(r => r.GetByClubIdAsync(_clubId, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(categories);
// Act
var result = await _sut.GetAllAsync();
// Assert
result.Should().HaveCount(3);
}
[Fact]
public async Task GetAllAsync_IncludesInactive_WhenRequested()
{
// Arrange
_repositoryMock.Setup(r => r.GetByClubIdAsync(_clubId, true, It.IsAny<CancellationToken>()))
.ReturnsAsync([]);
// Act
await _sut.GetAllAsync(includeInactive: true);
// Assert
_repositoryMock.Verify(r => r.GetByClubIdAsync(_clubId, true, It.IsAny<CancellationToken>()), Times.Once);
}
#endregion
#region GetByIdAsync Tests
[Fact]
public async Task GetByIdAsync_ReturnsNull_WhenCategoryNotFound()
{
// Arrange
var id = Guid.NewGuid();
_repositoryMock.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory?)null);
// Act
var result = await _sut.GetByIdAsync(id);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetByIdAsync_ReturnsNull_WhenCategoryBelongsToDifferentClub()
{
// Arrange
var category = CreateCategory("Test", clubId: Guid.NewGuid());
_repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(category);
// Act
var result = await _sut.GetByIdAsync(category.Id);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetByIdAsync_ReturnsCategory_WhenExists()
{
// Arrange
var category = CreateCategory("Test");
_repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(category);
// Act
var result = await _sut.GetByIdAsync(category.Id);
// Assert
result.Should().NotBeNull();
result!.Name.Should().Be("Test");
}
#endregion
#region CreateAsync Tests
[Fact]
public async Task CreateAsync_CreatesCategory_WithCorrectData()
{
// Arrange
var dto = new CreateBookingCategoryDto
{
Name = "Neue Kategorie",
Description = "Beschreibung",
CategoryType = BookingCategoryType.Income,
Color = "#FF0000",
Icon = "Star"
};
BookingCategory? captured = null;
_repositoryMock.Setup(r => r.AddAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()))
.Callback<BookingCategory, CancellationToken>((c, _) => captured = c)
.ReturnsAsync((BookingCategory c, CancellationToken _) => c);
// Act
var result = await _sut.CreateAsync(dto);
// Assert
captured.Should().NotBeNull();
captured!.Name.Should().Be("Neue Kategorie");
captured.ClubId.Should().Be(_clubId);
captured.IsSystemCategory.Should().BeFalse();
captured.IsActive.Should().BeTrue();
}
[Fact]
public async Task CreateAsync_SetsCreatedAt()
{
// Arrange
var dto = new CreateBookingCategoryDto
{
Name = "Test",
CategoryType = BookingCategoryType.Expense
};
var beforeCreate = DateTime.UtcNow;
BookingCategory? captured = null;
_repositoryMock.Setup(r => r.AddAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()))
.Callback<BookingCategory, CancellationToken>((c, _) => captured = c)
.ReturnsAsync((BookingCategory c, CancellationToken _) => c);
// Act
await _sut.CreateAsync(dto);
var afterCreate = DateTime.UtcNow;
// Assert
captured!.CreatedAt.Should().BeOnOrAfter(beforeCreate);
captured.CreatedAt.Should().BeOnOrBefore(afterCreate);
}
#endregion
#region UpdateAsync Tests
[Fact]
public async Task UpdateAsync_ThrowsException_WhenCategoryNotFound()
{
// Arrange
var dto = new UpdateBookingCategoryDto
{
Id = Guid.NewGuid(),
Name = "Updated",
IsActive = true
};
_repositoryMock.Setup(r => r.GetByIdAsync(dto.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory?)null);
// Act
var act = () => _sut.UpdateAsync(dto);
// Assert
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage($"Category with id {dto.Id} not found.");
}
[Fact]
public async Task UpdateAsync_ThrowsException_WhenCategoryBelongsToDifferentClub()
{
// Arrange
var category = CreateCategory("Test", clubId: Guid.NewGuid());
var dto = new UpdateBookingCategoryDto
{
Id = category.Id,
Name = "Updated",
IsActive = true
};
_repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(category);
// Act
var act = () => _sut.UpdateAsync(dto);
// Assert
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("Category does not belong to current club.");
}
[Fact]
public async Task UpdateAsync_UpdatesCategory_Correctly()
{
// Arrange
var category = CreateCategory("Original");
var dto = new UpdateBookingCategoryDto
{
Id = category.Id,
Name = "Updated",
Description = "New Desc",
Color = "#00FF00",
Icon = "Edit",
IsActive = false
};
_repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(category);
_repositoryMock.Setup(r => r.UpdateAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory c, CancellationToken _) => c);
// Act
var result = await _sut.UpdateAsync(dto);
// Assert
result.Name.Should().Be("Updated");
}
#endregion
#region DeleteAsync Tests
[Fact]
public async Task DeleteAsync_ReturnsFalse_WhenCategoryNotFound()
{
// Arrange
var id = Guid.NewGuid();
_repositoryMock.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory?)null);
// Act
var result = await _sut.DeleteAsync(id);
// Assert
result.Should().BeFalse();
}
[Fact]
public async Task DeleteAsync_ThrowsException_WhenSystemCategory()
{
// Arrange
var category = CreateCategory("Spielstrafe", isSystem: true);
_repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(category);
// Act
var act = () => _sut.DeleteAsync(category.Id);
// Assert
await act.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("System categories cannot be deleted.");
}
[Fact]
public async Task DeleteAsync_SoftDeletes_WhenNonSystemCategory()
{
// Arrange
var category = CreateCategory("Custom", isSystem: false);
_repositoryMock.Setup(r => r.GetByIdAsync(category.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(category);
_repositoryMock.Setup(r => r.UpdateAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory c, CancellationToken _) => c);
// Act
var result = await _sut.DeleteAsync(category.Id);
// Assert
result.Should().BeTrue();
category.IsDeleted.Should().BeTrue();
}
#endregion
#region EnsureSystemCategoriesAsync Tests
[Fact]
public async Task EnsureSystemCategoriesAsync_CreatesAllSystemCategories_WhenNoneExist()
{
// Arrange
_repositoryMock.Setup(r => r.GetSystemCategoryAsync(_clubId, It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory?)null);
_repositoryMock.Setup(r => r.AddAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory c, CancellationToken _) => c);
// Act
await _sut.EnsureSystemCategoriesAsync();
// Assert
_repositoryMock.Verify(r => r.AddAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()), Times.Exactly(4));
}
[Fact]
public async Task EnsureSystemCategoriesAsync_SkipsExisting_Categories()
{
// Arrange
var existing = CreateCategory("Spielstrafe", isSystem: true);
_repositoryMock.Setup(r => r.GetSystemCategoryAsync(_clubId, "Spielstrafe", It.IsAny<CancellationToken>()))
.ReturnsAsync(existing);
_repositoryMock.Setup(r => r.GetSystemCategoryAsync(_clubId, It.Is<string>(s => s != "Spielstrafe"), It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory?)null);
_repositoryMock.Setup(r => r.AddAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((BookingCategory c, CancellationToken _) => c);
// Act
await _sut.EnsureSystemCategoriesAsync();
// Assert
_repositoryMock.Verify(r => r.AddAsync(It.IsAny<BookingCategory>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
}
#endregion
#region Helper Methods
private BookingCategory CreateCategory(
string name,
Guid? clubId = null,
bool isSystem = false,
BookingCategoryType type = BookingCategoryType.Income)
{
return new BookingCategory
{
Id = Guid.NewGuid(),
ClubId = clubId ?? _clubId,
Name = name,
CategoryType = type,
IsSystemCategory = isSystem,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
}
#endregion
}