using AutoMapper; using Koogle.Application.DTOs; using Koogle.Application.Services; using Koogle.Domain.Entities; using Koogle.Domain.Enums; using Koogle.Domain.Interfaces; using Koogle.Tests.Common; using KoogleApp.Data; using Microsoft.EntityFrameworkCore; namespace Koogle.Tests.Unit.Services; /// /// Unit tests for ClubService. /// public class ClubServiceTests : IDisposable { private readonly Mock _repositoryMock; private readonly Mock _mapperMock; private readonly AppDbContext _appDbContext; private readonly ClubService _sut; public ClubServiceTests() { _repositoryMock = new Mock(); _mapperMock = TestMockHelpers.CreateClubMapperMock(); var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"UnitTest_{Guid.NewGuid()}") .Options; _appDbContext = new AppDbContext(options); _sut = new ClubService(_repositoryMock.Object, _mapperMock.Object, _appDbContext); } public void Dispose() { _appDbContext.Dispose(); } #region GetAllAsync Tests [Fact] public async Task GetAllAsync_ReturnsEmptyList_WhenNoClubsExist() { // Arrange _repositoryMock.SetupGetAllAsync([]); // Act var result = await _sut.GetAllAsync(); // Assert result.Should().BeEmpty(); _repositoryMock.Verify(r => r.GetAllAsync(It.IsAny()), Times.Once); } [Fact] public async Task GetAllAsync_ReturnsAllClubs_WhenClubsExist() { // Arrange var clubs = TestDataGenerator.CreateClubs(3); _repositoryMock.SetupGetAllAsync(clubs); // Act var result = await _sut.GetAllAsync(); // Assert result.Should().HaveCount(3); result.Select(c => c.Id).Should().BeEquivalentTo(clubs.Select(c => c.Id)); } [Fact] public async Task GetAllAsync_MapsClubsToDtos_Correctly() { // Arrange var club = TestDataGenerator.CreateClub( name: "Test Kegelclub", expenseCalculation: ExpenseCalculation.Average); _repositoryMock.SetupGetAllAsync([club]); // Act var result = await _sut.GetAllAsync(); // Assert result.Should().ContainSingle(); var dto = result.First(); dto.Name.Should().Be("Test Kegelclub"); dto.ExpenseCalculation.Should().Be(ExpenseCalculation.Average); } #endregion #region GetByIdAsync Tests [Fact] public async Task GetByIdAsync_ReturnsNull_WhenClubNotFound() { // Arrange var id = Guid.NewGuid(); _repositoryMock.SetupGetByIdAsync(id, null); // Act var result = await _sut.GetByIdAsync(id); // Assert result.Should().BeNull(); } [Fact] public async Task GetByIdAsync_ReturnsClub_WhenExists() { // Arrange var club = TestDataGenerator.CreateClub(); _repositoryMock.SetupGetByIdAsync(club.Id, club); // Act var result = await _sut.GetByIdAsync(club.Id); // Assert result.Should().NotBeNull(); result!.Id.Should().Be(club.Id); result.Name.Should().Be(club.Name); } [Theory] [InlineData("00000000-0000-0000-0000-000000000001")] [InlineData("11111111-1111-1111-1111-111111111111")] public async Task GetByIdAsync_QueriesCorrectId(string guidString) { // Arrange var id = Guid.Parse(guidString); _repositoryMock.SetupGetByIdAsync(null); // Act await _sut.GetByIdAsync(id); // Assert _repositoryMock.Verify(r => r.GetByIdAsync(id, It.IsAny()), Times.Once); } #endregion #region CreateAsync Tests [Fact] public async Task CreateAsync_CreatesClubWithCorrectData() { // Arrange var dto = TestDataGenerator.CreateCreateClubDto( name: "New Club", expenseCalculation: ExpenseCalculation.Maximum); _repositoryMock.SetupAddAsync(); // Act var result = await _sut.CreateAsync(dto); // Assert result.Should().NotBeNull(); result.Name.Should().Be("New Club"); result.ExpenseCalculation.Should().Be(ExpenseCalculation.Maximum); } [Fact] public async Task CreateAsync_GeneratesNewId() { // Arrange var dto = TestDataGenerator.CreateCreateClubDto(); Club? capturedClub = null; _repositoryMock.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .Callback((c, _) => capturedClub = c) .ReturnsAsync((Club c, CancellationToken _) => c); // Act await _sut.CreateAsync(dto); // Assert capturedClub.Should().NotBeNull(); capturedClub!.Id.Should().NotBe(Guid.Empty); } [Fact] public async Task CreateAsync_SetsCreatedAtToUtcNow() { // Arrange var dto = TestDataGenerator.CreateCreateClubDto(); Club? capturedClub = null; var beforeCreate = DateTime.UtcNow; _repositoryMock.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .Callback((c, _) => capturedClub = c) .ReturnsAsync((Club c, CancellationToken _) => c); // Act await _sut.CreateAsync(dto); var afterCreate = DateTime.UtcNow; // Assert capturedClub!.CreatedAt.Should().BeOnOrAfter(beforeCreate); capturedClub.CreatedAt.Should().BeOnOrBefore(afterCreate); } [Fact] public async Task CreateAsync_SetsIsDeletedToFalse() { // Arrange var dto = TestDataGenerator.CreateCreateClubDto(); Club? capturedClub = null; _repositoryMock.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .Callback((c, _) => capturedClub = c) .ReturnsAsync((Club c, CancellationToken _) => c); // Act await _sut.CreateAsync(dto); // Assert capturedClub!.IsDeleted.Should().BeFalse(); } [Theory] [InlineData(ExpenseCalculation.None)] [InlineData(ExpenseCalculation.Average)] [InlineData(ExpenseCalculation.Maximum)] public async Task CreateAsync_HandlesAllExpenseCalculationTypes(ExpenseCalculation calculation) { // Arrange var dto = TestDataGenerator.CreateCreateClubDto(expenseCalculation: calculation); _repositoryMock.SetupAddAsync(); // Act var result = await _sut.CreateAsync(dto); // Assert result.ExpenseCalculation.Should().Be(calculation); } #endregion #region UpdateAsync Tests [Fact] public async Task UpdateAsync_ThrowsException_WhenClubNotFound() { // Arrange var dto = TestDataGenerator.CreateUpdateClubDto(); _repositoryMock.SetupGetByIdAsync(dto.Id, null); // Act var act = () => _sut.UpdateAsync(dto); // Assert await act.Should().ThrowAsync() .WithMessage($"Club with id {dto.Id} not found."); } [Fact] public async Task UpdateAsync_UpdatesClubProperties() { // Arrange var existingClub = TestDataGenerator.CreateClub( name: "Old Name", expenseCalculation: ExpenseCalculation.None); var dto = TestDataGenerator.CreateUpdateClubDto( id: existingClub.Id, name: "New Name", expenseCalculation: ExpenseCalculation.Maximum); _repositoryMock.SetupGetByIdAsync(existingClub.Id, existingClub); _repositoryMock.SetupUpdateAsync(); // Act var result = await _sut.UpdateAsync(dto); // Assert result.Name.Should().Be("New Name"); result.ExpenseCalculation.Should().Be(ExpenseCalculation.Maximum); } [Fact] public async Task UpdateAsync_SetsModifiedAtToUtcNow() { // Arrange var existingClub = TestDataGenerator.CreateClub(); var dto = TestDataGenerator.CreateUpdateClubDto(id: existingClub.Id); var beforeUpdate = DateTime.UtcNow; _repositoryMock.SetupGetByIdAsync(existingClub.Id, existingClub); Club? capturedClub = null; _repositoryMock.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny())) .Callback((c, _) => capturedClub = c) .ReturnsAsync((Club c, CancellationToken _) => c); // Act await _sut.UpdateAsync(dto); var afterUpdate = DateTime.UtcNow; // Assert capturedClub!.ModifiedAt.Should().BeOnOrAfter(beforeUpdate); capturedClub.ModifiedAt.Should().BeOnOrBefore(afterUpdate); } [Fact] public async Task UpdateAsync_CallsRepositoryUpdateAsync() { // Arrange var existingClub = TestDataGenerator.CreateClub(); var dto = TestDataGenerator.CreateUpdateClubDto(id: existingClub.Id); _repositoryMock.SetupGetByIdAsync(existingClub.Id, existingClub); _repositoryMock.SetupUpdateAsync(); // Act await _sut.UpdateAsync(dto); // Assert _repositoryMock.Verify(r => r.UpdateAsync(existingClub, It.IsAny()), Times.Once); } #endregion #region DeleteAsync Tests [Fact] public async Task DeleteAsync_ReturnsTrue_WhenSuccessful() { // Arrange var id = Guid.NewGuid(); _repositoryMock.SetupDeleteAsync(true); // Act var result = await _sut.DeleteAsync(id); // Assert result.Should().BeTrue(); } [Fact] public async Task DeleteAsync_ReturnsFalse_WhenClubNotFound() { // Arrange var id = Guid.NewGuid(); _repositoryMock.SetupDeleteAsync(false); // Act var result = await _sut.DeleteAsync(id); // Assert result.Should().BeFalse(); } [Fact] public async Task DeleteAsync_CallsRepositoryWithCorrectId() { // Arrange var id = Guid.NewGuid(); _repositoryMock.SetupDeleteAsync(); // Act await _sut.DeleteAsync(id); // Assert _repositoryMock.Verify(r => r.DeleteAsync(id, It.IsAny()), Times.Once); } #endregion #region Cancellation Token Tests [Fact] public async Task GetAllAsync_PassesCancellationToken() { // Arrange var cts = new CancellationTokenSource(); _repositoryMock.SetupGetAllAsync([]); // Act await _sut.GetAllAsync(cts.Token); // Assert _repositoryMock.Verify(r => r.GetAllAsync(cts.Token), Times.Once); } [Fact] public async Task CreateAsync_PassesCancellationToken() { // Arrange var cts = new CancellationTokenSource(); var dto = TestDataGenerator.CreateCreateClubDto(); _repositoryMock.SetupAddAsync(); // Act await _sut.CreateAsync(dto, cts.Token); // Assert _repositoryMock.Verify(r => r.AddAsync(It.IsAny(), cts.Token), Times.Once); } #endregion }