暂存提交

This commit is contained in:
2026-08-19 16:24:02 +08:00
parent f5c4bf130a
commit 6acb6178da
354 changed files with 25000 additions and 198 deletions
@@ -0,0 +1,29 @@
package com.baiyue.warehouse.config;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class WarehouseDatabaseGuardConfigurationTest {
@Test
void acceptsDedicatedOverseasWarehouseDatabase() {
MockEnvironment environment = new MockEnvironment()
.withProperty("spring.datasource.url",
"jdbc:mysql://127.0.0.1:3306/baiyue_warehouse_overseas?serverTimezone=Asia/Shanghai");
assertThatCode(() -> WarehouseDatabaseGuardConfiguration.warehouseDatabaseGuard(environment))
.doesNotThrowAnyException();
}
@Test
void rejectsAnyOtherDatabaseBeforeSchemaInitialization() {
MockEnvironment environment = new MockEnvironment()
.withProperty("spring.datasource.url", "jdbc:mysql://127.0.0.1:3306/baiyue_warehouse");
assertThatThrownBy(() -> WarehouseDatabaseGuardConfiguration.warehouseDatabaseGuard(environment))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("baiyue_warehouse_overseas");
}
}
@@ -0,0 +1,69 @@
package com.baiyue.warehouse.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
import com.baiyue.warehouse.service.WarehouseResourceService;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class WarehouseResourceControllerTest {
private final WarehouseResourceService service = mock(WarehouseResourceService.class);
@Test
void merchantTenantAlwaysComesFromAuthenticatedUser() {
MerchantWarehouseResourceController controller = merchantController();
AuthenticatedUser merchant = user(10001L, "MERCHANT");
WarehouseLocationEntity expected = new WarehouseLocationEntity();
when(service.location(10001L, 20001L, 30001L)).thenReturn(expected);
controller.location(merchant, 20001L, 30001L);
verify(service).location(10001L, 20001L, 30001L);
}
@Test
void platformApiRejectsNonPlatformIdentity() {
PlatformWarehouseResourceController controller = platformController();
assertThatThrownBy(() -> controller.location(user(10001L, "MERCHANT"),
10002L, 20001L, 30001L))
.isInstanceOf(BizException.class)
.hasMessageContaining("平台端");
}
@Test
void platformApiUsesExplicitTargetTenant() {
PlatformWarehouseResourceController controller = platformController();
WarehouseLocationEntity expected = new WarehouseLocationEntity();
when(service.location(10002L, 20001L, 30001L)).thenReturn(expected);
controller.location(user(1L, "PLATFORM"), 10002L, 20001L, 30001L);
verify(service).location(10002L, 20001L, 30001L);
}
private static AuthenticatedUser user(long customerId, String appCode) {
return new AuthenticatedUser(1L, customerId, customerId, 1L,
"tester", appCode, "WEB", Set.of());
}
private MerchantWarehouseResourceController merchantController() {
MerchantWarehouseResourceController controller = new MerchantWarehouseResourceController();
ReflectionTestUtils.setField(controller, "service", service);
return controller;
}
private PlatformWarehouseResourceController platformController() {
PlatformWarehouseResourceController controller = new PlatformWarehouseResourceController();
ReflectionTestUtils.setField(controller, "service", service);
return controller;
}
}
@@ -0,0 +1,98 @@
package com.baiyue.warehouse.schema;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SchemaChineseCommentContractTest {
private static final Pattern TABLE = Pattern.compile(
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(wh_[a-z0-9_]+)"
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB\\s+COMMENT\\s*=\\s*'([^']*)'\\s*;");
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
@Test
void allWarehouseTablesAndColumnsHaveChineseCommentsAndMigrationCoverage() throws Exception {
String baseline = Files.readString(
Path.of("src", "main", "resources", "db", "warehouse.sql"), StandardCharsets.UTF_8);
String incremental = Files.readString(
Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8);
List<TableDefinition> tables = parseAndAssert(baseline, "仓储基线 SQL");
assertEquals(7, tables.size(), "仓储表数量变化时必须同步表索引和备注迁移");
parseAndAssert(incremental, "统一增量 SQL");
int marker = incremental.indexOf("2026-08-18 / 025 Chinese table and column comments");
assertTrue(marker >= 0, "统一增量 SQL 缺少 025 中文备注迁移");
String migration = incremental.substring(marker);
for (TableDefinition table : tables) {
assertTrue(migration.contains("table_name='" + table.name() + "'"),
"025 备注迁移缺少表: " + table.name());
for (String column : table.columns()) {
assertTrue(migration.contains("table_name='" + table.name()
+ "' AND column_name='" + column + "'"),
"025 备注迁移缺少字段: " + table.name() + "." + column);
}
}
}
private static List<TableDefinition> parseAndAssert(String sql, String source) {
List<TableDefinition> tables = new ArrayList<>();
Matcher matcher = TABLE.matcher(sql);
while (matcher.find()) {
String table = matcher.group(1);
assertTrue(CHINESE.matcher(matcher.group(3)).find(), source + " 表备注必须包含中文: " + table);
List<String> columns = new ArrayList<>();
for (String definition : splitTopLevel(matcher.group(2))) {
String normalized = definition.stripLeading().toUpperCase();
if (normalized.matches("^(PRIMARY|UNIQUE|KEY|CONSTRAINT|FOREIGN|CHECK)\\b.*")) {
continue;
}
String column = definition.stripLeading().split("\\s+", 2)[0].replace("`", "");
columns.add(column);
assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s+'[^']*[\\u4e00-\\u9fff][^']*'")
.matcher(definition).find(),
source + " 字段备注必须包含中文: " + table + "." + column);
}
assertFalse(columns.isEmpty(), source + " 表定义没有字段: " + table);
tables.add(new TableDefinition(table, columns));
}
assertFalse(tables.isEmpty(), source + " 未解析到仓储表");
return tables;
}
private static List<String> splitTopLevel(String body) {
List<String> definitions = new ArrayList<>();
int start = 0;
int parentheses = 0;
boolean quoted = false;
for (int i = 0; i < body.length(); i++) {
char current = body.charAt(i);
if (current == '\'' && (i == 0 || body.charAt(i - 1) != '\\')) {
quoted = !quoted;
} else if (!quoted && current == '(') {
parentheses++;
} else if (!quoted && current == ')') {
parentheses--;
} else if (!quoted && parentheses == 0 && current == ',') {
definitions.add(body.substring(start, i).trim());
start = i + 1;
}
}
definitions.add(body.substring(start).trim());
return definitions.stream().filter(value -> !value.isEmpty()).toList();
}
private record TableDefinition(String name, List<String> columns) {
}
}
@@ -0,0 +1,24 @@
package com.baiyue.warehouse.schema;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
class WarehouseResourceLockSchemaTest {
@Test
void schemaContainsIdempotentResourceLockContract() throws IOException {
String sql;
try (var stream = getClass().getResourceAsStream("/db/warehouse.sql")) {
assertThat(stream).isNotNull();
sql = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_resource_lock");
assertThat(sql).contains("UNIQUE KEY uk_resource_lock_reservation (customer_id, reservation_key)");
assertThat(sql).contains("KEY idx_resource_lock_pallet (customer_id, warehouse_id, pallet_id, status)");
assertThat(sql).contains("KEY idx_resource_lock_location (customer_id, warehouse_id, location_id, status)");
}
}
@@ -0,0 +1,32 @@
package com.baiyue.warehouse.security;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class InternalServiceTokenGuardTest {
private final InternalServiceTokenGuard guard =
new InternalServiceTokenGuard("phase1-local-internal-token-at-least-32-bytes");
@Test
void acceptsConfiguredToken() {
assertThatCode(() -> guard.verify("phase1-local-internal-token-at-least-32-bytes"))
.doesNotThrowAnyException();
}
@Test
void rejectsMissingOrWrongToken() {
assertUnauthorized(null);
assertUnauthorized("wrong");
}
private void assertUnauthorized(String token) {
assertThatThrownBy(() -> guard.verify(token))
.isInstanceOfSatisfying(ResponseStatusException.class,
error -> assertThat(error.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED));
}
}
@@ -0,0 +1,59 @@
package com.baiyue.warehouse.service;
import com.baiyue.warehouse.dto.WarehouseRequests;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
import com.baiyue.warehouse.repository.WarehouseRepository;
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.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class WarehouseApplicationServiceTest {
@Mock WarehouseRepository repository;
private WarehouseApplicationService service;
@BeforeEach
void setUp() {
service = new WarehouseApplicationService();
ReflectionTestUtils.setField(service, "repository", repository);
}
@Test
void warehouseCreationAlsoCreatesActiveDefaultVirtualLocation() {
doAnswer(call -> {
((WarehouseEntity) call.getArgument(0)).setId(20001L);
return null;
}).when(repository).insertWarehouse(any());
service.create(request());
ArgumentCaptor<WarehouseLocationEntity> location = ArgumentCaptor.forClass(WarehouseLocationEntity.class);
verify(repository).insertLocation(location.capture());
assertThat(location.getValue().getCustomerId()).isEqualTo(10001L);
assertThat(location.getValue().getWarehouseId()).isEqualTo(20001L);
assertThat(location.getValue().getLocationCode()).isEqualTo("LOC-US-LAX-01-DEFAULT");
assertThat(location.getValue().getLocationType()).isEqualTo("VIRTUAL");
assertThat(location.getValue().getStatus()).isEqualTo("ACTIVE");
}
private static WarehouseRequests.SaveRequest request() {
WarehouseRequests.AddressRequest address = new WarehouseRequests.AddressRequest(
"WAREHOUSE", "US", "CA", "Los Angeles", null, "90001",
"1 Logistics Ave", null, null, null);
return new WarehouseRequests.SaveRequest(10001L, "US-LAX-01", "LAX Warehouse",
"OVERSEAS", "US", "America/Los_Angeles", null, null, null,
"ACTIVE", null, address, List.of());
}
}
@@ -0,0 +1,263 @@
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 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;
}
}
@@ -0,0 +1,260 @@
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;
import com.baiyue.warehouse.entity.WarehouseEntity;
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;
private WarehouseResourceService service;
@BeforeEach
void setUp() {
service = new WarehouseResourceService();
ReflectionTestUtils.setField(service, "repository", repository);
}
@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 createsLocationWithSnowflakeAssignedByMapper() {
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
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);
}
@Test
void rejectsResourceAccessWhenWarehouseDoesNotBelongToTenant() {
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(false);
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.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
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() {
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
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.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
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.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
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() {
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
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() {
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
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() {
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
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() {
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
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.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
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() {
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
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() {
when(repository.findWarehouse(TENANT, WAREHOUSE)).thenReturn(warehouse());
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() {
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
WarehousePalletEntity existing = new WarehousePalletEntity();
existing.setId(40002L);
existing.setPalletCode("PLT-OLD");
existing.setStatus("SCRAPPED");
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)))
.isInstanceOf(BizException.class)
.hasMessageContaining("cannot be reused");
}
private static WarehouseEntity warehouse() {
WarehouseEntity warehouse = new WarehouseEntity();
warehouse.setId(WAREHOUSE);
warehouse.setCustomerId(TENANT);
warehouse.setWarehouseCode("US-LAX-01");
return warehouse;
}
}