414 lines
13 KiB
C#
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
|
|
}
|