This commit is contained in:
2026-08-07 14:46:55 +08:00
parent 830575cc92
commit b0ea1a0c5c
109 changed files with 5774 additions and 339 deletions
@@ -0,0 +1,67 @@
package com.baiyue.file.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.file.service.PriceBookFileService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InternalPriceBookFileController.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InternalPriceBookFileControllerTest {
@MockBean PriceBookFileService service;
@Resource InternalPriceBookFileController controller;
@Test
void uploadsWithAuthenticatedOperatorAndBusinessId() {
var file = new MockMultipartFile("file", "price.xlsx", "application/octet-stream", new byte[]{1});
var metadata = metadata();
when(service.upload(41L, 7L, file)).thenReturn(metadata);
var response = controller.upload("internal-token", 7L, 41L, file);
assertThat(response.data()).isEqualTo(metadata);
verify(service).upload(41L, 7L, file);
}
@Test
void rejectsInvalidToken() {
assertThatThrownBy(() -> controller.metadata("bad-token", 7L, 41L, 99L))
.isInstanceOf(BizException.class).hasMessageContaining("凭证无效");
}
@Test
void rejectsMissingOperatorOrBusinessOwnership() {
assertThatThrownBy(() -> controller.metadata("internal-token", 0L, 41L, 99L))
.isInstanceOf(BizException.class).hasMessageContaining("必须大于0");
assertThatThrownBy(() -> controller.metadata("internal-token", 7L, 0L, 99L))
.isInstanceOf(BizException.class).hasMessageContaining("必须大于0");
}
@Test
void downloadsAsAttachmentWithoutCaching() {
PriceBookFileService.FileMetadata metadata = metadata();
when(service.download(41L, 99L)).thenReturn(
new PriceBookFileService.Download(metadata, new byte[]{1, 2, 3}));
var response = controller.download("internal-token", 7L, 41L, 99L);
assertThat(response.getBody()).containsExactly(1, 2, 3);
assertThat(response.getHeaders().getCacheControl()).contains("no-store");
assertThat(response.getHeaders().getContentDisposition().isAttachment()).isTrue();
}
private static PriceBookFileService.FileMetadata metadata() {
return new PriceBookFileService.FileMetadata(99L, 41L, "price.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 3L, "hash",
PriceBookFileService.BUSINESS_TYPE);
}
}
@@ -0,0 +1,79 @@
package com.baiyue.file.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.file.entity.FileObjectEntity;
import com.baiyue.file.mapper.FileObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
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.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PriceBookFileServiceTest {
@Mock PriceBookFileStorage storage;
@Mock FileObjectMapper files;
@InjectMocks PriceBookFileService service;
@Test
void persistsPlatformOwnedPriceBookMetadata() {
var file = new MockMultipartFile("file", "price.xlsx", PriceBookFileStorage.XLSX_CONTENT_TYPE,
new byte[]{1});
when(storage.store(41L, file)).thenReturn(new PriceBookFileStorage.Stored(
"price-books/0/41/a.xlsx", "price.xlsx", PriceBookFileStorage.XLSX_CONTENT_TYPE,
1L, "hash"));
when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> {
FileObjectEntity object = invocation.getArgument(0);
object.setId(99L);
return 1;
});
PriceBookFileService.FileMetadata result = service.upload(41L, 7L, file);
assertThat(result.fileId()).isEqualTo(99L);
assertThat(result.businessId()).isEqualTo(41L);
assertThat(result.businessType()).isEqualTo(PriceBookFileService.BUSINESS_TYPE);
verify(files).insert(any(FileObjectEntity.class));
}
@Test
void rejectsDownloadWhenBusinessOwnershipDoesNotMatch() {
when(files.selectOne(any(Wrapper.class))).thenReturn(null);
assertThatThrownBy(() -> service.download(42L, 99L))
.isInstanceOf(BizException.class).hasMessageContaining("不存在");
}
@Test
void downloadsOnlyOwnedPriceBookObject() {
FileObjectEntity object = object(42L, 99L);
when(files.selectOne(any(Wrapper.class))).thenReturn(object);
when(storage.read("price-books/0/42/a.xlsx")).thenReturn(new byte[]{1, 2});
PriceBookFileService.Download result = service.download(42L, 99L);
assertThat(result.content()).containsExactly(1, 2);
assertThat(result.metadata().businessId()).isEqualTo(42L);
}
private static FileObjectEntity object(long businessId, long fileId) {
FileObjectEntity object = new FileObjectEntity();
object.setId(fileId);
object.setTenantId(0L);
object.setBusinessId(Long.toString(businessId));
object.setBusinessType(PriceBookFileService.BUSINESS_TYPE);
object.setOriginalName("price.xlsx");
object.setObjectKey("price-books/0/" + businessId + "/a.xlsx");
object.setContentType(PriceBookFileStorage.XLSX_CONTENT_TYPE);
object.setSizeBytes(2L);
object.setSha256("hash");
return object;
}
}
@@ -0,0 +1,97 @@
package com.baiyue.file.service;
import com.baiyue.common.core.exception.BizException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class PriceBookFileStorageTest {
private static final String XLSX = PriceBookFileStorage.XLSX_CONTENT_TYPE;
@TempDir Path root;
private PriceBookFileStorage storage;
@BeforeEach
void setUp() {
storage = new PriceBookFileStorage();
ReflectionTestUtils.setField(storage, "configuredRoot", root);
ReflectionTestUtils.setField(storage, "maxSizeBytes", 1024L * 1024);
storage.initializeRoot();
}
@Test
void storesValidPackageEvenWhenTableMetadataIsMalformed() throws IOException {
Map<String, byte[]> entries = requiredEntries();
entries.put("xl/tables/table1.xml", "<table ref=\"5:31\">".getBytes());
var file = new MockMultipartFile("file", "2026-price.xlsx", XLSX, zip(entries));
PriceBookFileStorage.Stored stored = storage.store(101L, file);
assertThat(stored.originalName()).isEqualTo("2026-price.xlsx");
assertThat(stored.objectKey()).matches("price-books/0/101/[0-9a-f-]{36}\\.xlsx");
assertThat(stored.sha256()).hasSize(64);
assertThat(Files.isRegularFile(root.resolve(stored.objectKey()))).isTrue();
}
@Test
void rejectsFakeXlsxExtension() {
var file = new MockMultipartFile("file", "fake.xlsx", XLSX, "not-a-zip".getBytes());
assertThatThrownBy(() -> storage.store(1L, file))
.isInstanceOf(BizException.class).hasMessageContaining("有效XLSX");
}
@Test
void rejectsOversizedFile() throws IOException {
ReflectionTestUtils.setField(storage, "maxSizeBytes", 8L);
var file = new MockMultipartFile("file", "large.xlsx", XLSX, zip(requiredEntries()));
assertThatThrownBy(() -> storage.store(1L, file))
.isInstanceOf(BizException.class).hasMessageContaining("超过允许大小");
}
@Test
void rejectsTraversalFileName() throws IOException {
var file = new MockMultipartFile("file", "../price.xlsx", XLSX, zip(requiredEntries()));
assertThatThrownBy(() -> storage.store(1L, file))
.isInstanceOf(BizException.class).hasMessageContaining("文件名不安全");
}
@Test
void rejectsWrongMimeType() throws IOException {
var file = new MockMultipartFile("file", "price.xlsx", "text/plain", zip(requiredEntries()));
assertThatThrownBy(() -> storage.store(1L, file))
.isInstanceOf(BizException.class).hasMessageContaining("类型不正确");
}
private static Map<String, byte[]> requiredEntries() {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put("[Content_Types].xml", "<Types/>".getBytes());
entries.put("xl/workbook.xml", "<workbook/>".getBytes());
entries.put("xl/_rels/workbook.xml.rels", "<Relationships/>".getBytes());
return entries;
}
private static byte[] zip(Map<String, byte[]> entries) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ZipOutputStream zip = new ZipOutputStream(bytes)) {
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
zip.putNextEntry(new ZipEntry(entry.getKey()));
zip.write(entry.getValue());
zip.closeEntry();
}
}
return bytes.toByteArray();
}
}