package com.baiyue.warehouse.service; import com.baiyue.common.core.exception.BizException; import com.baiyue.warehouse.dto.WarehouseResourceLockRequests; import com.baiyue.warehouse.entity.WarehouseLocationEntity; import com.baiyue.warehouse.entity.WarehousePalletEntity; import com.baiyue.warehouse.entity.WarehouseResourceLockEntity; import com.baiyue.warehouse.repository.WarehouseResourceLockRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class WarehouseResourceLockServiceTest { private static final long TENANT = 101L; private static final long WAREHOUSE = 201L; private static final long PALLET = 301L; private static final long LOCATION = 401L; @Mock WarehouseResourceLockRepository repository; private WarehouseResourceLockService service; @BeforeEach void setUp() { service = new WarehouseResourceLockService(); ReflectionTestUtils.setField(service, "repository", repository); } @Test void atomicallyOccupiesIdlePalletAndLocation() { var request = request("reserve-1"); when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("IDLE")); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE")); when(repository.occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION)).thenReturn(1); when(repository.occupyLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(1); doAnswer(call -> { ((WarehouseResourceLockEntity) call.getArgument(0)).setId(501L); return null; }).when(repository).insert(any()); var result = service.reserve(request); assertThat(result.lockId()).isEqualTo(501L); assertThat(result.status()).isEqualTo("ACTIVE"); ArgumentCaptor lock = ArgumentCaptor.forClass(WarehouseResourceLockEntity.class); verify(repository).insert(lock.capture()); assertThat(lock.getValue().getReservationKey()).isEqualTo("reserve-1"); assertThat(lock.getValue().getPalletId()).isEqualTo(PALLET); assertThat(lock.getValue().getLocationId()).isEqualTo(LOCATION); } @Test void duplicateReservationReturnsExistingLockWithoutMutatingResources() { WarehouseResourceLockEntity existing = activeLock(); when(repository.findByReservationKey(TENANT, "reserve-1")).thenReturn(existing); var result = service.reserve(request("reserve-1")); assertThat(result.lockId()).isEqualTo(existing.getId()); verify(repository, never()).occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION); verify(repository, never()).insert(any()); } @Test void concurrentDuplicateReturnsExistingAfterResourcesBecomeOccupied() { WarehouseResourceLockEntity existing = activeLock(); when(repository.findByReservationKey(TENANT, "reserve-1")).thenReturn(null, existing); when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("OCCUPIED")); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED")); var result = service.reserve(request("reserve-1")); assertThat(result.lockId()).isEqualTo(existing.getId()); verify(repository, never()).occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION); } @Test void reservationKeyCannotBeReusedForDifferentResources() { WarehouseResourceLockEntity existing = activeLock(); existing.setPalletId(999L); when(repository.findByReservationKey(TENANT, "reserve-1")).thenReturn(existing); assertThatThrownBy(() -> service.reserve(request("reserve-1"))) .isInstanceOf(BizException.class) .hasMessageContaining("different resources"); } @Test void rejectsResourceOutsideTenantWarehouse() { when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(null); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE")); assertThatThrownBy(() -> service.reserve(request("reserve-1"))) .isInstanceOf(BizException.class) .hasMessageContaining("tenant warehouse"); verify(repository, never()).occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION); } @Test void rejectsNonIdleResources() { when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("OCCUPIED")); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE")); assertThatThrownBy(() -> service.reserve(request("reserve-1"))) .isInstanceOf(BizException.class) .hasMessageContaining("Only IDLE"); } @Test void releaseRestoresBothResourcesAndClosesLock() { WarehouseResourceLockEntity lock = activeLock(); when(repository.find(TENANT, lock.getId())).thenReturn(lock); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED")); when(repository.releasePallet(lock)).thenReturn(1); when(repository.releaseLocation(lock)).thenReturn(1); when(repository.close(lock, "RELEASED")).thenReturn(1); var result = service.release(TENANT, lock.getId()); assertThat(result.status()).isEqualTo("RELEASED"); verify(repository).releasePallet(lock); verify(repository).releaseLocation(lock); } @Test void scrapDiscardsPalletAndReleasesLocation() { WarehouseResourceLockEntity lock = activeLock(); when(repository.find(TENANT, lock.getId())).thenReturn(lock); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED")); when(repository.scrapPallet(lock)).thenReturn(1); when(repository.releaseLocation(lock)).thenReturn(1); when(repository.close(lock, "SCRAPPED")).thenReturn(1); var result = service.scrap(TENANT, lock.getId()); assertThat(result.status()).isEqualTo("SCRAPPED"); verify(repository).scrapPallet(lock); verify(repository).releaseLocation(lock); } @Test void concurrentLocationClaimFailsWholeOperation() { when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("IDLE")); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE")); when(repository.occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION)).thenReturn(1); when(repository.occupyLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(0); assertThatThrownBy(() -> service.reserve(request("reserve-1"))) .isInstanceOf(BizException.class) .hasMessageContaining("no longer IDLE"); verify(repository, never()).insert(any()); } @Test void virtualLocationIsNeverOccupiedOrReleased() { WarehouseLocationEntity virtual = location("ACTIVE"); virtual.setLocationType("VIRTUAL"); when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("IDLE")); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual); when(repository.occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION)).thenReturn(1); doAnswer(call -> { ((WarehouseResourceLockEntity) call.getArgument(0)).setId(502L); return null; }).when(repository).insert(any()); service.reserve(request("virtual-reserve")); verify(repository, never()).occupyLocation(TENANT, WAREHOUSE, LOCATION); } @Test void scrappingAtVirtualLocationKeepsLocationUntouched() { WarehouseResourceLockEntity lock = activeLock(); WarehouseLocationEntity virtual = location("ACTIVE"); virtual.setLocationType("VIRTUAL"); when(repository.find(TENANT, lock.getId())).thenReturn(lock); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual); when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1); when(repository.close(lock, "SCRAPPED")).thenReturn(1); var result = service.scrap(TENANT, lock.getId()); assertThat(result.status()).isEqualTo("SCRAPPED"); verify(repository).scrapPalletAtVirtualLocation(lock); verify(repository, never()).releaseLocation(lock); } @Test void scrapByPalletClosesItsActiveLockAndKeepsLocationReference() { WarehousePalletEntity pallet = pallet("OCCUPIED"); pallet.setWarehouseId(WAREHOUSE); pallet.setLocationId(LOCATION); WarehouseResourceLockEntity lock = activeLock(); when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet); when(repository.findActiveByPallet(TENANT, PALLET)).thenReturn(lock); when(repository.find(TENANT, lock.getId())).thenReturn(lock); WarehouseLocationEntity virtual = location("ACTIVE"); virtual.setLocationType("VIRTUAL"); when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual); when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1); when(repository.close(lock, "SCRAPPED")).thenReturn(1); var result = service.scrapByPallet(TENANT, PALLET); assertThat(result.status()).isEqualTo("SCRAPPED"); assertThat(result.locationId()).isEqualTo(LOCATION); verify(repository).scrapPalletAtVirtualLocation(lock); } @Test void sharedDropshipPalletIsScrappedThroughItsActualLockOwner() { WarehousePalletEntity pallet = pallet("OCCUPIED"); pallet.setPalletCode("DP-260910-0001"); pallet.setWarehouseId(WAREHOUSE); pallet.setLocationId(LOCATION); WarehouseResourceLockEntity lock = activeLock(); lock.setCustomerId(TENANT + 1); when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet); when(repository.findAnyActiveByPallet(PALLET)).thenReturn(lock); when(repository.find(TENANT + 1, lock.getId())).thenReturn(lock); WarehouseLocationEntity virtual = location("ACTIVE"); virtual.setLocationType("VIRTUAL"); when(repository.findLocation(TENANT + 1, WAREHOUSE, LOCATION)).thenReturn(virtual); when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1); when(repository.close(lock, "SCRAPPED")).thenReturn(1); assertThat(service.scrapByPallet(TENANT, PALLET).status()).isEqualTo("SCRAPPED"); verify(repository).findAnyActiveByPallet(PALLET); verify(repository).find(TENANT + 1, lock.getId()); } @Test void scrapByPalletIsIdempotentWhenAlreadyScrapped() { WarehousePalletEntity pallet = pallet("SCRAPPED"); pallet.setWarehouseId(WAREHOUSE); pallet.setLocationId(LOCATION); when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet); assertThat(service.scrapByPallet(TENANT, PALLET).status()).isEqualTo("SCRAPPED"); verify(repository, never()).findActiveByPallet(TENANT, PALLET); } private static WarehouseResourceLockRequests.ReserveRequest request(String key) { return new WarehouseResourceLockRequests.ReserveRequest(TENANT, WAREHOUSE, PALLET, LOCATION, "INBOUND_PLAN_PALLET", "plan-pallet-1", key); } private static WarehousePalletEntity pallet(String status) { WarehousePalletEntity entity = new WarehousePalletEntity(); entity.setStatus(status); return entity; } private static WarehouseLocationEntity location(String status) { WarehouseLocationEntity entity = new WarehouseLocationEntity(); entity.setStatus(status); return entity; } private static WarehouseResourceLockEntity activeLock() { WarehouseResourceLockEntity lock = new WarehouseResourceLockEntity(); lock.setId(501L); lock.setCustomerId(TENANT); lock.setWarehouseId(WAREHOUSE); lock.setPalletId(PALLET); lock.setLocationId(LOCATION); lock.setBusinessType("INBOUND_PLAN_PALLET"); lock.setBusinessId("plan-pallet-1"); lock.setReservationKey("reserve-1"); lock.setStatus("ACTIVE"); return lock; } }