Files
baiyue-firstleg/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehouseResourceLockServiceTest.java
T

287 lines
12 KiB
Java

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<WarehouseResourceLockEntity> 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;
}
}