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

337 lines
15 KiB
Java
Raw Normal View History

2026-08-19 16:24:02 +08:00
package com.baiyue.warehouse.service;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.LocationRequests;
import com.baiyue.warehouse.dto.PalletRequests;
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
import com.baiyue.warehouse.entity.WarehousePalletEntity;
2026-08-26 13:58:33 +08:00
import com.baiyue.warehouse.dto.SharedWarehouseReference;
2026-08-19 16:24:02 +08:00
import com.baiyue.warehouse.repository.WarehouseResourceRepository;
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 java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
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 WarehouseResourceServiceTest {
private static final long TENANT = 10001L;
private static final long WAREHOUSE = 20001L;
@Mock
private WarehouseResourceRepository repository;
2026-08-26 13:58:33 +08:00
@Mock
private DomesticWarehouseDirectory warehouseDirectory;
2026-08-19 16:24:02 +08:00
private WarehouseResourceService service;
@BeforeEach
void setUp() {
service = new WarehouseResourceService();
ReflectionTestUtils.setField(service, "repository", repository);
2026-08-26 13:58:33 +08:00
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory);
org.mockito.Mockito.lenient().when(warehouseDirectory.required(WAREHOUSE))
.thenReturn(warehouse());
2026-08-19 16:24:02 +08:00
}
@Test
void resourceIdsUseMybatisSnowflakeAssignment() throws NoSuchFieldException {
assertThat(WarehouseLocationEntity.class.getDeclaredField("id").getAnnotation(TableId.class).type())
.isEqualTo(IdType.ASSIGN_ID);
assertThat(WarehousePalletEntity.class.getDeclaredField("id").getAnnotation(TableId.class).type())
.isEqualTo(IdType.ASSIGN_ID);
}
@Test
void resolvesOnlyPublicLocationInsideBoundWarehouse() {
WarehouseLocationEntity location = new WarehouseLocationEntity(); location.setId(30001L);
location.setWarehouseId(WAREHOUSE); location.setCustomerId(0L); location.setLocationCode("A-01");
location.setLocationType("STORAGE"); location.setMaxLoadKg(new BigDecimal("1000")); location.setStatus("IDLE");
when(repository.findLocationByCode(0L, WAREHOUSE, "A-01")).thenReturn(location);
var result = service.resolvePublicLocation(WAREHOUSE, " A-01 ");
assertThat(result.locationId()).isEqualTo(30001L);
assertThat(result.locationCode()).isEqualTo("A-01");
}
2026-08-19 16:24:02 +08:00
@Test
void createsLocationWithSnowflakeAssignedByMapper() {
doAnswer(invocation -> {
((WarehouseLocationEntity) invocation.getArgument(0)).setId(2089000000000000001L);
return null;
}).when(repository).insertLocation(org.mockito.ArgumentMatchers.any());
Long id = service.createLocation(TENANT, WAREHOUSE, locationRequest("A-01"));
assertThat(id).isEqualTo(2089000000000000001L);
ArgumentCaptor<WarehouseLocationEntity> captor = ArgumentCaptor.forClass(WarehouseLocationEntity.class);
verify(repository).insertLocation(captor.capture());
assertThat(captor.getValue().getCustomerId()).isEqualTo(TENANT);
assertThat(captor.getValue().getWarehouseId()).isEqualTo(WAREHOUSE);
}
2026-08-20 18:02:38 +08:00
@Test
void platformPalletUsesPublicScopeAgainstAnyExistingWarehouse() {
WarehouseLocationEntity location = new WarehouseLocationEntity();
location.setId(30001L);
when(repository.findLocation(0L, WAREHOUSE, 30001L)).thenReturn(location);
service.createPallet(0L, WAREHOUSE, new PalletRequests.SaveRequest(
"PLT-PUBLIC", "STANDARD", null, BigDecimal.ONE, "IDLE", 30001L, null));
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
verify(repository).insertPallet(captor.capture());
assertThat(captor.getValue().getCustomerId()).isZero();
assertThat(captor.getValue().getWarehouseId()).isEqualTo(WAREHOUSE);
}
2026-08-19 16:24:02 +08:00
@Test
void rejectsResourceAccessWhenWarehouseDoesNotBelongToTenant() {
2026-08-26 13:58:33 +08:00
when(warehouseDirectory.required(WAREHOUSE)).thenThrow(new BizException(
com.baiyue.common.core.api.ResultCode.NOT_FOUND, "仓库不存在"));
2026-08-19 16:24:02 +08:00
assertThatThrownBy(() -> service.createLocation(TENANT, WAREHOUSE, locationRequest("A-01")))
.isInstanceOf(BizException.class)
.hasMessageContaining("仓库不存在");
verify(repository, never()).insertLocation(org.mockito.ArgumentMatchers.any());
}
@Test
void rejectsDuplicateLocationCodeWithinWarehouse() {
when(repository.locationCodeExists(TENANT, WAREHOUSE, "A-01", null)).thenReturn(true);
assertThatThrownBy(() -> service.createLocation(TENANT, WAREHOUSE, locationRequest("A-01")))
.isInstanceOf(BizException.class)
.hasMessageContaining("库位编码已存在");
}
@Test
void palletDefaultsToTwoCubicMetresAndBindsOwnedLocation() {
WarehouseLocationEntity location = new WarehouseLocationEntity();
location.setId(30001L);
when(repository.findLocation(TENANT, WAREHOUSE, 30001L)).thenReturn(location);
doAnswer(invocation -> {
((WarehousePalletEntity) invocation.getArgument(0)).setId(2089000000000000002L);
return null;
}).when(repository).insertPallet(org.mockito.ArgumentMatchers.any());
Long id = service.createPallet(TENANT, WAREHOUSE,
new PalletRequests.SaveRequest("PLT-01", "STANDARD", null,
new BigDecimal("12.50"), "IDLE", 30001L, null));
assertThat(id).isEqualTo(2089000000000000002L);
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
verify(repository).insertPallet(captor.capture());
assertThat(captor.getValue().getCapacityCbm()).isEqualByComparingTo("2.0000");
assertThat(captor.getValue().getLocationId()).isEqualTo(30001L);
}
@Test
void rejectsLocationFromAnotherWarehouseWhenBindingPallet() {
when(repository.findLocation(TENANT, WAREHOUSE, 30002L)).thenReturn(null);
assertThatThrownBy(() -> service.createPallet(TENANT, WAREHOUSE,
new PalletRequests.SaveRequest("PLT-01", "STANDARD", null,
BigDecimal.ONE, "IDLE", 30002L, null)))
.isInstanceOf(BizException.class)
.hasMessageContaining("库位不存在");
verify(repository, never()).insertPallet(org.mockito.ArgumentMatchers.any());
}
@Test
void preventsDeletingLocationThatStillHasPallets() {
when(repository.findLocation(TENANT, WAREHOUSE, 30001L)).thenReturn(new WarehouseLocationEntity());
when(repository.locationHasPallets(TENANT, WAREHOUSE, 30001L)).thenReturn(true);
assertThatThrownBy(() -> service.deleteLocation(TENANT, WAREHOUSE, 30001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("仍绑定托盘");
verify(repository, never()).deleteLocation(30001L);
}
@Test
void preventsDeletingOccupiedPallet() {
WarehousePalletEntity pallet = new WarehousePalletEntity();
pallet.setStatus("OCCUPIED");
when(repository.findPallet(TENANT, WAREHOUSE, 40001L)).thenReturn(pallet);
assertThatThrownBy(() -> service.deletePallet(TENANT, WAREHOUSE, 40001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("占用中的托盘");
verify(repository, never()).deletePallet(40001L);
}
@Test
void preventsDeletingScrappedPalletSoItsIdentityCannotBeReused() {
WarehousePalletEntity pallet = new WarehousePalletEntity();
pallet.setStatus("SCRAPPED");
when(repository.findPallet(TENANT, WAREHOUSE, 40003L)).thenReturn(pallet);
assertThatThrownBy(() -> service.deletePallet(TENANT, WAREHOUSE, 40003L))
.isInstanceOf(BizException.class)
.hasMessageContaining("cannot be reused");
verify(repository, never()).deletePallet(40003L);
}
@Test
void rejectsUnknownPalletStatus() {
assertThatThrownBy(() -> service.createPallet(TENANT, WAREHOUSE,
new PalletRequests.SaveRequest("PLT-01", "STANDARD", null,
BigDecimal.ONE, "BROKEN", null, null)))
.isInstanceOf(BizException.class)
.hasMessageContaining("IDLE、OCCUPIED、SCRAPPED");
}
private static LocationRequests.SaveRequest locationRequest(String code) {
return new LocationRequests.SaveRequest("A", code, "STORAGE",
new BigDecimal("1000.000"), "ACTIVE", null);
}
@Test
void generatesRestartSafePalletCodeAndUsesDefaultVirtualLocation() {
WarehouseLocationEntity location = new WarehouseLocationEntity();
location.setId(30003L);
location.setLocationType("VIRTUAL");
location.setStatus("ACTIVE");
when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT"))
.thenReturn(location);
when(repository.findLocation(TENANT, WAREHOUSE, 30003L)).thenReturn(location);
service.createPallet(TENANT, WAREHOUSE,
new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", null, null));
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
verify(repository).insertPallet(captor.capture());
assertThat(captor.getValue().getPalletCode()).matches("PLT-US-LAX-01-[0-9]{8}-[0-9]+");
assertThat(captor.getValue().getLocationId()).isEqualTo(30003L);
assertThat(captor.getValue().getId()).isNotNull();
}
@Test
void defaultVirtualLocationIsInsertedIdempotentlyForAnExistingWarehouse() {
WarehouseLocationEntity resolved = new WarehouseLocationEntity();
resolved.setId(30004L);
resolved.setLocationType("VIRTUAL");
resolved.setStatus("ACTIVE");
when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT"))
.thenReturn(null, resolved);
WarehouseLocationEntity result = service.ensureDefaultVirtualLocation(TENANT, WAREHOUSE);
assertThat(result.getId()).isEqualTo(30004L);
ArgumentCaptor<WarehouseLocationEntity> location = ArgumentCaptor.forClass(WarehouseLocationEntity.class);
verify(repository).insertDefaultVirtualLocationIgnore(location.capture());
assertThat(location.getValue().getLocationCode()).isEqualTo("LOC-US-LAX-01-DEFAULT");
assertThat(location.getValue().getLocationType()).isEqualTo("VIRTUAL");
assertThat(location.getValue().getStatus()).isEqualTo("ACTIVE");
}
@Test
void rejectsAConflictingDefaultLocationCode() {
WarehouseLocationEntity conflict = new WarehouseLocationEntity();
conflict.setLocationType("STORAGE");
conflict.setStatus("ACTIVE");
when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT"))
.thenReturn(conflict);
assertThatThrownBy(() -> service.ensureDefaultVirtualLocation(TENANT, WAREHOUSE))
.isInstanceOf(BizException.class)
.hasMessageContaining("Default virtual location code");
verify(repository, never()).insertDefaultVirtualLocationIgnore(org.mockito.ArgumentMatchers.any());
}
@Test
void scrappedPalletCannotBeReactivated() {
WarehousePalletEntity existing = new WarehousePalletEntity();
existing.setId(40002L);
existing.setPalletCode("PLT-OLD");
existing.setStatus("SCRAPPED");
existing.setVersion(0);
2026-08-19 16:24:02 +08:00
when(repository.findPallet(TENANT, WAREHOUSE, 40002L)).thenReturn(existing);
assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40002L,
new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", null, null, null, 0)))
2026-08-19 16:24:02 +08:00
.isInstanceOf(BizException.class)
.hasMessageContaining("cannot be reused");
}
2026-08-21 11:43:03 +08:00
@Test
void updatePalletChangesCodeAndFbaWarehouseCode() {
WarehousePalletEntity existing = new WarehousePalletEntity();
existing.setId(40003L);
existing.setPalletCode("PLT-OLD");
existing.setStatus("IDLE");
existing.setVersion(2);
2026-08-21 11:43:03 +08:00
when(repository.findPallet(TENANT, WAREHOUSE, 40003L)).thenReturn(existing);
when(repository.updatePallet(org.mockito.ArgumentMatchers.any())).thenReturn(1);
2026-08-21 11:43:03 +08:00
service.updatePallet(TENANT, WAREHOUSE, 40003L,
new PalletRequests.SaveRequest("PLT-NEW", "WOOD", new BigDecimal("2.5000"),
new BigDecimal("25.00"), "IDLE", null, "updated", "ONT8", 2));
2026-08-21 11:43:03 +08:00
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
verify(repository).palletCodeExists(TENANT, WAREHOUSE, "PLT-NEW", 40003L);
verify(repository).updatePallet(captor.capture());
assertThat(captor.getValue().getPalletCode()).isEqualTo("PLT-NEW");
assertThat(captor.getValue().getFbaWarehouseCode()).isEqualTo("ONT8");
assertThat(captor.getValue().getVersion()).isEqualTo(2);
2026-08-21 11:43:03 +08:00
}
@Test
void updatePalletRejectsDuplicateCodeInSameWarehouse() {
WarehousePalletEntity existing = new WarehousePalletEntity();
existing.setId(40004L);
existing.setPalletCode("PLT-OLD");
existing.setStatus("IDLE");
existing.setVersion(0);
2026-08-21 11:43:03 +08:00
when(repository.findPallet(TENANT, WAREHOUSE, 40004L)).thenReturn(existing);
when(repository.palletCodeExists(TENANT, WAREHOUSE, "PLT-DUP", 40004L)).thenReturn(true);
assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40004L,
new PalletRequests.SaveRequest("PLT-DUP", "WOOD", BigDecimal.ONE,
BigDecimal.ONE, "IDLE", null, null, "ONT8", 0)))
2026-08-21 11:43:03 +08:00
.isInstanceOf(BizException.class)
.hasMessageContaining("托盘编码已存在");
verify(repository, never()).updatePallet(org.mockito.ArgumentMatchers.any());
}
@Test
void updatePalletRejectsStaleVersionBeforeMutation() {
WarehousePalletEntity existing = new WarehousePalletEntity();
existing.setId(40005L);
existing.setPalletCode("PLT-OLD");
existing.setStatus("IDLE");
existing.setVersion(3);
when(repository.findPallet(TENANT, WAREHOUSE, 40005L)).thenReturn(existing);
assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40005L,
new PalletRequests.SaveRequest("PLT-STALE", "WOOD", BigDecimal.ONE,
BigDecimal.ONE, "IDLE", null, null, "ONT8", 2)))
.isInstanceOf(BizException.class)
.hasMessageContaining("版本已变化");
verify(repository, never()).palletCodeExists(org.mockito.ArgumentMatchers.anyLong(),
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.any());
verify(repository, never()).updatePallet(org.mockito.ArgumentMatchers.any());
}
2026-08-26 13:58:33 +08:00
private static SharedWarehouseReference warehouse() {
return new SharedWarehouseReference(WAREHOUSE, "US-LAX-01", "Los Angeles", "ACTIVE");
2026-08-19 16:24:02 +08:00
}
}