新需求更新 同步更新

This commit is contained in:
supernijia
2026-08-06 01:11:54 +08:00
parent 9048bbb7f8
commit 28e7fce11c
112 changed files with 7739 additions and 637 deletions
@@ -0,0 +1,53 @@
package com.nanri.aiimage.modules.filetemplate;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.server.ResponseStatusException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class ModuleTemplateControllerTest {
@Test
void returnsXlsxWithUtf8AttachmentFilename() throws Exception {
ModuleTemplateService service = mock(ModuleTemplateService.class);
byte[] bytes = {1, 2, 3, 4};
when(service.download("publish")).thenReturn(new ModuleTemplateService.TemplateDownload(
"上架 文档格式.xlsx",
ModuleTemplateService.XLSX_CONTENT_TYPE,
bytes));
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new ModuleTemplateController(service)).build();
mockMvc.perform(get("/api/module-templates/publish/download").param("user_id", "1"))
.andExpect(status().isOk())
.andExpect(content().bytes(bytes))
.andExpect(content().contentType(ModuleTemplateService.XLSX_CONTENT_TYPE))
.andExpect(header().longValue(HttpHeaders.CONTENT_LENGTH, bytes.length))
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"download.xlsx\"; "
+ "filename*=UTF-8''%E4%B8%8A%E6%9E%B6%20%E6%96%87%E6%A1%A3%E6%A0%BC%E5%BC%8F.xlsx"));
}
@Test
void propagatesUnknownModuleAndStorageStatusCodes() throws Exception {
ModuleTemplateService service = mock(ModuleTemplateService.class);
when(service.download("missing"))
.thenThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "模板不存在"));
when(service.download("publish"))
.thenThrow(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "模板存储暂不可用"));
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new ModuleTemplateController(service)).build();
mockMvc.perform(get("/api/module-templates/missing/download"))
.andExpect(status().isNotFound());
mockMvc.perform(get("/api/module-templates/publish/download"))
.andExpect(status().isServiceUnavailable());
}
}
@@ -0,0 +1,51 @@
package com.nanri.aiimage.modules.filetemplate;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.io.InputStream;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ModuleTemplateRegistryTest {
@Test
void exposesSixFixedModulesWithPackagedXlsxResources() throws Exception {
ModuleTemplateRegistry registry = new ModuleTemplateRegistry();
assertEquals(Set.of(
"publish",
"delete-brand",
"appearance-patent",
"price-track",
"collect-data",
"similar-asin"),
registry.templates().stream()
.map(ModuleTemplateRegistry.ModuleTemplate::moduleCode)
.collect(Collectors.toSet()));
for (ModuleTemplateRegistry.ModuleTemplate template : registry.templates()) {
assertTrue(template.resourcePath().matches("templates/module-input/[a-z-]+\\.xlsx"));
assertEquals("input/" + template.resourcePath().substring(template.resourcePath().lastIndexOf('/') + 1),
template.objectKey());
ClassPathResource resource = new ClassPathResource(template.resourcePath());
assertTrue(resource.exists(), template.resourcePath());
try (InputStream input = resource.getInputStream()) {
assertArrayEquals(new byte[]{'P', 'K'}, input.readNBytes(2), template.resourcePath());
}
}
}
@Test
void lookupNormalizesCaseAndWhitespaceWithoutAcceptingUnknownCodes() {
ModuleTemplateRegistry registry = new ModuleTemplateRegistry();
assertEquals("上架 文档格式.xlsx", registry.find(" PUBLISH ").orElseThrow().downloadFilename());
assertTrue(registry.find("../../publish").isEmpty());
assertTrue(registry.find(null).isEmpty());
}
}
@@ -0,0 +1,120 @@
package com.nanri.aiimage.modules.filetemplate;
import com.nanri.aiimage.config.OssProperties;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class ModuleTemplateServiceTest {
private static final String BUCKET = "aiimage-templates";
private OssStorageService ossStorageService;
private ModuleTemplateService service;
@BeforeEach
void setUp() {
OssProperties properties = new OssProperties();
properties.setTemplateBucket(BUCKET);
ossStorageService = mock(OssStorageService.class);
service = new ModuleTemplateService(new ModuleTemplateRegistry(), properties, ossStorageService);
}
@Test
void downloadsExistingObjectWithoutUploadingItAgain() {
byte[] stored = {1, 2, 3};
when(ossStorageService.objectExists(BUCKET, "input/publish.xlsx")).thenReturn(true);
when(ossStorageService.readObjectBytes(BUCKET, "input/publish.xlsx")).thenReturn(stored);
ModuleTemplateService.TemplateDownload download = service.download("publish");
assertEquals("上架 文档格式.xlsx", download.filename());
assertEquals(ModuleTemplateService.XLSX_CONTENT_TYPE, download.contentType());
assertArrayEquals(stored, download.content());
verify(ossStorageService).ensureBucketExists(BUCKET);
verify(ossStorageService, never()).uploadBytes(
eq(BUCKET), eq("input/publish.xlsx"), org.mockito.ArgumentMatchers.any(), anyString());
}
@Test
void lazilyUploadsMissingObjectBeforeReadingItFromStorage() {
byte[] stored = {4, 5, 6};
when(ossStorageService.objectExists(BUCKET, "input/publish.xlsx")).thenReturn(false);
when(ossStorageService.readObjectBytes(BUCKET, "input/publish.xlsx")).thenReturn(stored);
ModuleTemplateService.TemplateDownload download = service.download("publish");
assertArrayEquals(stored, download.content());
ArgumentCaptor<byte[]> uploaded = ArgumentCaptor.forClass(byte[].class);
verify(ossStorageService).uploadBytes(
eq(BUCKET), eq("input/publish.xlsx"), uploaded.capture(),
eq(ModuleTemplateService.XLSX_CONTENT_TYPE));
assertTrue(uploaded.getValue().length > 2);
assertArrayEquals(new byte[]{'P', 'K'}, new byte[]{uploaded.getValue()[0], uploaded.getValue()[1]});
verify(ossStorageService).readObjectBytes(BUCKET, "input/publish.xlsx");
}
@Test
void startupSynchronizationUploadsOnlyMissingTemplates() {
when(ossStorageService.objectExists(BUCKET, "input/publish.xlsx")).thenReturn(true);
assertEquals(5, service.synchronizeAll());
verify(ossStorageService).ensureBucketExists(BUCKET);
verify(ossStorageService, times(6)).objectExists(eq(BUCKET), anyString());
verify(ossStorageService, never()).uploadBytes(
eq(BUCKET), eq("input/publish.xlsx"), org.mockito.ArgumentMatchers.any(), anyString());
verify(ossStorageService, times(5)).uploadBytes(
eq(BUCKET), anyString(), org.mockito.ArgumentMatchers.any(),
eq(ModuleTemplateService.XLSX_CONTENT_TYPE));
}
@Test
void unknownModuleReturnsNotFoundWithoutTouchingStorage() {
ResponseStatusException ex = assertThrows(ResponseStatusException.class,
() -> service.download("not-a-module"));
assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode());
verifyNoInteractions(ossStorageService);
}
@Test
void storageFailureReturnsServiceUnavailable() {
doThrow(new IllegalStateException("MinIO unavailable"))
.when(ossStorageService).ensureBucketExists(BUCKET);
ResponseStatusException ex = assertThrows(ResponseStatusException.class,
() -> service.download("publish"));
assertEquals(HttpStatus.SERVICE_UNAVAILABLE, ex.getStatusCode());
}
@Test
void initializerLeavesApplicationRunningWhenStartupStorageIsUnavailable() {
ModuleTemplateService unavailableService = mock(ModuleTemplateService.class);
doThrow(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "unavailable"))
.when(unavailableService).synchronizeAll();
ModuleTemplateStorageInitializer initializer = new ModuleTemplateStorageInitializer(unavailableService);
assertDoesNotThrow(initializer::synchronize);
verify(unavailableService).synchronizeAll();
}
}