Commit 03ce4fad5766bd85d318b9d6fb4cf8e43dad92d1
1 parent
223a4d81
feat(infra): 增加文件上传类型白名单校验
Showing
5 changed files
with
279 additions
and
10 deletions
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java
| ... | ... | @@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.Parameters; |
| 12 | 12 | import io.swagger.v3.oas.annotations.media.Schema; |
| 13 | 13 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 14 | 14 | import jakarta.annotation.Resource; |
| 15 | -import jakarta.annotation.security.PermitAll; | |
| 16 | 15 | import jakarta.validation.Valid; |
| 17 | 16 | import lombok.extern.slf4j.Slf4j; |
| 18 | 17 | import org.springframework.validation.annotation.Validated; | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
| ... | ... | @@ -34,6 +34,7 @@ public interface ErrorCodeConstants { |
| 34 | 34 | ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在"); |
| 35 | 35 | ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空"); |
| 36 | 36 | ErrorCode FILE_PATH_INVALID = new ErrorCode(1_001_003_003, "文件路径不正确"); |
| 37 | + ErrorCode FILE_TYPE_NOT_ALLOWED = new ErrorCode(1_001_003_004, "不支持的文件类型"); | |
| 37 | 38 | |
| 38 | 39 | // ========== 代码生成器 1-001-004-000 ========== |
| 39 | 40 | ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在"); | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
| ... | ... | @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.infra.service.file; |
| 3 | 3 | import cn.hutool.core.date.LocalDateTimeUtil; |
| 4 | 4 | import cn.hutool.core.io.FileUtil; |
| 5 | 5 | import cn.hutool.core.lang.Assert; |
| 6 | +import cn.hutool.core.util.ArrayUtil; | |
| 7 | +import cn.hutool.core.util.ObjectUtil; | |
| 6 | 8 | import cn.hutool.core.util.RandomUtil; |
| 7 | 9 | import cn.hutool.core.util.StrUtil; |
| 8 | 10 | import cn.hutool.crypto.digest.DigestUtil; |
| ... | ... | @@ -23,10 +25,11 @@ import lombok.SneakyThrows; |
| 23 | 25 | import org.springframework.stereotype.Service; |
| 24 | 26 | |
| 25 | 27 | import java.util.List; |
| 28 | +import java.util.Set; | |
| 26 | 29 | |
| 27 | 30 | import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN; |
| 28 | 31 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 29 | -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; | |
| 32 | +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; | |
| 30 | 33 | |
| 31 | 34 | /** |
| 32 | 35 | * 文件 Service 实现类 |
| ... | ... | @@ -37,6 +40,29 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX |
| 37 | 40 | public class FileServiceImpl implements FileService { |
| 38 | 41 | |
| 39 | 42 | /** |
| 43 | + * 允许上传的文件类型,可按业务需要调整 | |
| 44 | + */ | |
| 45 | + private static final Set<String> ALLOWED_FILE_TYPES = Set.of( | |
| 46 | + // 图片 | |
| 47 | + "image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp", | |
| 48 | + "image/vnd.microsoft.icon", "image/tiff", "image/avif", "image/heic", "image/heif", "image/svg+xml", | |
| 49 | + // 文档 | |
| 50 | + "application/pdf", "text/plain", "text/csv", "text/markdown", "text/html", "application/xhtml+xml", | |
| 51 | + "application/xml", "application/json", "application/epub+zip", "message/rfc822", "application/vnd.ms-outlook", | |
| 52 | + "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", | |
| 53 | + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| 54 | + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| 55 | + "application/vnd.openxmlformats-officedocument.presentationml.presentation", | |
| 56 | + // 压缩包 | |
| 57 | + "application/zip", "application/x-rar-compressed", "application/x-7z-compressed", "application/gzip", | |
| 58 | + // 音视频 | |
| 59 | + "audio/mpeg", "audio/vnd.wave", "audio/vorbis", "audio/ogg", "audio/opus", "audio/mp4", "audio/webm", | |
| 60 | + "audio/x-aac", "audio/x-flac", "audio/amr", "audio/x-ms-wma", | |
| 61 | + "video/mp4", "video/quicktime", "video/x-msvideo", "video/webm", "video/x-ms-wmv", "video/x-flv", | |
| 62 | + "application/x-matroska", "video/x-matroska" | |
| 63 | + ); | |
| 64 | + | |
| 65 | + /** | |
| 40 | 66 | * 上传文件的前缀,是否包含日期(yyyyMMdd) |
| 41 | 67 | * |
| 42 | 68 | * 目的:按照日期,进行分目录 |
| ... | ... | @@ -74,9 +100,15 @@ public class FileServiceImpl implements FileService { |
| 74 | 100 | // 1.1 处理 name 的合法性,禁止携带目录路径 |
| 75 | 101 | name = FilePathUtils.validateFileName(name); |
| 76 | 102 | |
| 77 | - // 1.2.1 处理 type 为空的情况 | |
| 78 | - if (StrUtil.isEmpty(type)) { | |
| 79 | - type = FileTypeUtils.getMineType(content, name); | |
| 103 | + // 1.2.1 校验文件内容和类型,不信任客户端传入的 type | |
| 104 | + if (ArrayUtil.isEmpty(content)) { | |
| 105 | + throw exception(FILE_IS_EMPTY); | |
| 106 | + } | |
| 107 | + type = FileTypeUtils.getMineType(content, name); | |
| 108 | + String nameType = StrUtil.isNotEmpty(FileUtil.extName(name)) ? validateFileType(name) : null; | |
| 109 | + if (!ALLOWED_FILE_TYPES.contains(type) | |
| 110 | + || (FileTypeUtils.isImage(nameType) && ObjectUtil.notEqual(type, nameType))) { | |
| 111 | + throw exception(FILE_TYPE_NOT_ALLOWED); | |
| 80 | 112 | } |
| 81 | 113 | // 1.2.2 处理 name 为空的情况 |
| 82 | 114 | if (StrUtil.isEmpty(name)) { |
| ... | ... | @@ -148,8 +180,10 @@ public class FileServiceImpl implements FileService { |
| 148 | 180 | @Override |
| 149 | 181 | @SneakyThrows |
| 150 | 182 | public FilePresignedUrlRespVO presignPutUrl(String name, String directory) { |
| 151 | - // 1. 生成上传的 path,需要保证唯一 | |
| 183 | + // 1.1 生成上传的 path,需要保证唯一 | |
| 152 | 184 | String path = generateUploadPath(name, directory); |
| 185 | + // 1.2 前端直传无法读取文件内容,此处仅校验文件名类型 | |
| 186 | + validateFileType(name); | |
| 153 | 187 | |
| 154 | 188 | // 2. 获取文件预签名地址 |
| 155 | 189 | FileClient fileClient = fileConfigService.getMasterFileClient(); |
| ... | ... | @@ -170,7 +204,13 @@ public class FileServiceImpl implements FileService { |
| 170 | 204 | // 1.1 校验参数的合法性 |
| 171 | 205 | FilePathUtils.validatePath(createReqVO.getPath()); |
| 172 | 206 | createReqVO.setName(FilePathUtils.validateFileName(createReqVO.getName())); |
| 173 | - // 1.2 处理 URL 的合法性,移除 URL 中的查询参数(例如签名参数),保证 URL 的唯一性 | |
| 207 | + // 1.2 校验原文件名和存储路径的类型,前端直传不校验文件内容 | |
| 208 | + String type = validateFileType(createReqVO.getName()); | |
| 209 | + if (ObjectUtil.notEqual(type, validateFileType(FileUtil.getName(createReqVO.getPath())))) { | |
| 210 | + throw exception(FILE_TYPE_NOT_ALLOWED); | |
| 211 | + } | |
| 212 | + createReqVO.setType(type); | |
| 213 | + // 1.3 处理 URL 的合法性,移除 URL 中的查询参数(例如签名参数),保证 URL 的唯一性 | |
| 174 | 214 | createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数 |
| 175 | 215 | |
| 176 | 216 | // 2. 保存到数据库 |
| ... | ... | @@ -179,6 +219,14 @@ public class FileServiceImpl implements FileService { |
| 179 | 219 | return file.getId(); |
| 180 | 220 | } |
| 181 | 221 | |
| 222 | + private String validateFileType(String name) { | |
| 223 | + String type = FileTypeUtils.getMineType(name); | |
| 224 | + if (!ALLOWED_FILE_TYPES.contains(type)) { | |
| 225 | + throw exception(FILE_TYPE_NOT_ALLOWED); | |
| 226 | + } | |
| 227 | + return type; | |
| 228 | + } | |
| 229 | + | |
| 182 | 230 | @Override |
| 183 | 231 | public FileDO getFile(Long id) { |
| 184 | 232 | return validateFileExists(id); | ... | ... |
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
| ... | ... | @@ -7,23 +7,31 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; |
| 7 | 7 | import cn.iocoder.yudao.framework.test.core.util.AssertUtils; |
| 8 | 8 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; |
| 9 | 9 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; |
| 10 | +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; | |
| 10 | 11 | import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; |
| 11 | 12 | import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; |
| 12 | 13 | import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; |
| 13 | 14 | import jakarta.annotation.Resource; |
| 15 | +import org.apache.poi.hssf.usermodel.HSSFWorkbook; | |
| 16 | +import org.apache.poi.ss.usermodel.Workbook; | |
| 17 | +import org.apache.poi.xssf.usermodel.XSSFWorkbook; | |
| 14 | 18 | import org.junit.jupiter.api.BeforeEach; |
| 15 | 19 | import org.junit.jupiter.api.Test; |
| 16 | 20 | import org.springframework.context.annotation.Import; |
| 17 | 21 | import org.springframework.test.context.bean.override.mockito.MockitoBean; |
| 18 | 22 | |
| 23 | +import javax.imageio.ImageIO; | |
| 24 | +import java.awt.image.BufferedImage; | |
| 25 | +import java.io.ByteArrayOutputStream; | |
| 26 | +import java.nio.charset.StandardCharsets; | |
| 19 | 27 | import java.time.LocalDateTime; |
| 28 | +import java.util.Locale; | |
| 20 | 29 | import java.util.concurrent.atomic.AtomicReference; |
| 21 | 30 | |
| 22 | 31 | import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; |
| 23 | 32 | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; |
| 24 | 33 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; |
| 25 | -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; | |
| 26 | -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID; | |
| 34 | +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; | |
| 27 | 35 | import static org.junit.jupiter.api.Assertions.*; |
| 28 | 36 | import static org.mockito.ArgumentMatchers.same; |
| 29 | 37 | import static org.mockito.Mockito.*; |
| ... | ... | @@ -147,6 +155,198 @@ public class FileServiceImplTest extends BaseDbUnitTest { |
| 147 | 155 | } |
| 148 | 156 | |
| 149 | 157 | @Test |
| 158 | + public void testCreateFile_success_image() throws Exception { | |
| 159 | + for (String extension : new String[]{"jpg", "png", "gif", "bmp"}) { | |
| 160 | + // 准备参数 | |
| 161 | + String type = "jpg".equals(extension) ? "image/jpeg" : "image/" + extension; | |
| 162 | + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); | |
| 163 | + assertTrue(ImageIO.write(new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB), extension, outputStream)); | |
| 164 | + byte[] content = outputStream.toByteArray(); | |
| 165 | + String name = "头像." + extension.toUpperCase(Locale.ROOT); | |
| 166 | + // mock 方法 | |
| 167 | + FileClient client = mock(FileClient.class); | |
| 168 | + when(fileConfigService.getMasterFileClient()).thenReturn(client); | |
| 169 | + String url = randomString(); | |
| 170 | + when(client.upload(same(content), anyString(), eq(type))).thenReturn(url); | |
| 171 | + | |
| 172 | + // 调用 | |
| 173 | + String result = fileService.createFile(content, name, null, "application/octet-stream"); | |
| 174 | + // 断言 | |
| 175 | + assertEquals(url, result); | |
| 176 | + FileDO file = fileMapper.selectOne(FileDO::getUrl, url); | |
| 177 | + assertEquals(name, file.getName()); | |
| 178 | + assertEquals(type, file.getType()); | |
| 179 | + } | |
| 180 | + } | |
| 181 | + | |
| 182 | + @Test | |
| 183 | + public void testCreateFile_success_excel() throws Exception { | |
| 184 | + for (String extension : new String[]{"xls", "xlsx"}) { | |
| 185 | + // 准备参数 | |
| 186 | + String type = "xls".equals(extension) ? "application/vnd.ms-excel" | |
| 187 | + : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; | |
| 188 | + byte[] content; | |
| 189 | + try (Workbook workbook = "xls".equals(extension) ? new HSSFWorkbook() : new XSSFWorkbook(); | |
| 190 | + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { | |
| 191 | + workbook.createSheet("用户").createRow(0).createCell(0).setCellValue("芋道源码"); | |
| 192 | + workbook.write(outputStream); | |
| 193 | + content = outputStream.toByteArray(); | |
| 194 | + } | |
| 195 | + // mock 方法 | |
| 196 | + FileClient client = mock(FileClient.class); | |
| 197 | + when(fileConfigService.getMasterFileClient()).thenReturn(client); | |
| 198 | + String url = randomString(); | |
| 199 | + when(client.upload(same(content), anyString(), eq(type))).thenReturn(url); | |
| 200 | + | |
| 201 | + // 调用 | |
| 202 | + String result = fileService.createFile(content, "用户." + extension, null, null); | |
| 203 | + // 断言 | |
| 204 | + assertEquals(url, result); | |
| 205 | + assertEquals(type, fileMapper.selectOne(FileDO::getUrl, url).getType()); | |
| 206 | + } | |
| 207 | + } | |
| 208 | + | |
| 209 | + @Test | |
| 210 | + public void testCreateFile_success_detectType() throws Exception { | |
| 211 | + for (String declaredType : new String[]{null, "", "image/jpg", "text/html", "application/octet-stream"}) { | |
| 212 | + // 准备参数 | |
| 213 | + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); | |
| 214 | + // mock 方法 | |
| 215 | + FileClient client = mock(FileClient.class); | |
| 216 | + when(fileConfigService.getMasterFileClient()).thenReturn(client); | |
| 217 | + String url = randomString(); | |
| 218 | + when(client.upload(same(content), anyString(), eq("image/jpeg"))).thenReturn(url); | |
| 219 | + | |
| 220 | + // 调用 | |
| 221 | + String result = fileService.createFile(content, "头像.jpg", null, declaredType); | |
| 222 | + // 断言 | |
| 223 | + assertEquals(url, result); | |
| 224 | + assertEquals("image/jpeg", fileMapper.selectOne(FileDO::getUrl, url).getType()); | |
| 225 | + } | |
| 226 | + } | |
| 227 | + | |
| 228 | + @Test | |
| 229 | + public void testCreateFile_success_document() throws Exception { | |
| 230 | + for (String[] document : new String[][]{ | |
| 231 | + {"test.svg", "<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>", "image/svg+xml"}, | |
| 232 | + {"test.html", "<html><script>alert(1)</script></html>", "text/html"}, | |
| 233 | + {"test.xml", "<?xml version=\"1.0\"?><root>test</root>", "application/xml"}, | |
| 234 | + {"test.markdown", "# test\n\nhello", "text/markdown"}, | |
| 235 | + {"test.eml", "From: a@example.com\r\nTo: b@example.com\r\nSubject: test\r\n\r\nhello", "message/rfc822"}}) { | |
| 236 | + // 准备参数 | |
| 237 | + String name = document[0]; | |
| 238 | + byte[] content = document[1].getBytes(StandardCharsets.UTF_8); | |
| 239 | + String type = document[2]; | |
| 240 | + // mock 方法 | |
| 241 | + FileClient client = mock(FileClient.class); | |
| 242 | + when(fileConfigService.getMasterFileClient()).thenReturn(client); | |
| 243 | + String url = randomString(); | |
| 244 | + when(client.upload(same(content), anyString(), eq(type))).thenReturn(url); | |
| 245 | + | |
| 246 | + // 调用 | |
| 247 | + String result = fileService.createFile(content, name, null, "application/octet-stream"); | |
| 248 | + // 断言 | |
| 249 | + assertEquals(url, result); | |
| 250 | + assertEquals(type, fileMapper.selectOne(FileDO::getUrl, url).getType()); | |
| 251 | + } | |
| 252 | + } | |
| 253 | + | |
| 254 | + @Test | |
| 255 | + public void testCreateFile_success_opus() throws Exception { | |
| 256 | + // 准备参数 | |
| 257 | + byte[] content = new byte[64]; | |
| 258 | + System.arraycopy("OggS".getBytes(StandardCharsets.US_ASCII), 0, content, 0, 4); | |
| 259 | + System.arraycopy("OpusHead".getBytes(StandardCharsets.US_ASCII), 0, content, 28, 8); | |
| 260 | + // mock 方法 | |
| 261 | + FileClient client = mock(FileClient.class); | |
| 262 | + when(fileConfigService.getMasterFileClient()).thenReturn(client); | |
| 263 | + String url = randomString(); | |
| 264 | + when(client.upload(same(content), anyString(), eq("audio/opus"))).thenReturn(url); | |
| 265 | + | |
| 266 | + // 调用 | |
| 267 | + String result = fileService.createFile(content, "voice.ogg", null, "audio/ogg;codecs=opus"); | |
| 268 | + // 断言 | |
| 269 | + assertEquals(url, result); | |
| 270 | + assertEquals("audio/opus", fileMapper.selectOne(FileDO::getUrl, url).getType()); | |
| 271 | + } | |
| 272 | + | |
| 273 | + @Test | |
| 274 | + public void testCreateFile_typeNotAllowed_fakeImage() { | |
| 275 | + for (String text : new String[]{"<html><script>alert(1)</script></html>", | |
| 276 | + "<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>", "普通文本"}) { | |
| 277 | + // 准备参数 | |
| 278 | + byte[] content = text.getBytes(StandardCharsets.UTF_8); | |
| 279 | + | |
| 280 | + // 调用,并断言异常 | |
| 281 | + assertServiceException(() -> fileService.createFile(content, "头像.jpg", null, "image/jpeg"), | |
| 282 | + FILE_TYPE_NOT_ALLOWED); | |
| 283 | + // 断言未上传文件、未保存记录 | |
| 284 | + verifyNoInteractions(fileConfigService); | |
| 285 | + assertEquals(0L, fileMapper.selectCount()); | |
| 286 | + } | |
| 287 | + } | |
| 288 | + | |
| 289 | + @Test | |
| 290 | + public void testCreateFile_typeNotAllowed_extension() { | |
| 291 | + for (String name : new String[]{"头像.svg", "头像.js", "头像.exe", "头像.unknown", "头像.png"}) { | |
| 292 | + // 准备参数 | |
| 293 | + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); | |
| 294 | + | |
| 295 | + // 调用,并断言异常 | |
| 296 | + assertServiceException(() -> fileService.createFile(content, name, null, "image/jpeg"), FILE_TYPE_NOT_ALLOWED); | |
| 297 | + // 校验调用 | |
| 298 | + verifyNoInteractions(fileConfigService); | |
| 299 | + } | |
| 300 | + } | |
| 301 | + | |
| 302 | + @Test | |
| 303 | + public void testCreateFile_fileIsEmpty() { | |
| 304 | + // 准备参数 | |
| 305 | + byte[] content = new byte[0]; | |
| 306 | + | |
| 307 | + // 调用,并断言异常 | |
| 308 | + assertServiceException(() -> fileService.createFile(content, "头像.jpg", null, null), FILE_IS_EMPTY); | |
| 309 | + // 校验调用 | |
| 310 | + verifyNoInteractions(fileConfigService); | |
| 311 | + } | |
| 312 | + | |
| 313 | + @Test | |
| 314 | + public void testPresignPutUrl_success() { | |
| 315 | + for (String extension : new String[]{"jpg", "JPEG", "jfif", "png", "gif", "webp", "bmp", "ico", "tiff", | |
| 316 | + "avif", "heic", "heif", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv", "md", | |
| 317 | + "zip", "rar", "mp3", "mp4", "svg", "html", "htm", "xml", "markdown", "eml", "msg", "epub", | |
| 318 | + "json", "opus", "ogg", "m4a", "webm", "amr", "wma", "wmv", "flv", "mkv"}) { | |
| 319 | + // 准备参数 | |
| 320 | + String name = "测试文件." + extension; | |
| 321 | + // mock 方法 | |
| 322 | + FileClient client = mock(FileClient.class); | |
| 323 | + when(fileConfigService.getMasterFileClient()).thenReturn(client); | |
| 324 | + when(client.getId()).thenReturn(10L); | |
| 325 | + when(client.presignPutUrl(anyString())).thenReturn("upload-url"); | |
| 326 | + when(client.presignGetUrl(anyString(), isNull())).thenReturn("visit-url"); | |
| 327 | + | |
| 328 | + // 调用 | |
| 329 | + FilePresignedUrlRespVO result = fileService.presignPutUrl(name, "avatar"); | |
| 330 | + // 断言 | |
| 331 | + assertEquals(10L, result.getConfigId()); | |
| 332 | + assertTrue(result.getPath().endsWith("/" + name)); | |
| 333 | + assertEquals("upload-url", result.getUploadUrl()); | |
| 334 | + assertEquals("visit-url", result.getUrl()); | |
| 335 | + } | |
| 336 | + } | |
| 337 | + | |
| 338 | + @Test | |
| 339 | + public void testPresignPutUrl_typeNotAllowed() { | |
| 340 | + // 准备参数 | |
| 341 | + for (String name : new String[]{"test.php", "test.jsp", "test.js", "test.exe", "test.unknown", "test"}) { | |
| 342 | + // 调用,并断言异常 | |
| 343 | + assertServiceException(() -> fileService.presignPutUrl(name, null), FILE_TYPE_NOT_ALLOWED); | |
| 344 | + // 校验调用 | |
| 345 | + verifyNoInteractions(fileConfigService); | |
| 346 | + } | |
| 347 | + } | |
| 348 | + | |
| 349 | + @Test | |
| 150 | 350 | public void testDeleteFile_success() throws Exception { |
| 151 | 351 | // mock 数据 |
| 152 | 352 | FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg")); |
| ... | ... | @@ -245,6 +445,27 @@ public class FileServiceImplTest extends BaseDbUnitTest { |
| 245 | 445 | assertEquals("avatar/test.jpg", file.getPath()); |
| 246 | 446 | assertEquals("test.jpg", file.getName()); |
| 247 | 447 | assertEquals("https://www.iocoder.cn/test.jpg", file.getUrl()); |
| 448 | + assertEquals("image/jpeg", file.getType()); | |
| 449 | + } | |
| 450 | + | |
| 451 | + @Test | |
| 452 | + public void testCreateFileByPresignedPath_typeNotAllowed() { | |
| 453 | + for (String[] file : new String[][]{{"test.js", "avatar/test.js"}, {"test.jpg", "avatar/test.html"}, | |
| 454 | + {"test.html", "avatar/test.jpg"}, {"test.jpg", "avatar/test.png"}}) { | |
| 455 | + // 准备参数 | |
| 456 | + String name = file[0]; | |
| 457 | + String path = file[1]; | |
| 458 | + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> { | |
| 459 | + o.setName(name); | |
| 460 | + o.setPath(path); | |
| 461 | + o.setType("image/jpeg"); | |
| 462 | + }); | |
| 463 | + | |
| 464 | + // 调用,并断言异常 | |
| 465 | + assertServiceException(() -> fileService.createFile(reqVO), FILE_TYPE_NOT_ALLOWED); | |
| 466 | + // 断言未保存记录 | |
| 467 | + assertEquals(0L, fileMapper.selectCount()); | |
| 468 | + } | |
| 248 | 469 | } |
| 249 | 470 | |
| 250 | 471 | @Test | ... | ... |
yudao-module-infra/src/test/resources/sql/create_tables.sql
| ... | ... | @@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS "infra_file" ( |
| 37 | 37 | "name" varchar(256), |
| 38 | 38 | "path" varchar(512), |
| 39 | 39 | "url" varchar(1024), |
| 40 | - "type" varchar(63) DEFAULT NULL, | |
| 40 | + "type" varchar(128) DEFAULT NULL, | |
| 41 | 41 | "size" bigint NOT NULL, |
| 42 | 42 | "creator" varchar(64) DEFAULT '', |
| 43 | 43 | "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ... | ... |