test project created

This commit is contained in:
Kalana B. Thilakarathna
2026-08-19 14:40:04 +05:30
parent 397af8fd59
commit 4673d72d53
148 changed files with 8252 additions and 285 deletions
@@ -0,0 +1,99 @@
using Distribution.Features.Auth;
using Distribution.Features.User;
using Distribution.Infra.Authorization;
using Distribution.Models;
using Microsoft.AspNetCore.Identity;
using Moq;
using Xunit;
namespace Distribution.Tests.Services;
public class AuthServiceTests
{
private readonly Mock<IUserRepository> _userRepo = new();
private readonly Mock<IJwtTokenService> _tokenService = new();
private readonly AuthService _sut;
private readonly PasswordHasher<User> _hasher = new();
public AuthServiceTests()
{
_sut = new AuthService(_userRepo.Object, _tokenService.Object);
}
private User ActiveUserWithPassword(string password) => new()
{
Id = Guid.NewGuid(),
Username = "testuser",
FullName = "Test User",
IsActive = true,
FK_RoleId = Guid.NewGuid(),
PasswordHash = _hasher.HashPassword(null!, password),
Role = new Role { Name = "Sales", Level = 10 }
};
// LoginAsync
[Fact]
public async Task LoginAsync_ReturnsNull_WhenUserNotFound()
{
_userRepo.Setup(r => r.GetByUsernameAsync(It.IsAny<string>())).ReturnsAsync((User?)null);
var result = await _sut.LoginAsync(new LoginRequest("nobody", "pass"));
Assert.Null(result);
_tokenService.Verify(t => t.GenerateToken(It.IsAny<User>()), Times.Never);
}
[Fact]
public async Task LoginAsync_ReturnsNull_WhenUserIsInactive()
{
var user = ActiveUserWithPassword("secret");
user.IsActive = false;
_userRepo.Setup(r => r.GetByUsernameAsync("testuser")).ReturnsAsync(user);
var result = await _sut.LoginAsync(new LoginRequest("testuser", "secret"));
Assert.Null(result);
_tokenService.Verify(t => t.GenerateToken(It.IsAny<User>()), Times.Never);
}
[Fact]
public async Task LoginAsync_ReturnsNull_WhenPasswordIsWrong()
{
var user = ActiveUserWithPassword("correct");
_userRepo.Setup(r => r.GetByUsernameAsync("testuser")).ReturnsAsync(user);
var result = await _sut.LoginAsync(new LoginRequest("testuser", "wrong"));
Assert.Null(result);
_tokenService.Verify(t => t.GenerateToken(It.IsAny<User>()), Times.Never);
}
[Fact]
public async Task LoginAsync_ReturnsLoginResponse_WhenCredentialsAreValid()
{
var user = ActiveUserWithPassword("secret");
_userRepo.Setup(r => r.GetByUsernameAsync("testuser")).ReturnsAsync(user);
_tokenService.Setup(t => t.GenerateToken(user)).Returns("jwt-token");
var result = await _sut.LoginAsync(new LoginRequest("testuser", "secret"));
Assert.NotNull(result);
Assert.Equal("jwt-token", result.Token);
Assert.Equal(user.Id, result.UserId);
Assert.Equal("testuser", result.Username);
Assert.Equal("Sales", result.RoleName);
}
[Fact]
public async Task LoginAsync_CallsGenerateToken_OnSuccess()
{
var user = ActiveUserWithPassword("pass");
_userRepo.Setup(r => r.GetByUsernameAsync(It.IsAny<string>())).ReturnsAsync(user);
_tokenService.Setup(t => t.GenerateToken(user)).Returns("token");
await _sut.LoginAsync(new LoginRequest("testuser", "pass"));
_tokenService.Verify(t => t.GenerateToken(user), Times.Once);
}
}
@@ -0,0 +1,259 @@
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<IItemRepository> _repo = new();
private readonly Mock<IUnitOfWork> _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<string>(), It.IsAny<Guid?>())).ReturnsAsync(true);
await Assert.ThrowsAsync<InvalidOperationException>(
() => _sut.CreateAsync(SampleCreateRequest("DUP")));
_repo.Verify(r => r.AddAsync(It.IsAny<Item>()), Times.Never);
}
[Fact]
public async Task CreateAsync_SetsManualSyncSource()
{
var item = ManualItem();
_repo.Setup(r => r.SkuExistsAsync(It.IsAny<string>(), It.IsAny<Guid?>())).ReturnsAsync(false);
_repo.Setup(r => r.AddAsync(It.IsAny<Item>())).Returns(Task.CompletedTask);
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(item);
await _sut.CreateAsync(SampleCreateRequest());
_repo.Verify(r => r.AddAsync(It.Is<Item>(i => i.SyncSource == SyncSource.Manual)), Times.Once);
}
[Fact]
public async Task CreateAsync_BeginsAndCommitsTransaction()
{
var item = ManualItem();
_repo.Setup(r => r.SkuExistsAsync(It.IsAny<string>(), It.IsAny<Guid?>())).ReturnsAsync(false);
_repo.Setup(r => r.AddAsync(It.IsAny<Item>())).Returns(Task.CompletedTask);
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).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<string>(), It.IsAny<Guid?>())).ReturnsAsync(false);
_repo.Setup(r => r.AddAsync(It.IsAny<Item>())).ThrowsAsync(new Exception("db error"));
await Assert.ThrowsAsync<Exception>(() => _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<Guid>())).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<Guid>())).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<Guid>())).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<Guid>())).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<Guid>())).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<Guid>())).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<Guid>())).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<Guid>())).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<Guid>())).ReturnsAsync(ErpItem());
var result = await _sut.ReplaceConversionsAsync(Guid.NewGuid(),
new ReplaceConversionsRequest([]));
Assert.Null(result);
_repo.Verify(r => r.ReplaceConversionsAsync(It.IsAny<Guid>(), It.IsAny<List<UomConversion>>()), 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<Guid>(), It.IsAny<List<UomConversion>>()))
.Returns(Task.CompletedTask);
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(item);
await _sut.ReplaceConversionsAsync(item.Id, new ReplaceConversionsRequest([]));
_repo.Verify(r => r.ReplaceConversionsAsync(item.Id, It.IsAny<List<UomConversion>>()), Times.Once);
_uow.Verify(u => u.CommitAsync(default), Times.Once);
}
}
@@ -0,0 +1,229 @@
using Distribution.Features.Order;
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
using Distribution.Models;
using Moq;
using Xunit;
namespace Distribution.Tests.Services;
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> _repo = new();
private readonly Mock<IUnitOfWork> _uow = new();
private readonly Mock<ICurrentUserService> _currentUser = new();
private readonly Mock<IScopeAccessService> _scopeAccess = new();
private readonly OrderService _sut;
public OrderServiceTests()
{
_sut = new OrderService(_repo.Object, _uow.Object, _currentUser.Object, _scopeAccess.Object);
}
private static Order ActiveOrder(Guid storeId) => new()
{
Id = Guid.NewGuid(),
FK_StoreId = storeId,
OrderDate = DateTime.Now,
TotalAmount = 100,
IsActive = true
};
private void SetupGlobalUser(Guid userId)
{
_currentUser.Setup(u => u.UserId).Returns(userId);
_currentUser.Setup(u => u.RoleLevel).Returns(50);
}
private void SetupScopedUser(Guid userId)
{
_currentUser.Setup(u => u.UserId).Returns(userId);
_currentUser.Setup(u => u.RoleLevel).Returns(10);
}
// GetAllAsync
[Fact]
public async Task GetAllAsync_GlobalUser_ReturnsAllActiveOrders()
{
var userId = Guid.NewGuid();
var storeA = Guid.NewGuid();
var storeB = Guid.NewGuid();
SetupGlobalUser(userId);
var orders = new List<Order> { ActiveOrder(storeA), ActiveOrder(storeB) };
_repo.Setup(r => r.GetAllAsync()).ReturnsAsync(orders);
_repo.Setup(r => r.GetItemsByOrderIdAsync(It.IsAny<Guid>())).ReturnsAsync([]);
var result = await _sut.GetAllAsync();
Assert.Equal(2, result.Count);
}
[Fact]
public async Task GetAllAsync_ScopedUser_FiltersToAccessibleStores()
{
var userId = Guid.NewGuid();
var accessibleStore = Guid.NewGuid();
var restrictedStore = Guid.NewGuid();
SetupScopedUser(userId);
_scopeAccess.Setup(s => s.GetAccessibleStoreIdsAsync(userId)).ReturnsAsync([accessibleStore]);
var orders = new List<Order> { ActiveOrder(accessibleStore), ActiveOrder(restrictedStore) };
_repo.Setup(r => r.GetAllAsync()).ReturnsAsync(orders);
_repo.Setup(r => r.GetItemsByOrderIdAsync(It.IsAny<Guid>())).ReturnsAsync([]);
var result = await _sut.GetAllAsync();
Assert.Single(result);
Assert.Equal(accessibleStore, result[0].StoreId);
}
[Fact]
public async Task GetAllAsync_FiltersOutInactiveOrders()
{
var userId = Guid.NewGuid();
SetupGlobalUser(userId);
var inactiveOrder = ActiveOrder(Guid.NewGuid());
inactiveOrder.IsActive = false;
_repo.Setup(r => r.GetAllAsync()).ReturnsAsync([inactiveOrder]);
_repo.Setup(r => r.GetItemsByOrderIdAsync(It.IsAny<Guid>())).ReturnsAsync([]);
var result = await _sut.GetAllAsync();
Assert.Empty(result);
}
// GetByIdAsync
[Fact]
public async Task GetByIdAsync_ReturnsNull_WhenOrderNotFound()
{
SetupGlobalUser(Guid.NewGuid());
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync((Order?)null);
var result = await _sut.GetByIdAsync(Guid.NewGuid());
Assert.Null(result);
}
[Fact]
public async Task GetByIdAsync_ReturnsNull_WhenUserLacksStoreScope()
{
var userId = Guid.NewGuid();
var storeId = Guid.NewGuid();
SetupScopedUser(userId);
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(ActiveOrder(storeId));
_scopeAccess.Setup(s => s.HasStoreScopeAsync(userId, storeId)).ReturnsAsync(false);
var result = await _sut.GetByIdAsync(Guid.NewGuid());
Assert.Null(result);
}
[Fact]
public async Task GetByIdAsync_ReturnsOrder_WhenGlobalUser()
{
var userId = Guid.NewGuid();
var order = ActiveOrder(Guid.NewGuid());
SetupGlobalUser(userId);
_repo.Setup(r => r.GetByIdAsync(order.Id)).ReturnsAsync(order);
_repo.Setup(r => r.GetItemsByOrderIdAsync(order.Id)).ReturnsAsync([]);
var result = await _sut.GetByIdAsync(order.Id);
Assert.NotNull(result);
Assert.Equal(order.Id, result.Id);
}
// CreateAsync
[Fact]
public async Task CreateAsync_ReturnsNull_WhenUserLacksStoreScope()
{
var userId = Guid.NewGuid();
var storeId = Guid.NewGuid();
SetupScopedUser(userId);
_scopeAccess.Setup(s => s.HasStoreScopeAsync(userId, storeId)).ReturnsAsync(false);
var result = await _sut.CreateAsync(
new CreateOrderRequest(storeId, DateTime.Now, 100, []));
Assert.Null(result);
_uow.Verify(u => u.BeginAsync(default), Times.Never);
}
[Fact]
public async Task CreateAsync_CreatesOrder_WhenGlobalUser()
{
var userId = Guid.NewGuid();
var storeId = Guid.NewGuid();
SetupGlobalUser(userId);
_repo.Setup(r => r.AddAsync(It.IsAny<Order>())).Returns(Task.CompletedTask);
_repo.Setup(r => r.AddItemsAsync(It.IsAny<IEnumerable<OrderItem>>())).Returns(Task.CompletedTask);
var result = await _sut.CreateAsync(
new CreateOrderRequest(storeId, DateTime.Now, 250, []));
Assert.NotNull(result);
Assert.Equal(storeId, result.StoreId);
Assert.Equal(250, result.TotalAmount);
_uow.Verify(u => u.BeginAsync(default), Times.Once);
_uow.Verify(u => u.CommitAsync(default), Times.Once);
}
[Fact]
public async Task CreateAsync_Rollbacks_OnException()
{
var userId = Guid.NewGuid();
SetupGlobalUser(userId);
_repo.Setup(r => r.AddAsync(It.IsAny<Order>())).ThrowsAsync(new Exception("db error"));
await Assert.ThrowsAsync<Exception>(() =>
_sut.CreateAsync(new CreateOrderRequest(Guid.NewGuid(), DateTime.Now, 100, [])));
_uow.Verify(u => u.RollbackAsync(default), Times.Once);
_uow.Verify(u => u.CommitAsync(default), Times.Never);
}
// DeleteAsync
[Fact]
public async Task DeleteAsync_ReturnsFalse_WhenOrderNotFound()
{
SetupGlobalUser(Guid.NewGuid());
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync((Order?)null);
var result = await _sut.DeleteAsync(Guid.NewGuid());
Assert.False(result);
}
[Fact]
public async Task DeleteAsync_ReturnsFalse_WhenUserLacksStoreScope()
{
var userId = Guid.NewGuid();
var storeId = Guid.NewGuid();
SetupScopedUser(userId);
_repo.Setup(r => r.GetByIdAsync(It.IsAny<Guid>())).ReturnsAsync(ActiveOrder(storeId));
_scopeAccess.Setup(s => s.HasStoreScopeAsync(userId, storeId)).ReturnsAsync(false);
var result = await _sut.DeleteAsync(Guid.NewGuid());
Assert.False(result);
_uow.Verify(u => u.BeginAsync(default), Times.Never);
}
[Fact]
public async Task DeleteAsync_RemovesOrder_WhenAuthorized()
{
var userId = Guid.NewGuid();
var order = ActiveOrder(Guid.NewGuid());
SetupGlobalUser(userId);
_repo.Setup(r => r.GetByIdAsync(order.Id)).ReturnsAsync(order);
var result = await _sut.DeleteAsync(order.Id);
Assert.True(result);
_repo.Verify(r => r.Remove(order), Times.Once);
_uow.Verify(u => u.CommitAsync(default), Times.Once);
}
}
@@ -0,0 +1,194 @@
using Distribution.Features.Region;
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 RegionService _sut;
public RegionServiceTests()
{
_sut = new RegionService(_repo.Object, _uow.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);
}
// 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);
}
}
@@ -0,0 +1,163 @@
using Distribution.Infra.Authorization;
using Moq;
using Xunit;
namespace Distribution.Tests.Services;
public class ScopeAccessServiceTests
{
private readonly Mock<IScopeRepository> _repo = new();
private readonly ScopeAccessService _sut;
private readonly Guid _userId = Guid.NewGuid();
public ScopeAccessServiceTests()
{
_sut = new ScopeAccessService(_repo.Object);
}
// Delegation tests
[Fact]
public async Task HasStoreScopeAsync_DelegatesToRepository()
{
var storeId = Guid.NewGuid();
_repo.Setup(r => r.HasStoreScopeAsync(_userId, storeId)).ReturnsAsync(true);
var result = await _sut.HasStoreScopeAsync(_userId, storeId);
Assert.True(result);
_repo.Verify(r => r.HasStoreScopeAsync(_userId, storeId), Times.Once);
}
[Fact]
public async Task HasRouteScopeAsync_DelegatesToRepository()
{
var routeId = Guid.NewGuid();
_repo.Setup(r => r.HasRouteScopeAsync(_userId, routeId)).ReturnsAsync(false);
var result = await _sut.HasRouteScopeAsync(_userId, routeId);
Assert.False(result);
}
[Fact]
public async Task HasRegionScopeAsync_DelegatesToRepository()
{
var regionId = Guid.NewGuid();
_repo.Setup(r => r.HasRegionScopeAsync(_userId, regionId)).ReturnsAsync(true);
var result = await _sut.HasRegionScopeAsync(_userId, regionId);
Assert.True(result);
}
// GetAccessibleStoreIdsAsync
[Fact]
public async Task GetAccessibleStoreIdsAsync_ReturnsEmpty_WhenNoAssignments()
{
_repo.Setup(r => r.GetDirectStoreIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetRouteIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetRegionIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(It.IsAny<List<Guid>>())).ReturnsAsync([]);
_repo.Setup(r => r.GetRouteIdsByRegionIdsAsync(It.IsAny<List<Guid>>())).ReturnsAsync([]);
var result = await _sut.GetAccessibleStoreIdsAsync(_userId);
Assert.Empty(result);
}
[Fact]
public async Task GetAccessibleStoreIdsAsync_ReturnsDirectStores()
{
var storeId = Guid.NewGuid();
_repo.Setup(r => r.GetDirectStoreIdsAsync(_userId)).ReturnsAsync([storeId]);
_repo.Setup(r => r.GetRouteIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetRegionIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(It.IsAny<List<Guid>>())).ReturnsAsync([]);
_repo.Setup(r => r.GetRouteIdsByRegionIdsAsync(It.IsAny<List<Guid>>())).ReturnsAsync([]);
var result = await _sut.GetAccessibleStoreIdsAsync(_userId);
Assert.Single(result);
Assert.Contains(storeId, result);
}
[Fact]
public async Task GetAccessibleStoreIdsAsync_IncludesStoresViaRoute()
{
var routeId = Guid.NewGuid();
var storeViaRoute = Guid.NewGuid();
_repo.Setup(r => r.GetDirectStoreIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetRouteIdsAsync(_userId)).ReturnsAsync([routeId]);
_repo.Setup(r => r.GetRegionIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid> { routeId })).ReturnsAsync([storeViaRoute]);
_repo.Setup(r => r.GetRouteIdsByRegionIdsAsync(It.IsAny<List<Guid>>())).ReturnsAsync([]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid>())).ReturnsAsync([]);
var result = await _sut.GetAccessibleStoreIdsAsync(_userId);
Assert.Contains(storeViaRoute, result);
}
[Fact]
public async Task GetAccessibleStoreIdsAsync_IncludesStoresViaRegion()
{
var regionId = Guid.NewGuid();
var routeViaRegion = Guid.NewGuid();
var storeViaRegion = Guid.NewGuid();
_repo.Setup(r => r.GetDirectStoreIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetRouteIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetRegionIdsAsync(_userId)).ReturnsAsync([regionId]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid>())).ReturnsAsync([]);
_repo.Setup(r => r.GetRouteIdsByRegionIdsAsync(new List<Guid> { regionId })).ReturnsAsync([routeViaRegion]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid> { routeViaRegion })).ReturnsAsync([storeViaRegion]);
var result = await _sut.GetAccessibleStoreIdsAsync(_userId);
Assert.Contains(storeViaRegion, result);
}
[Fact]
public async Task GetAccessibleStoreIdsAsync_DeduplicatesStores()
{
var sharedStoreId = Guid.NewGuid();
var routeId = Guid.NewGuid();
_repo.Setup(r => r.GetDirectStoreIdsAsync(_userId)).ReturnsAsync([sharedStoreId]);
_repo.Setup(r => r.GetRouteIdsAsync(_userId)).ReturnsAsync([routeId]);
_repo.Setup(r => r.GetRegionIdsAsync(_userId)).ReturnsAsync([]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid> { routeId })).ReturnsAsync([sharedStoreId]);
_repo.Setup(r => r.GetRouteIdsByRegionIdsAsync(It.IsAny<List<Guid>>())).ReturnsAsync([]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid>())).ReturnsAsync([]);
var result = await _sut.GetAccessibleStoreIdsAsync(_userId);
Assert.Single(result);
Assert.Equal(sharedStoreId, result[0]);
}
[Fact]
public async Task GetAccessibleStoreIdsAsync_CombinesAllSources()
{
var directStore = Guid.NewGuid();
var routeStore = Guid.NewGuid();
var regionStore = Guid.NewGuid();
var routeId = Guid.NewGuid();
var regionId = Guid.NewGuid();
var routeViaRegion = Guid.NewGuid();
_repo.Setup(r => r.GetDirectStoreIdsAsync(_userId)).ReturnsAsync([directStore]);
_repo.Setup(r => r.GetRouteIdsAsync(_userId)).ReturnsAsync([routeId]);
_repo.Setup(r => r.GetRegionIdsAsync(_userId)).ReturnsAsync([regionId]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid> { routeId })).ReturnsAsync([routeStore]);
_repo.Setup(r => r.GetRouteIdsByRegionIdsAsync(new List<Guid> { regionId })).ReturnsAsync([routeViaRegion]);
_repo.Setup(r => r.GetStoreIdsByRouteIdsAsync(new List<Guid> { routeViaRegion })).ReturnsAsync([regionStore]);
var result = await _sut.GetAccessibleStoreIdsAsync(_userId);
Assert.Equal(3, result.Count);
Assert.Contains(directStore, result);
Assert.Contains(routeStore, result);
Assert.Contains(regionStore, result);
}
}