From 2ecda623ac56240da082c47eea457181bac41b0a Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Mon, 31 Aug 2026 13:00:42 +0530 Subject: [PATCH] add route service tests --- .../Services/RouteServiceTests.cs | 343 ++++++++++++++++++ .../Services/StoreServiceTests.cs | 85 +++++ 2 files changed, 428 insertions(+) create mode 100644 Distribution.Tests/Services/RouteServiceTests.cs create mode 100644 Distribution.Tests/Services/StoreServiceTests.cs diff --git a/Distribution.Tests/Services/RouteServiceTests.cs b/Distribution.Tests/Services/RouteServiceTests.cs new file mode 100644 index 0000000..9592a21 --- /dev/null +++ b/Distribution.Tests/Services/RouteServiceTests.cs @@ -0,0 +1,343 @@ +using Distribution.Features.Region; +using Distribution.Features.Route; +using Distribution.Infra.Authorization; +using Distribution.Infra.UoW; +using Distribution.Models; +using Moq; +using Xunit; + +namespace Distribution.Tests.Services; + +public class RouteServiceTests +{ + private readonly Mock _repo = new(); + private readonly Mock _regionRepo = new(); + private readonly Mock _uow = new(); + private readonly Mock _currentUser = new(); + private readonly Mock _scopeAccess = new(); + private readonly RouteService _sut; + + private static readonly Guid RepUserId = Guid.NewGuid(); + + public RouteServiceTests() + { + _currentUser.Setup(u => u.RoleLevel).Returns(20); + _currentUser.Setup(u => u.UserId).Returns(RepUserId); + _sut = new RouteService(_repo.Object, _regionRepo.Object, _uow.Object, _currentUser.Object, _scopeAccess.Object); + } + + private static Models.Route ActiveRoute(Guid? ownerId = null) => + new() { Id = Guid.NewGuid(), RouteName = "R1", StartPoint = "A", EndPoint = "B", Length = 10, IsActive = true, FK_OwnerUserId = ownerId }; + + private static Models.Route InactiveRoute() => + new() { Id = Guid.NewGuid(), RouteName = "Old", StartPoint = "A", EndPoint = "B", Length = 10, IsActive = false }; + + private static Store ActiveStore(Guid routeId, decimal lat = 10m, decimal lon = 20m) => + new() { Id = Guid.NewGuid(), FK_RouteId = routeId, IsActive = true, Latitude = lat, Longitude = lon }; + + // AssignStoresToRouteAsync + + [Fact] + public async Task AssignStoresToRouteAsync_ReturnsNull_WhenRouteNotFound() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Models.Route?)null); + + var result = await _sut.AssignStoresToRouteAsync(Guid.NewGuid(), new AssignStoresToRouteRequest([Guid.NewGuid()])); + + Assert.Null(result); + _uow.Verify(u => u.BeginAsync(default), Times.Never); + } + + [Fact] + public async Task AssignStoresToRouteAsync_ReturnsNull_WhenRouteInactive() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(InactiveRoute()); + + var result = await _sut.AssignStoresToRouteAsync(Guid.NewGuid(), new AssignStoresToRouteRequest([Guid.NewGuid()])); + + Assert.Null(result); + } + + [Fact] + public async Task AssignStoresToRouteAsync_ReturnsNull_WhenCallerLacksRouteScope() + { + var route = ActiveRoute(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(false); + + var result = await _sut.AssignStoresToRouteAsync(route.Id, new AssignStoresToRouteRequest([Guid.NewGuid()])); + + Assert.Null(result); + _uow.Verify(u => u.BeginAsync(default), Times.Never); + } + + [Fact] + public async Task AssignStoresToRouteAsync_ReturnsNull_WhenCallerBelowRoleFloor() + { + _currentUser.Setup(u => u.RoleLevel).Returns(10); + var route = ActiveRoute(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + + var result = await _sut.AssignStoresToRouteAsync(route.Id, new AssignStoresToRouteRequest([Guid.NewGuid()])); + + Assert.Null(result); + } + + [Fact] + public async Task AssignStoresToRouteAsync_ReturnsNull_WhenAnyStoreIdMissing() + { + var route = ActiveRoute(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + var storeIds = new List { Guid.NewGuid(), Guid.NewGuid() }; + _repo.Setup(r => r.GetStoresByIdsAsync(storeIds)).ReturnsAsync([ActiveStore(route.Id)]); // only one of two found + + var result = await _sut.AssignStoresToRouteAsync(route.Id, new AssignStoresToRouteRequest(storeIds)); + + Assert.Null(result); + _uow.Verify(u => u.BeginAsync(default), Times.Never); + } + + [Fact] + public async Task AssignStoresToRouteAsync_UpdatesEachStore_AndCommits() + { + var route = ActiveRoute(); + var otherRouteId = Guid.NewGuid(); + var stores = new List { ActiveStore(otherRouteId), ActiveStore(otherRouteId) }; + var storeIds = stores.Select(s => s.Id).ToList(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetStoresByIdsAsync(storeIds)).ReturnsAsync(stores); + + var result = await _sut.AssignStoresToRouteAsync(route.Id, new AssignStoresToRouteRequest(storeIds)); + + Assert.NotNull(result); + Assert.All(stores, s => Assert.Equal(route.Id, s.FK_RouteId)); + _repo.Verify(r => r.UpdateStore(It.IsAny()), Times.Exactly(2)); + _uow.Verify(u => u.CommitAsync(default), Times.Once); + } + + [Fact] + public async Task AssignStoresToRouteAsync_Rollbacks_WhenUpdateThrows() + { + var route = ActiveRoute(); + var stores = new List { ActiveStore(route.Id) }; + var storeIds = stores.Select(s => s.Id).ToList(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetStoresByIdsAsync(storeIds)).ReturnsAsync(stores); + _repo.Setup(r => r.UpdateStore(It.IsAny())).Throws(new Exception("db error")); + + await Assert.ThrowsAsync(() => _sut.AssignStoresToRouteAsync(route.Id, new AssignStoresToRouteRequest(storeIds))); + + _uow.Verify(u => u.RollbackAsync(default), Times.Once); + _uow.Verify(u => u.CommitAsync(default), Times.Never); + } + + // AssignOwnerAsync + + [Fact] + public async Task AssignOwnerAsync_ReturnsNull_WhenRouteNotFound() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Models.Route?)null); + + var result = await _sut.AssignOwnerAsync(Guid.NewGuid(), new AssignRouteOwnerRequest(Guid.NewGuid())); + + Assert.Null(result); + } + + [Fact] + public async Task AssignOwnerAsync_ReturnsNull_WhenTargetUserNotActive() + { + var route = ActiveRoute(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetActiveUserAsync(It.IsAny())).ReturnsAsync((User?)null); + + var result = await _sut.AssignOwnerAsync(route.Id, new AssignRouteOwnerRequest(Guid.NewGuid())); + + Assert.Null(result); + _uow.Verify(u => u.BeginAsync(default), Times.Never); + } + + [Fact] + public async Task AssignOwnerAsync_SetsOwner_AndCommits() + { + var route = ActiveRoute(); + var salesRep = new User { Id = Guid.NewGuid(), IsActive = true }; + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetActiveUserAsync(salesRep.Id)).ReturnsAsync(salesRep); + + var result = await _sut.AssignOwnerAsync(route.Id, new AssignRouteOwnerRequest(salesRep.Id)); + + Assert.NotNull(result); + Assert.Equal(salesRep.Id, result.OwnerUserId); + Assert.Equal(salesRep.Id, route.FK_OwnerUserId); + _uow.Verify(u => u.CommitAsync(default), Times.Once); + } + + [Fact] + public async Task AssignOwnerAsync_AllowsReassignment_ToDifferentSalesRep() + { + var firstOwner = Guid.NewGuid(); + var route = ActiveRoute(firstOwner); + var newRep = new User { Id = Guid.NewGuid(), IsActive = true }; + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetActiveUserAsync(newRep.Id)).ReturnsAsync(newRep); + + var result = await _sut.AssignOwnerAsync(route.Id, new AssignRouteOwnerRequest(newRep.Id)); + + Assert.NotNull(result); + Assert.Equal(newRep.Id, result.OwnerUserId); + Assert.NotEqual(firstOwner, result.OwnerUserId); + } + + // RecordCheckInAsync + + [Fact] + public async Task RecordCheckInAsync_ReturnsNull_WhenCallerNotAuthenticated() + { + _currentUser.Setup(u => u.UserId).Returns((Guid?)null); + + var result = await _sut.RecordCheckInAsync(Guid.NewGuid(), new RecordCheckInRequest(Guid.NewGuid(), 1, 2, null)); + + Assert.Null(result); + } + + [Fact] + public async Task RecordCheckInAsync_ReturnsNull_WhenRouteNotFoundOrInactive() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync(InactiveRoute()); + + var result = await _sut.RecordCheckInAsync(Guid.NewGuid(), new RecordCheckInRequest(Guid.NewGuid(), 1, 2, null)); + + Assert.Null(result); + } + + [Fact] + public async Task RecordCheckInAsync_ReturnsNull_WhenCallerLacksRouteScope() + { + var route = ActiveRoute(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(false); + + var result = await _sut.RecordCheckInAsync(route.Id, new RecordCheckInRequest(Guid.NewGuid(), 1, 2, null)); + + Assert.Null(result); + } + + [Fact] + public async Task RecordCheckInAsync_ReturnsNull_WhenStoreNotOnRoute() + { + var route = ActiveRoute(); + var store = ActiveStore(Guid.NewGuid()); // belongs to a different route + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetStoreByIdAsync(store.Id)).ReturnsAsync(store); + + var result = await _sut.RecordCheckInAsync(route.Id, new RecordCheckInRequest(store.Id, 1, 2, null)); + + Assert.Null(result); + } + + [Fact] + public async Task RecordCheckInAsync_NotFlagged_WhenGpsMatchesStoreLocation() + { + var route = ActiveRoute(); + var store = ActiveStore(route.Id, lat: 12.9716m, lon: 77.5946m); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetStoreByIdAsync(store.Id)).ReturnsAsync(store); + + var result = await _sut.RecordCheckInAsync(route.Id, new RecordCheckInRequest(store.Id, 12.9716m, 77.5946m, null)); + + Assert.NotNull(result); + Assert.False(result.IsDeviationFlagged); + _repo.Verify(r => r.AddVisitLogAsync(It.IsAny()), Times.Once); + _uow.Verify(u => u.CommitAsync(default), Times.Once); + } + + [Fact] + public async Task RecordCheckInAsync_Flagged_WhenGpsFarFromStoreLocation() + { + var route = ActiveRoute(); + var store = ActiveStore(route.Id, lat: 12.9716m, lon: 77.5946m); // Bengaluru + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetStoreByIdAsync(store.Id)).ReturnsAsync(store); + + // Mumbai coordinates - hundreds of km away + var result = await _sut.RecordCheckInAsync(route.Id, new RecordCheckInRequest(store.Id, 19.0760m, 72.8777m, null)); + + Assert.NotNull(result); + Assert.True(result.IsDeviationFlagged); + } + + [Fact] + public async Task RecordCheckInAsync_UsesCurrentUserId_AsVisitingRep() + { + var route = ActiveRoute(); + var store = ActiveStore(route.Id); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetStoreByIdAsync(store.Id)).ReturnsAsync(store); + + var result = await _sut.RecordCheckInAsync(route.Id, new RecordCheckInRequest(store.Id, store.Latitude, store.Longitude, null)); + + Assert.NotNull(result); + Assert.Equal(RepUserId, result.UserId); + } + + // RecordCheckOutAsync + + [Fact] + public async Task RecordCheckOutAsync_ReturnsNull_WhenNoOpenVisit() + { + var route = ActiveRoute(); + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetOpenVisitAsync(route.Id, It.IsAny())).ReturnsAsync((RouteVisitLog?)null); + + var result = await _sut.RecordCheckOutAsync(route.Id, new RecordCheckOutRequest(Guid.NewGuid(), null)); + + Assert.Null(result); + _uow.Verify(u => u.BeginAsync(default), Times.Never); + } + + [Fact] + public async Task RecordCheckOutAsync_SetsCheckOutAt_AndCommits() + { + var route = ActiveRoute(); + var storeId = Guid.NewGuid(); + var log = new RouteVisitLog { Id = Guid.NewGuid(), FK_RouteId = route.Id, FK_StoreId = storeId, FK_UserId = RepUserId, CheckInAt = DateTime.Now.AddHours(-1) }; + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetOpenVisitAsync(route.Id, storeId)).ReturnsAsync(log); + + var result = await _sut.RecordCheckOutAsync(route.Id, new RecordCheckOutRequest(storeId, null)); + + Assert.NotNull(result); + Assert.NotNull(result.CheckOutAt); + Assert.NotNull(log.CheckOutAt); + _repo.Verify(r => r.UpdateVisitLog(log), Times.Once); + _uow.Verify(u => u.CommitAsync(default), Times.Once); + } + + [Fact] + public async Task RecordCheckOutAsync_Rollbacks_WhenUpdateThrows() + { + var route = ActiveRoute(); + var storeId = Guid.NewGuid(); + var log = new RouteVisitLog { Id = Guid.NewGuid(), FK_RouteId = route.Id, FK_StoreId = storeId, FK_UserId = RepUserId, CheckInAt = DateTime.Now }; + _repo.Setup(r => r.GetByIdAsync(route.Id)).ReturnsAsync(route); + _scopeAccess.Setup(s => s.HasRouteScopeAsync(RepUserId, route.Id)).ReturnsAsync(true); + _repo.Setup(r => r.GetOpenVisitAsync(route.Id, storeId)).ReturnsAsync(log); + _repo.Setup(r => r.UpdateVisitLog(It.IsAny())).Throws(new Exception("db error")); + + await Assert.ThrowsAsync(() => _sut.RecordCheckOutAsync(route.Id, new RecordCheckOutRequest(storeId, null))); + + _uow.Verify(u => u.RollbackAsync(default), Times.Once); + } +} diff --git a/Distribution.Tests/Services/StoreServiceTests.cs b/Distribution.Tests/Services/StoreServiceTests.cs new file mode 100644 index 0000000..6626009 --- /dev/null +++ b/Distribution.Tests/Services/StoreServiceTests.cs @@ -0,0 +1,85 @@ +using Distribution.Features.Item; +using Distribution.Features.Order; +using Distribution.Features.Route; +using Distribution.Features.Scheme; +using Distribution.Features.Stock; +using Distribution.Features.Store; +using Distribution.Infra.Authorization; +using Distribution.Infra.UoW; +using Distribution.Models; +using Moq; +using Xunit; + +namespace Distribution.Tests.Services; + +public class StoreServiceTests +{ + private readonly Mock _repo = new(); + private readonly Mock _uow = new(); + private readonly Mock _currentUser = new(); + private readonly Mock _scopeAccess = new(); + private readonly Mock _routeRepo = new(); + private readonly StoreService _sut; + + public StoreServiceTests() + { + _currentUser.Setup(u => u.RoleLevel).Returns(50); // global scope, skips per-store scope lookups + var schemeService = new SchemeService( + Mock.Of(), + Mock.Of(), + Mock.Of(), + _repo.Object, + Mock.Of(), + _uow.Object); + _sut = new StoreService(_repo.Object, _uow.Object, _currentUser.Object, _scopeAccess.Object, schemeService, _routeRepo.Object); + } + + private static Store ActiveStore() => + new() { Id = Guid.NewGuid(), IsActive = true }; + + // GetVisitHistoryAsync + + [Fact] + public async Task GetVisitHistoryAsync_ReturnsNull_WhenStoreNotFound() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Store?)null); + + var result = await _sut.GetVisitHistoryAsync(Guid.NewGuid()); + + Assert.Null(result); + } + + [Fact] + public async Task GetVisitHistoryAsync_ReturnsEmpty_WhenNoVisitsLogged() + { + var store = ActiveStore(); + _repo.Setup(r => r.GetByIdAsync(store.Id)).ReturnsAsync(store); + _routeRepo.Setup(r => r.GetVisitsByStoreIdAsync(store.Id)).ReturnsAsync([]); + + var result = await _sut.GetVisitHistoryAsync(store.Id); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public async Task GetVisitHistoryAsync_MapsVisits_MostRecentFirst() + { + var store = ActiveStore(); + var repId = Guid.NewGuid(); + var older = new RouteVisitLog { Id = Guid.NewGuid(), FK_StoreId = store.Id, FK_UserId = repId, CheckInAt = DateTime.Now.AddDays(-2), CheckOutAt = DateTime.Now.AddDays(-2).AddHours(1) }; + var newer = new RouteVisitLog { Id = Guid.NewGuid(), FK_StoreId = store.Id, FK_UserId = repId, CheckInAt = DateTime.Now.AddDays(-1), CheckOutAt = null }; + _repo.Setup(r => r.GetByIdAsync(store.Id)).ReturnsAsync(store); + _routeRepo.Setup(r => r.GetVisitsByStoreIdAsync(store.Id)).ReturnsAsync([older, newer]); + + var result = await _sut.GetVisitHistoryAsync(store.Id); + + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.Equal(newer.Id, result[0].VisitId); + Assert.Equal("In Progress", result[0].Outcome); + Assert.Equal(older.Id, result[1].VisitId); + Assert.Equal("Completed", result[1].Outcome); + Assert.Equal(repId, result[0].RepUserId); + } +}