Files
RuoYi-Vue/ruoyi-office/src/test/java/com/ruoyi/office/service/TtScreenshotArchiveServiceTest.java

267 lines
9.4 KiB
Java

package com.ruoyi.office.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.office.domain.TtCode;
import com.ruoyi.office.domain.TtFile;
import com.ruoyi.office.mapper.TtCodeMapper;
import com.ruoyi.office.mapper.TtFileMapper;
import com.sun.net.httpserver.HttpServer;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.ArgumentMatchers.any;
@RunWith(MockitoJUnitRunner.class)
public class TtScreenshotArchiveServiceTest
{
@InjectMocks
private TtScreenshotArchiveService service;
@Mock
private TtCodeMapper codeMapper;
@Mock
private TtFileMapper fileMapper;
private HttpServer imageServer;
private String imageBaseUrl;
@Before
public void setUp() throws IOException
{
imageServer = HttpServer.create(new InetSocketAddress(0), 0);
imageServer.createContext("/home.png", exchange -> {
byte[] content = "first-image".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "image/png");
exchange.sendResponseHeaders(200, content.length);
exchange.getResponseBody().write(content);
exchange.close();
});
imageServer.createContext("/redirect-home", exchange -> {
exchange.getResponseHeaders().set("Location", "/home.png");
exchange.sendResponseHeaders(301, -1);
exchange.close();
});
imageServer.createContext("/redirect-loop", exchange -> {
exchange.getResponseHeaders().set("Location", "/redirect-loop");
exchange.sendResponseHeaders(302, -1);
exchange.close();
});
imageServer.createContext("/detail", exchange -> {
byte[] content = "second-image".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "image/jpeg");
exchange.sendResponseHeaders(200, content.length);
exchange.getResponseBody().write(content);
exchange.close();
});
imageServer.createContext("/中文截图.png", exchange -> {
byte[] content = "unicode-image".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "image/png");
exchange.sendResponseHeaders(200, content.length);
exchange.getResponseBody().write(content);
exchange.close();
});
imageServer.start();
imageBaseUrl = "http://127.0.0.1:" + imageServer.getAddress().getPort();
}
@After
public void tearDown()
{
imageServer.stop(0);
}
@Test
public void downloadsScreenshotsWithStableNumberedNames() throws Exception
{
TtCode code = new TtCode();
code.setCodeId(9L);
code.setCodeName("【K001】药店进销存系统");
TtFile first = screenshot("首页", imageBaseUrl + "/redirect-home");
TtFile second = screenshot("详情图.jpg", imageBaseUrl + "/detail");
when(codeMapper.selectTtCodeByCodeId(9L)).thenReturn(code);
when(fileMapper.selectTtFileByCodeName(code.getCodeName()))
.thenReturn(Arrays.asList(first, second));
MockHttpServletResponse response = new MockHttpServletResponse();
service.download(9L, response);
Assert.assertEquals("application/zip", response.getContentType());
Assert.assertEquals(FileUtils.percentEncode("【K001】药店进销存系统截图.zip"),
response.getHeader("download-filename"));
List<String> names = new ArrayList<String>();
List<String> contents = new ArrayList<String>();
try (ZipInputStream zip = new ZipInputStream(
new ByteArrayInputStream(response.getContentAsByteArray()), StandardCharsets.UTF_8))
{
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null)
{
names.add(entry.getName());
contents.add(readText(zip));
}
}
Assert.assertEquals(Arrays.asList("01_首页.png", "02_详情图.jpg"), names);
Assert.assertEquals(Arrays.asList("first-image", "second-image"), contents);
}
@Test
public void rejectsProjectWithoutScreenshots() throws Exception
{
TtCode code = new TtCode();
code.setCodeName("无截图项目");
when(codeMapper.selectTtCodeByCodeId(10L)).thenReturn(code);
when(fileMapper.selectTtFileByCodeName(code.getCodeName())).thenReturn(new ArrayList<TtFile>());
try
{
service.download(10L, new MockHttpServletResponse());
Assert.fail("应拒绝下载空压缩包");
}
catch (ServiceException exception)
{
Assert.assertEquals("该项目暂无截图", exception.getMessage());
}
}
@Test
public void downloadsScreenshotWithUnicodeUrlPath() throws Exception
{
TtCode code = new TtCode();
code.setCodeName("中文路径项目");
when(codeMapper.selectTtCodeByCodeId(14L)).thenReturn(code);
when(fileMapper.selectTtFileByCodeName(code.getCodeName()))
.thenReturn(Arrays.asList(screenshot("中文截图", imageBaseUrl + "/中文截图.png")));
MockHttpServletResponse response = new MockHttpServletResponse();
service.download(14L, response);
try (ZipInputStream zip = new ZipInputStream(
new ByteArrayInputStream(response.getContentAsByteArray()), StandardCharsets.UTF_8))
{
Assert.assertEquals("01_中文截图.png", zip.getNextEntry().getName());
Assert.assertEquals("unicode-image", readText(zip));
}
}
@Test
public void rejectsMissingProject() throws Exception
{
when(codeMapper.selectTtCodeByCodeId(12L)).thenReturn(null);
try
{
service.download(12L, new MockHttpServletResponse());
Assert.fail("应拒绝下载不存在项目的截图");
}
catch (ServiceException exception)
{
Assert.assertEquals("源码项目不存在", exception.getMessage());
}
}
@Test
public void rejectsUnsupportedScreenshotUrl() throws Exception
{
TtCode code = new TtCode();
code.setCodeName("无效链接项目");
when(codeMapper.selectTtCodeByCodeId(13L)).thenReturn(code);
when(fileMapper.selectTtFileByCodeName(code.getCodeName()))
.thenReturn(Arrays.asList(screenshot("本地图片", "file:///tmp/image.png")));
try
{
service.download(13L, new MockHttpServletResponse());
Assert.fail("应拒绝非HTTP截图地址");
}
catch (ServiceException exception)
{
Assert.assertEquals("截图“本地图片”仅支持HTTP或HTTPS图片链接", exception.getMessage());
}
}
@Test
public void rejectsRedirectLoop() throws Exception
{
TtCode code = new TtCode();
code.setCodeName("循环跳转项目");
when(codeMapper.selectTtCodeByCodeId(11L)).thenReturn(code);
when(fileMapper.selectTtFileByCodeName(code.getCodeName()))
.thenReturn(Arrays.asList(screenshot("循环图片", imageBaseUrl + "/redirect-loop")));
try
{
service.download(11L, new MockHttpServletResponse());
Assert.fail("应拒绝循环重定向");
}
catch (ServiceException exception)
{
Assert.assertEquals("截图“循环图片”重定向次数过多", exception.getMessage());
}
}
@Test
public void sanitizesInvalidCharactersInScreenshotName()
{
Assert.assertEquals("03_功能_首页.webp",
TtScreenshotArchiveService.buildEntryName(
3, "功能/首页", "https://example.com/image", "image/webp"));
}
@Test
public void configuresCertificateBypassOnlyForHttpsConnections()
{
HttpsURLConnection https = mock(HttpsURLConnection.class);
TtScreenshotArchiveService.disableCertificateValidation(https);
verify(https).setSSLSocketFactory(any(SSLSocketFactory.class));
}
private TtFile screenshot(String name, String url)
{
TtFile file = new TtFile();
file.setFileName(name);
file.setFileUrl(url);
return file;
}
private String readText(ZipInputStream input) throws IOException
{
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[128];
int length;
while ((length = input.read(buffer)) != -1)
{
output.write(buffer, 0, length);
}
return new String(output.toByteArray(), StandardCharsets.UTF_8);
}
}