优化
This commit is contained in:
+67
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user