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);
}
}