using Distribution.Features.Item; using Distribution.Infra.UoW; using Distribution.Models; using Moq; using Xunit; namespace Distribution.Tests.Services; public class ItemServiceTests { private readonly Mock _repo = new(); private readonly Mock _uow = new(); private readonly ItemService _sut; public ItemServiceTests() { _sut = new ItemService(_repo.Object, _uow.Object); } private static Item ManualItem(string sku = "SKU-001") => new() { Id = Guid.NewGuid(), Sku = sku, Name = "Test Item", IsActive = true, SyncSource = SyncSource.Manual, FK_CategoryId = Guid.NewGuid(), FK_BaseUomId = Guid.NewGuid(), Category = new Category { Name = "Cat" }, BaseUom = new Uom { Name = "Each" }, Conversions = [] }; private static Item ErpItem() => new() { Id = Guid.NewGuid(), Sku = "ERP-001", Name = "Erp Item", IsActive = true, SyncSource = SyncSource.Erp, FK_CategoryId = Guid.NewGuid(), FK_BaseUomId = Guid.NewGuid(), Category = new Category { Name = "Cat" }, BaseUom = new Uom { Name = "Each" }, Conversions = [] }; private static Item InactiveItem() => new() { Id = Guid.NewGuid(), Sku = "OLD-001", Name = "Old", IsActive = false, SyncSource = SyncSource.Manual, FK_CategoryId = Guid.NewGuid(), FK_BaseUomId = Guid.NewGuid(), Category = new Category { Name = "Cat" }, BaseUom = new Uom { Name = "Each" }, Conversions = [] }; private static CreateItemRequest SampleCreateRequest(string sku = "SKU-001") => new(sku, "Test Item", null, Guid.NewGuid(), null, null, Guid.NewGuid(), null, null); // CreateAsync [Fact] public async Task CreateAsync_Throws_WhenSkuAlreadyExists() { _repo.Setup(r => r.SkuExistsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); await Assert.ThrowsAsync( () => _sut.CreateAsync(SampleCreateRequest("DUP"))); _repo.Verify(r => r.AddAsync(It.IsAny()), Times.Never); } [Fact] public async Task CreateAsync_SetsManualSyncSource() { var item = ManualItem(); _repo.Setup(r => r.SkuExistsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); _repo.Setup(r => r.AddAsync(It.IsAny())).Returns(Task.CompletedTask); _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(item); await _sut.CreateAsync(SampleCreateRequest()); _repo.Verify(r => r.AddAsync(It.Is(i => i.SyncSource == SyncSource.Manual)), Times.Once); } [Fact] public async Task CreateAsync_BeginsAndCommitsTransaction() { var item = ManualItem(); _repo.Setup(r => r.SkuExistsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); _repo.Setup(r => r.AddAsync(It.IsAny())).Returns(Task.CompletedTask); _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(item); await _sut.CreateAsync(SampleCreateRequest()); _uow.Verify(u => u.BeginAsync(default), Times.Once); _uow.Verify(u => u.CommitAsync(default), Times.Once); } [Fact] public async Task CreateAsync_Rollbacks_OnAddException() { _repo.Setup(r => r.SkuExistsAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); _repo.Setup(r => r.AddAsync(It.IsAny())).ThrowsAsync(new Exception("db error")); await Assert.ThrowsAsync(() => _sut.CreateAsync(SampleCreateRequest())); _uow.Verify(u => u.RollbackAsync(default), Times.Once); _uow.Verify(u => u.CommitAsync(default), Times.Never); } // UpdateAsync [Fact] public async Task UpdateAsync_ReturnsNull_WhenItemNotFound() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Item?)null); var result = await _sut.UpdateAsync(Guid.NewGuid(), new UpdateItemRequest("Name", null, Guid.NewGuid(), null, null, Guid.NewGuid(), null)); Assert.Null(result); _uow.Verify(u => u.BeginAsync(default), Times.Never); } [Fact] public async Task UpdateAsync_ReturnsNull_WhenItemInactive() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(InactiveItem()); var result = await _sut.UpdateAsync(Guid.NewGuid(), new UpdateItemRequest("Name", null, Guid.NewGuid(), null, null, Guid.NewGuid(), null)); Assert.Null(result); } [Fact] public async Task UpdateAsync_ReturnsNull_WhenItemIsErpSynced() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(ErpItem()); var result = await _sut.UpdateAsync(Guid.NewGuid(), new UpdateItemRequest("Name", null, Guid.NewGuid(), null, null, Guid.NewGuid(), null)); Assert.Null(result); _uow.Verify(u => u.BeginAsync(default), Times.Never); } [Fact] public async Task UpdateAsync_UpdatesFields_AndCommits() { var item = ManualItem(); _repo.Setup(r => r.GetByIdAsync(item.Id)).ReturnsAsync(item); _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(item); var catId = Guid.NewGuid(); var uomId = Guid.NewGuid(); item.Category = new Category { Name = "Cat" }; item.BaseUom = new Uom { Name = "Each" }; var result = await _sut.UpdateAsync(item.Id, new UpdateItemRequest("Updated", "desc", catId, null, null, uomId, 9.99m)); _uow.Verify(u => u.BeginAsync(default), Times.Once); _uow.Verify(u => u.CommitAsync(default), Times.Once); } // DeleteAsync [Fact] public async Task DeleteAsync_ReturnsFalse_WhenItemNotFound() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Item?)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_WhenItemInactive() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(InactiveItem()); var result = await _sut.DeleteAsync(Guid.NewGuid()); Assert.False(result); } [Fact] public async Task DeleteAsync_ReturnsFalse_WhenItemIsErpSynced() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(ErpItem()); var result = await _sut.DeleteAsync(Guid.NewGuid()); Assert.False(result); _uow.Verify(u => u.BeginAsync(default), Times.Never); } [Fact] public async Task DeleteAsync_CallsRemove_AndCommits_ForManualItem() { var item = ManualItem(); _repo.Setup(r => r.GetByIdAsync(item.Id)).ReturnsAsync(item); var result = await _sut.DeleteAsync(item.Id); Assert.True(result); _repo.Verify(r => r.Remove(item), Times.Once); _uow.Verify(u => u.CommitAsync(default), Times.Once); } // ReplaceConversionsAsync [Fact] public async Task ReplaceConversionsAsync_ReturnsNull_WhenItemNotFound() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Item?)null); var result = await _sut.ReplaceConversionsAsync(Guid.NewGuid(), new ReplaceConversionsRequest([])); Assert.Null(result); } [Fact] public async Task ReplaceConversionsAsync_ReturnsNull_WhenItemIsErpSynced() { _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(ErpItem()); var result = await _sut.ReplaceConversionsAsync(Guid.NewGuid(), new ReplaceConversionsRequest([])); Assert.Null(result); _repo.Verify(r => r.ReplaceConversionsAsync(It.IsAny(), It.IsAny>()), Times.Never); } [Fact] public async Task ReplaceConversionsAsync_CallsReplace_AndCommits() { var item = ManualItem(); _repo.Setup(r => r.GetByIdAsync(item.Id)).ReturnsAsync(item); _repo.Setup(r => r.ReplaceConversionsAsync(It.IsAny(), It.IsAny>())) .Returns(Task.CompletedTask); _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(item); await _sut.ReplaceConversionsAsync(item.Id, new ReplaceConversionsRequest([])); _repo.Verify(r => r.ReplaceConversionsAsync(item.Id, It.IsAny>()), Times.Once); _uow.Verify(u => u.CommitAsync(default), Times.Once); } }