444 lines
12 KiB
C#
444 lines
12 KiB
C#
using AutoMapper;
|
|
using Koogle.Application.DTOs;
|
|
using Koogle.Application.Interfaces;
|
|
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.Integration;
|
|
|
|
/// <summary>
|
|
/// Integration tests for ClubService with in-memory database.
|
|
/// </summary>
|
|
public class ClubServiceIntegrationTests : IAsyncLifetime
|
|
{
|
|
private AppDbContext _context = null!;
|
|
private ClubService _sut = null!;
|
|
private IMapper _mapper = null!;
|
|
|
|
public Task InitializeAsync()
|
|
{
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.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<IClubGifService>();
|
|
var bookingCategoryServiceMock = new Mock<IBookingCategoryService>();
|
|
|
|
_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<InvalidOperationException>();
|
|
}
|
|
|
|
#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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple in-memory ClubRepository for integration testing.
|
|
/// </summary>
|
|
internal class ClubRepository : Koogle.Domain.Interfaces.IClubRepository
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public ClubRepository(AppDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public async Task<List<Club>> GetAllAsync(CancellationToken ct = default)
|
|
{
|
|
return await _context.Clubs
|
|
.Where(c => !c.IsDeleted)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<Club?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
return await _context.Clubs
|
|
.Where(c => c.Id == id && !c.IsDeleted)
|
|
.FirstOrDefaultAsync(ct);
|
|
}
|
|
|
|
public async Task<Club> AddAsync(Club entity, CancellationToken ct = default)
|
|
{
|
|
await _context.Clubs.AddAsync(entity, ct);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Club> UpdateAsync(Club entity, CancellationToken ct = default)
|
|
{
|
|
_context.Clubs.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<bool> 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<Club?> 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<Club?> 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);
|
|
}
|
|
}
|