using AutoMapper; using GoodWood.Application.DTOs; using GoodWood.Application.Interfaces; using GoodWood.Application.Services; using GoodWood.Domain.Entities; using GoodWood.Domain.Enums; using GoodWood.Domain.Interfaces; using GoodWood.Infrastructure.Data; using GoodWood.Tests.Common; using Microsoft.EntityFrameworkCore; namespace GoodWood.Tests.Integration; /// /// Integration tests for ClubService with in-memory database. /// public class ClubServiceIntegrationTests : IAsyncLifetime { private AppDbContext _context = null!; private ClubService _sut = null!; private IMapper _mapper = null!; public Task InitializeAsync() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}") .Options; _context = new AppDbContext(options); // Use TestMockHelpers for simple mapper _mapper = TestMockHelpers.CreateClubMapperMock().Object; var repository = new ClubRepository(_context); var gifServiceMock = new Mock(); var bookingCategoryServiceMock = new Mock(); _sut = new ClubService( repository, _mapper, _context, gifServiceMock.Object, bookingCategoryServiceMock.Object); return Task.CompletedTask; } public async Task DisposeAsync() { await _context.DisposeAsync(); } #region GetAllAsync Integration Tests [Fact] public async Task GetAllAsync_ReturnsEmptyList_WhenDatabaseEmpty() { // Act var result = await _sut.GetAllAsync(); // Assert result.Should().BeEmpty(); } [Fact] public async Task GetAllAsync_ReturnsAllClubs_FromDatabase() { // Arrange var clubs = new[] { TestDataGenerator.CreateClub(name: "Club A"), TestDataGenerator.CreateClub(name: "Club B"), TestDataGenerator.CreateClub(name: "Club C") }; await _context.Clubs.AddRangeAsync(clubs); await _context.SaveChangesAsync(); // Act var result = await _sut.GetAllAsync(); // Assert result.Should().HaveCount(3); result.Select(c => c.Name).Should().Contain(new[] { "Club A", "Club B", "Club C" }); } [Fact] public async Task GetAllAsync_ExcludesDeletedClubs() { // Arrange var activeClub = TestDataGenerator.CreateClub(name: "Active Club"); var deletedClub = TestDataGenerator.CreateClub(name: "Deleted Club", isDeleted: true); await _context.Clubs.AddRangeAsync(activeClub, deletedClub); await _context.SaveChangesAsync(); // Act var result = await _sut.GetAllAsync(); // Assert result.Should().ContainSingle(); result.First().Name.Should().Be("Active Club"); } #endregion #region GetByIdAsync Integration Tests [Fact] public async Task GetByIdAsync_ReturnsClub_WhenExists() { // Arrange var club = TestDataGenerator.CreateClub(name: "Test Club"); await _context.Clubs.AddAsync(club); await _context.SaveChangesAsync(); // Act var result = await _sut.GetByIdAsync(club.Id); // Assert result.Should().NotBeNull(); result!.Id.Should().Be(club.Id); result.Name.Should().Be("Test Club"); } [Fact] public async Task GetByIdAsync_ReturnsNull_WhenNotExists() { // Arrange var nonExistentId = Guid.NewGuid(); // Act var result = await _sut.GetByIdAsync(nonExistentId); // Assert result.Should().BeNull(); } [Fact] public async Task GetByIdAsync_ReturnsNull_WhenDeleted() { // Arrange var deletedClub = TestDataGenerator.CreateClub(isDeleted: true); await _context.Clubs.AddAsync(deletedClub); await _context.SaveChangesAsync(); // Act var result = await _sut.GetByIdAsync(deletedClub.Id); // Assert result.Should().BeNull(); } #endregion #region CreateAsync Integration Tests [Fact] public async Task CreateAsync_PersistsClubToDatabase() { // Arrange var dto = TestDataGenerator.CreateCreateClubDto( name: "New Kegelclub", expenseCalculation: ExpenseCalculation.Average); // Act var result = await _sut.CreateAsync(dto); // Assert result.Should().NotBeNull(); result.Name.Should().Be("New Kegelclub"); result.ExpenseCalculation.Should().Be(ExpenseCalculation.Average); // Verify in database var dbClub = await _context.Clubs.FindAsync(result.Id); dbClub.Should().NotBeNull(); dbClub!.Name.Should().Be("New Kegelclub"); } [Fact] public async Task CreateAsync_GeneratesUniqueIds() { // Arrange var dto1 = TestDataGenerator.CreateCreateClubDto(name: "Club 1"); var dto2 = TestDataGenerator.CreateCreateClubDto(name: "Club 2"); // Act var result1 = await _sut.CreateAsync(dto1); var result2 = await _sut.CreateAsync(dto2); // Assert result1.Id.Should().NotBe(result2.Id); result1.Id.Should().NotBe(Guid.Empty); result2.Id.Should().NotBe(Guid.Empty); } [Theory] [InlineData(ExpenseCalculation.None)] [InlineData(ExpenseCalculation.Average)] [InlineData(ExpenseCalculation.Maximum)] public async Task CreateAsync_HandlesAllExpenseCalculationTypes(ExpenseCalculation calculation) { // Arrange var dto = TestDataGenerator.CreateCreateClubDto(expenseCalculation: calculation); // Act var result = await _sut.CreateAsync(dto); // Assert result.ExpenseCalculation.Should().Be(calculation); var dbClub = await _context.Clubs.FindAsync(result.Id); dbClub!.ExpenseCalculation.Should().Be(calculation); } #endregion #region UpdateAsync Integration Tests [Fact] public async Task UpdateAsync_UpdatesClubInDatabase() { // Arrange var club = TestDataGenerator.CreateClub( name: "Original Name", expenseCalculation: ExpenseCalculation.None); await _context.Clubs.AddAsync(club); await _context.SaveChangesAsync(); var updateDto = new UpdateClubDto { Id = club.Id, Name = "Updated Name", ExpenseCalculation = ExpenseCalculation.Maximum }; // Act var result = await _sut.UpdateAsync(updateDto); // Assert result.Name.Should().Be("Updated Name"); result.ExpenseCalculation.Should().Be(ExpenseCalculation.Maximum); // Verify in database _context.ChangeTracker.Clear(); var dbClub = await _context.Clubs.FindAsync(club.Id); dbClub!.Name.Should().Be("Updated Name"); dbClub.ExpenseCalculation.Should().Be(ExpenseCalculation.Maximum); } [Fact] public async Task UpdateAsync_SetsModifiedAt() { // Arrange var club = TestDataGenerator.CreateClub(); club.ModifiedAt = null; await _context.Clubs.AddAsync(club); await _context.SaveChangesAsync(); var updateDto = TestDataGenerator.CreateUpdateClubDto(id: club.Id); var beforeUpdate = DateTime.UtcNow; // Act await _sut.UpdateAsync(updateDto); // Assert _context.ChangeTracker.Clear(); var dbClub = await _context.Clubs.FindAsync(club.Id); dbClub!.ModifiedAt.Should().NotBeNull(); dbClub.ModifiedAt.Should().BeOnOrAfter(beforeUpdate); } [Fact] public async Task UpdateAsync_ThrowsException_WhenClubNotFound() { // Arrange var updateDto = TestDataGenerator.CreateUpdateClubDto(id: Guid.NewGuid()); // Act var act = () => _sut.UpdateAsync(updateDto); // Assert await act.Should().ThrowAsync(); } #endregion #region DeleteAsync Integration Tests [Fact] public async Task DeleteAsync_SoftDeletesClub() { var useQueryFilter = true; // Arrange var club = TestDataGenerator.CreateClub(); await _context.Clubs.AddAsync(club); await _context.SaveChangesAsync(); // Act var result = await _sut.DeleteAsync(club.Id); // Assert result.Should().BeTrue(); _context.ChangeTracker.Clear(); var dbClub = await _context.Clubs.FindAsync(club.Id); if (!useQueryFilter) { dbClub!.IsDeleted.Should().BeTrue(); } else { dbClub.Should().BeNull(); } } [Fact] public async Task DeleteAsync_ReturnsFalse_WhenClubNotFound() { // Arrange var nonExistentId = Guid.NewGuid(); // Act var result = await _sut.DeleteAsync(nonExistentId); // Assert result.Should().BeFalse(); } [Fact] public async Task DeleteAsync_MakesClubInvisibleToGetAll() { // Arrange var club = TestDataGenerator.CreateClub(); await _context.Clubs.AddAsync(club); await _context.SaveChangesAsync(); // Act await _sut.DeleteAsync(club.Id); var allClubs = await _sut.GetAllAsync(); // Assert allClubs.Should().NotContain(c => c.Id == club.Id); } #endregion #region Concurrent Operations Tests [Fact] public async Task ConcurrentCreates_AllSucceed() { // Arrange var tasks = Enumerable.Range(0, 10) .Select(i => TestDataGenerator.CreateCreateClubDto(name: $"Club {i}")) .Select(dto => _sut.CreateAsync(dto)) .ToList(); // Act var results = await Task.WhenAll(tasks); // Assert results.Should().HaveCount(10); results.Select(r => r.Id).Should().OnlyHaveUniqueItems(); } #endregion } /// /// Simple in-memory ClubRepository for integration testing. /// internal class ClubRepository : GoodWood.Domain.Interfaces.IClubRepository { private readonly AppDbContext _context; public ClubRepository(AppDbContext context) { _context = context; } public async Task> GetAllAsync(CancellationToken ct = default) { return await _context.Clubs .Where(c => !c.IsDeleted) .ToListAsync(ct); } public async Task GetByIdAsync(Guid id, CancellationToken ct = default) { return await _context.Clubs .Where(c => c.Id == id && !c.IsDeleted) .FirstOrDefaultAsync(ct); } public async Task AddAsync(Club entity, CancellationToken ct = default) { await _context.Clubs.AddAsync(entity, ct); await _context.SaveChangesAsync(ct); return entity; } public async Task UpdateAsync(Club entity, CancellationToken ct = default) { _context.Clubs.Update(entity); await _context.SaveChangesAsync(ct); return entity; } public async Task DeleteAsync(Guid id, CancellationToken ct = default) { var club = await _context.Clubs.FindAsync([id], ct); if (club == null) return false; club.IsDeleted = true; club.ModifiedAt = DateTime.UtcNow; await _context.SaveChangesAsync(ct); return true; } public async Task GetByNameAsync(string clubName, Guid? excludeGuid, CancellationToken ct = default) { var query = _context.Clubs .Where(c => !c.IsDeleted && c.Name.ToLower() == clubName.ToLower()); if (excludeGuid.HasValue) { query = query.Where(c => c.Id != excludeGuid.Value); } return await query.FirstOrDefaultAsync(ct); } public async Task GetByLoginNameAsync(string clubLoginName, Guid? excludeGuid, CancellationToken ct = default) { var query = _context.Clubs .Where(c => !c.IsDeleted && c.LoginName.ToLower() == clubLoginName.ToLower()); if (excludeGuid.HasValue) { query = query.Where(c => c.Id != excludeGuid.Value); } return await query.FirstOrDefaultAsync(ct); } }