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 _repo = new(); private readonly Mock _uow = new(); private readonly Mock _currentUser = new(); private readonly Mock _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())).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())).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(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())).ThrowsAsync(new Exception("db error")); await Assert.ThrowsAsync(() => _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())).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())).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(() => _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(), It.IsAny()), 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(() => _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())).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())).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())).Throws(new Exception("db error")); await Assert.ThrowsAsync(() => _sut.DeleteAsync(region.Id)); _uow.Verify(u => u.RollbackAsync(default), Times.Once); } }