406 lines
14 KiB
C#
406 lines
14 KiB
C#
using Distribution.Features.Region;
|
|
using Distribution.Infra.Authorization;
|
|
using Distribution.Infra.UoW;
|
|
using Distribution.Models;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace Distribution.Tests.Services;
|
|
|
|
public class RegionServiceTests
|
|
{
|
|
private readonly Mock<IRegionRepository> _repo = new();
|
|
private readonly Mock<IUnitOfWork> _uow = new();
|
|
private readonly Mock<ICurrentUserService> _currentUser = new();
|
|
private readonly Mock<IScopeAccessService> _scopeAccess = new();
|
|
private readonly RegionService _sut;
|
|
|
|
public RegionServiceTests()
|
|
{
|
|
_currentUser.Setup(u => u.RoleLevel).Returns(60);
|
|
_repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync([]);
|
|
_sut = new RegionService(_repo.Object, _uow.Object, _currentUser.Object, _scopeAccess.Object);
|
|
}
|
|
|
|
private static Region ActiveRegion(string name = "North") =>
|
|
new() { Id = Guid.NewGuid(), Name = name, IsActive = true };
|
|
|
|
private static Region InactiveRegion() =>
|
|
new() { Id = Guid.NewGuid(), Name = "Old", IsActive = false };
|
|
|
|
// GetAllAsync
|
|
|
|
[Fact]
|
|
public async Task GetAllAsync_ReturnsOnlyActiveRegions()
|
|
{
|
|
_repo.Setup(r => r.GetAllAsync()).ReturnsAsync([ActiveRegion(), InactiveRegion()]);
|
|
|
|
var result = await _sut.GetAllAsync();
|
|
|
|
Assert.Single(result);
|
|
Assert.True(result[0].IsActive);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAllAsync_ReturnsEmpty_WhenNoneActive()
|
|
{
|
|
_repo.Setup(r => r.GetAllAsync()).ReturnsAsync([InactiveRegion()]);
|
|
|
|
var result = await _sut.GetAllAsync();
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
// GetByIdAsync
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_ReturnsNull_WhenNotFound()
|
|
{
|
|
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync((Region?)null);
|
|
|
|
var result = await _sut.GetByIdAsync(Guid.NewGuid());
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_ReturnsNull_WhenInactive()
|
|
{
|
|
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(InactiveRegion());
|
|
|
|
var result = await _sut.GetByIdAsync(Guid.NewGuid());
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_ReturnsResponse_WhenActiveRegionFound()
|
|
{
|
|
var region = ActiveRegion("South");
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
|
|
var result = await _sut.GetByIdAsync(region.Id);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("South", result.Name);
|
|
Assert.Equal(region.Id, result.Id);
|
|
}
|
|
|
|
// CreateAsync
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_AddsRegion_AndCommits()
|
|
{
|
|
var request = new CreateRegionRequest("East");
|
|
|
|
var result = await _sut.CreateAsync(request);
|
|
|
|
_uow.Verify(u => u.BeginAsync(default), Times.Once);
|
|
_repo.Verify(r => r.AddAsync(It.Is<Region>(x => x.Name == "East")), Times.Once);
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Once);
|
|
Assert.Equal("East", result.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_Rollbacks_WhenAddThrows()
|
|
{
|
|
_repo.Setup(r => r.AddAsync(It.IsAny<Region>())).ThrowsAsync(new Exception("db error"));
|
|
|
|
await Assert.ThrowsAsync<Exception>(() => _sut.CreateAsync(new CreateRegionRequest("X")));
|
|
|
|
_uow.Verify(u => u.RollbackAsync(default), Times.Once);
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Never);
|
|
}
|
|
|
|
// UpdateAsync
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_ReturnsNull_WhenNotFound()
|
|
{
|
|
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync((Region?)null);
|
|
|
|
var result = await _sut.UpdateAsync(Guid.NewGuid(), new UpdateRegionRequest("New"));
|
|
|
|
Assert.Null(result);
|
|
_uow.Verify(u => u.BeginAsync(default), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_ReturnsNull_WhenInactive()
|
|
{
|
|
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(InactiveRegion());
|
|
|
|
var result = await _sut.UpdateAsync(Guid.NewGuid(), new UpdateRegionRequest("New"));
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_UpdatesName_AndCommits()
|
|
{
|
|
var region = ActiveRegion("Old Name");
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
|
|
var result = await _sut.UpdateAsync(region.Id, new UpdateRegionRequest("New Name"));
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("New Name", result.Name);
|
|
_uow.Verify(u => u.BeginAsync(default), Times.Once);
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Once);
|
|
}
|
|
|
|
// Region code
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_GeneratesFirstCode_WhenNoRegionsExist()
|
|
{
|
|
_repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync([]);
|
|
|
|
var result = await _sut.CreateAsync(new CreateRegionRequest("North"));
|
|
|
|
Assert.Equal("REG-0001", result.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_GeneratesNextCode_FromHighestExisting_IgnoringNonMatching()
|
|
{
|
|
_repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync(["REG-0002", "REG-0009", "CUSTOM", "REG-abc", ""]);
|
|
|
|
var result = await _sut.CreateAsync(new CreateRegionRequest("North"));
|
|
|
|
Assert.Equal("REG-0010", result.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_StoresProvidedCode_AsEntered()
|
|
{
|
|
var result = await _sut.CreateAsync(new CreateRegionRequest("North", " nth-1 "));
|
|
|
|
Assert.Equal("nth-1", result.Code);
|
|
_repo.Verify(r => r.GetAllCodesAsync(), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_Throws_WhenProvidedCodeInUse()
|
|
{
|
|
_repo.Setup(r => r.CodeExistsAsync("NTH-1", null)).ReturnsAsync(true);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_sut.CreateAsync(new CreateRegionRequest("North", "NTH-1")));
|
|
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetNextCodeAsync_ReturnsSuggestion()
|
|
{
|
|
_repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync(["REG-0003"]);
|
|
|
|
var result = await _sut.GetNextCodeAsync();
|
|
|
|
Assert.Equal("REG-0004", result.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_ChangesCode_StoredAsEntered()
|
|
{
|
|
var region = ActiveRegion();
|
|
region.Code = "REG-0001";
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.CodeExistsAsync("north-1", region.Id)).ReturnsAsync(false);
|
|
|
|
var result = await _sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", " north-1 "));
|
|
|
|
Assert.Equal("north-1", result!.Code);
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_KeepsCode_WhenCodeBlank()
|
|
{
|
|
var region = ActiveRegion();
|
|
region.Code = "REG-0001";
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
|
|
var result = await _sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", " "));
|
|
|
|
Assert.Equal("REG-0001", result!.Code);
|
|
_repo.Verify(r => r.CodeExistsAsync(It.IsAny<string>(), It.IsAny<Guid?>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_Throws_WhenCodeUsedByAnotherRegion()
|
|
{
|
|
var region = ActiveRegion();
|
|
region.Code = "REG-0001";
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.CodeExistsAsync("REG-0002", region.Id)).ReturnsAsync(true);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", "REG-0002")));
|
|
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_AllowsResubmittingOwnCode()
|
|
{
|
|
var region = ActiveRegion();
|
|
region.Code = "REG-0001";
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
|
|
var result = await _sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", "REG-0001"));
|
|
|
|
Assert.Equal("REG-0001", result!.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByIdAsync_ReturnsCode()
|
|
{
|
|
var region = ActiveRegion();
|
|
region.Code = "REG-0007";
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
|
|
var result = await _sut.GetByIdAsync(region.Id);
|
|
|
|
Assert.Equal("REG-0007", result!.Code);
|
|
}
|
|
|
|
// DeleteAsync
|
|
|
|
[Fact]
|
|
public async Task DeleteAsync_ReturnsFalse_WhenNotFound()
|
|
{
|
|
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync((Region?)null);
|
|
|
|
var result = await _sut.DeleteAsync(Guid.NewGuid());
|
|
|
|
Assert.False(result);
|
|
_uow.Verify(u => u.BeginAsync(default), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteAsync_ReturnsFalse_WhenInactive()
|
|
{
|
|
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(InactiveRegion());
|
|
|
|
var result = await _sut.DeleteAsync(Guid.NewGuid());
|
|
|
|
Assert.False(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteAsync_CallsRemove_AndCommits()
|
|
{
|
|
var region = ActiveRegion();
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
|
|
var result = await _sut.DeleteAsync(region.Id);
|
|
|
|
Assert.True(result);
|
|
_repo.Verify(r => r.Remove(region), Times.Once);
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteAsync_Rollbacks_WhenRemoveThrows()
|
|
{
|
|
var region = ActiveRegion();
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.Remove(It.IsAny<Region>())).Throws(new Exception("db error"));
|
|
|
|
await Assert.ThrowsAsync<Exception>(() => _sut.DeleteAsync(region.Id));
|
|
|
|
_uow.Verify(u => u.RollbackAsync(default), Times.Once);
|
|
}
|
|
|
|
// Region manager
|
|
|
|
private static User ManagerUser(string role = "RegionalManager", bool active = true) => new()
|
|
{
|
|
Id = Guid.NewGuid(), Username = "mgr", FullName = "Mgr One", IsActive = active,
|
|
Role = new Role { Name = role, Level = 50 }
|
|
};
|
|
|
|
[Fact]
|
|
public async Task SetManagerAsync_ReturnsNull_WhenRegionMissing()
|
|
{
|
|
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync((Region?)null);
|
|
|
|
Assert.Null(await _sut.SetManagerAsync(Guid.NewGuid(), new SetRegionManagerRequest(Guid.NewGuid())));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetManagerAsync_Throws_WhenUserNotRegionalManager()
|
|
{
|
|
var region = ActiveRegion();
|
|
var user = ManagerUser("SalesRep");
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id)));
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetManagerAsync_ReplacesExistingManager_AndAddsScope()
|
|
{
|
|
var region = ActiveRegion();
|
|
var user = ManagerUser();
|
|
var old = new UserRegionScope { FK_UserId = Guid.NewGuid(), FK_RegionId = region.Id };
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user);
|
|
_repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([old]);
|
|
_repo.Setup(r => r.GetRegionScopeAsync(user.Id, region.Id)).ReturnsAsync((UserRegionScope?)null);
|
|
|
|
var result = await _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id));
|
|
|
|
Assert.Equal(user.Id, result!.UserId);
|
|
_repo.Verify(r => r.RemoveRegionScopes(It.Is<IEnumerable<UserRegionScope>>(s => s.Single() == old)), Times.Once);
|
|
_repo.Verify(r => r.AddRegionScopeAsync(It.Is<UserRegionScope>(s => s.FK_UserId == user.Id && s.FK_RegionId == region.Id)), Times.Once);
|
|
_uow.Verify(u => u.CommitAsync(default), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetManagerAsync_DoesNotDuplicateScope_WhenAlreadyManager()
|
|
{
|
|
var region = ActiveRegion();
|
|
var user = ManagerUser();
|
|
var existing = new UserRegionScope { FK_UserId = user.Id, FK_RegionId = region.Id };
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user);
|
|
_repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([existing]);
|
|
_repo.Setup(r => r.GetRegionScopeAsync(user.Id, region.Id)).ReturnsAsync(existing);
|
|
|
|
await _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id));
|
|
|
|
_repo.Verify(r => r.AddRegionScopeAsync(It.IsAny<UserRegionScope>()), Times.Never);
|
|
_repo.Verify(r => r.RemoveRegionScopes(It.Is<IEnumerable<UserRegionScope>>(s => !s.Any())), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemoveManagerAsync_ReturnsFalse_WhenNoManager()
|
|
{
|
|
var region = ActiveRegion();
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([]);
|
|
|
|
Assert.False(await _sut.RemoveManagerAsync(region.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetManagerAsync_ReturnsManager()
|
|
{
|
|
var region = ActiveRegion();
|
|
var user = ManagerUser();
|
|
_repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region);
|
|
_repo.Setup(r => r.GetManagerScopesAsync(region.Id))
|
|
.ReturnsAsync([new UserRegionScope { FK_UserId = user.Id, FK_RegionId = region.Id, User = user }]);
|
|
|
|
var result = await _sut.GetManagerAsync(region.Id);
|
|
|
|
Assert.Equal("mgr", result!.Username);
|
|
}
|
|
}
|