408 lines
12 KiB
C#
408 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.Unit.Services;
|
|
|
|
/// <summary>
|
|
/// Unit tests for ClubService.
|
|
/// </summary>
|
|
public class ClubServiceTests : IDisposable
|
|
{
|
|
private readonly Mock<IClubRepository> _repositoryMock;
|
|
private readonly Mock<IMapper> _mapperMock;
|
|
private readonly Mock<IClubGifService> _gifServiceMock;
|
|
private readonly Mock<IBookingCategoryService> _bookingCategoryServiceMock;
|
|
private readonly AppDbContext _appDbContext;
|
|
private readonly ClubService _sut;
|
|
|
|
public ClubServiceTests()
|
|
{
|
|
_repositoryMock = new Mock<IClubRepository>();
|
|
_mapperMock = TestMockHelpers.CreateClubMapperMock();
|
|
_gifServiceMock = new Mock<IClubGifService>();
|
|
_bookingCategoryServiceMock = new Mock<IBookingCategoryService>();
|
|
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseInMemoryDatabase($"UnitTest_{Guid.NewGuid()}")
|
|
.Options;
|
|
_appDbContext = new AppDbContext(options);
|
|
|
|
_sut = new ClubService(
|
|
_repositoryMock.Object,
|
|
_mapperMock.Object,
|
|
_appDbContext,
|
|
_gifServiceMock.Object,
|
|
_bookingCategoryServiceMock.Object);
|
|
}
|
|
|
|
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<CancellationToken>()), 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<CancellationToken>()), 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<Club>(), It.IsAny<CancellationToken>()))
|
|
.Callback<Club, CancellationToken>((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<Club>(), It.IsAny<CancellationToken>()))
|
|
.Callback<Club, CancellationToken>((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<Club>(), It.IsAny<CancellationToken>()))
|
|
.Callback<Club, CancellationToken>((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<InvalidOperationException>()
|
|
.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<Club>(), It.IsAny<CancellationToken>()))
|
|
.Callback<Club, CancellationToken>((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<CancellationToken>()), 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<CancellationToken>()), 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<Club>(), cts.Token), Times.Once);
|
|
}
|
|
|
|
#endregion
|
|
}
|