267 lines
9.4 KiB
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);
|
|
}
|
|
}
|