提交代码
This commit is contained in:
+9
-4
@@ -13,21 +13,26 @@
|
||||
| 功能 | 入口 |
|
||||
|---|---|
|
||||
| 商户资质上传与读取 | `src/main/java/com/baiyue/file/controller/QualificationFileController.java` |
|
||||
| 平台资质查看 | `controller/PlatformQualificationFileController.java` |
|
||||
| 平台资质上传、替换与查看 | `controller/PlatformQualificationFileController.java`;`POST /{tenantId}` 支持 `qualificationType`、`certificateNo`、`validTo`、`replace`,下载为 `GET /{tenantId}/{fileName}` |
|
||||
| 服务状态 | `controller/FileHealthController.java` |
|
||||
| 当前本地磁盘实现 | `service/LocalQualificationStorage.java` |
|
||||
| 未被当前 Controller 使用的旧实现 | `service/QualificationFileStorage.java` |
|
||||
| IAM 权限协作 | `client/IamQualificationClient.java` |
|
||||
| IAM 权限协作 | `client/IamQualificationClient.java`;商户登记 `/qualifications`,平台登记/替换 `/platform-qualifications` |
|
||||
| 目标表结构(尚未接入) | `src/main/resources/db/file.sql` |
|
||||
| 历史资质文件迁移 | `../scripts/migrate-local-qualification-file.ps1`、`../docs/企业资质文件迁移说明.md`;只适用于当前本地/共享目录存储实现 |
|
||||
| 本地/Nacos 配置 | `src/main/resources/application.yml`、`../config/nacos/{local,cn}/file-service.yml` |
|
||||
| 上传契约与存储测试 | `src/test/java/com/baiyue/file/controller/QualificationFileContractTest.java`、`service/LocalQualificationStorageTest.java` |
|
||||
|
||||
## 修改与验证
|
||||
|
||||
- 严格校验租户、文件名/路径、大小、类型和读取权限,防止目录穿越、越权访问和任意文件上传。
|
||||
- 生产存储应使用对象存储或共享文件存储;容器本地目录仅适合当前本地实现,不得硬编码环境路径。
|
||||
- 接入元数据持久化时需同时实现 Entity、Mapper、Repository、事务和迁移验证,并同步检查 IAM 资质流程及平台/商户 Controller。
|
||||
- 当前没有专属自动化测试;从父目录运行 `mvn -pl file -am test`,并按接口影响验证上传、预览、无权限和非法文件场景。
|
||||
- 非本地环境必须显式配置 `FILE_STORAGE_ROOT` 并挂载持久化可写目录;服务启动时会校验目录,存储不可用时直接启动失败,避免健康检查正常而上传返回 500。
|
||||
- 从父目录运行 `mvn -pl file -am test`,并按接口影响验证商户/平台上传、替换、预览、无权限、签名伪造和磁盘故障场景。
|
||||
- 非本地环境必须显式配置 `FILE_STORAGE_ROOT` 并挂载持久化可写目录;服务启动时会校验目录,存储不可用时直接启动失败,避免健康检查正常而首次上传才返回 HTTP 200 + `S0503`。
|
||||
- 文件写入失败属于基础设施故障,统一返回 `S0503`,服务日志保留实际目录和底层异常;禁止将磁盘故障包装成 `B0001`。
|
||||
- 迁移历史 `/phase1-seed/...` 路径时必须先写入并校验真实脱敏图片,再按 `id + tenant_id + 旧 URL` 更新 IAM 元数据;禁止只改 URL。对象存储环境必须改走 File 上传接口。
|
||||
- 上传需先写新文件,再调用 IAM 登记;收到明确业务拒绝时删除新文件,传输异常、空响应或缺少 data 时远端结果未知,必须保留文件并记录 tenant/documentUrl 供重试或对账,避免 IAM 已提交却被 File 删除。`replace=true` 成功时 IAM 返回 `replacedDocumentUrl`,File 只能在新元数据落库后按目标租户安全清理旧 UUID 文件。
|
||||
- 旧文件清理只接受 File 自身生成的 `/api/merchant/v1/files/qualifications/{uuid}.{ext}`,并且不得删除本次新上传的 `documentUrl`;不能仅从任意 URL 截取文件名后删除。
|
||||
- 平台上传要求平台身份以及 `platform:customer:create` 或 `platform:customer:edit`,平台下载属于客户档案只读能力并要求 `platform:customer:view`;不得复用审核权限,也不能只依赖 Gateway 路由或前端按钮隐藏。
|
||||
- `LocalQualificationStorage` 当前生成的 `documentUrl` 使用商户读取路径,但文件实际按 `storage-root/qualifications/{tenantId}/{uuid.ext}` 隔离;变更 URL 形态时同步检查 IAM 已存元数据、平台下载路由和迁移脚本。
|
||||
|
||||
@@ -15,14 +15,16 @@ public interface IamQualificationClient {
|
||||
@RequestBody QualificationRequest request);
|
||||
|
||||
@PostMapping("/platform-qualifications")
|
||||
ApiResponse<QualificationResponse> submitPlatform(
|
||||
ApiResponse<PlatformQualificationResponse> submitPlatform(
|
||||
@RequestHeader("X-Internal-Service-Token") String internalToken,
|
||||
@RequestBody PlatformQualificationRequest request);
|
||||
|
||||
record QualificationRequest(Long tenantId, String qualificationType,
|
||||
String certificateNo, String documentUrl) { }
|
||||
record QualificationResponse(Long qualificationId, Long reviewId, String tenantStatus) { }
|
||||
record PlatformQualificationResponse(Long qualificationId, Long reviewId, String tenantStatus,
|
||||
String replacedDocumentUrl) { }
|
||||
record PlatformQualificationRequest(Long tenantId, String qualificationType,
|
||||
String certificateNo, String documentUrl,
|
||||
java.time.LocalDate validTo) { }
|
||||
java.time.LocalDate validTo, boolean replace) { }
|
||||
}
|
||||
|
||||
+26
-9
@@ -7,6 +7,8 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.file.client.IamQualificationClient;
|
||||
import com.baiyue.file.service.LocalQualificationStorage;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -27,6 +29,7 @@ import java.time.LocalDate;
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/files/qualifications")
|
||||
public class PlatformQualificationFileController {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PlatformQualificationFileController.class);
|
||||
@Resource private LocalQualificationStorage storage;
|
||||
@Resource private IamQualificationClient iamClient;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
@@ -39,30 +42,44 @@ public class PlatformQualificationFileController {
|
||||
@RequestPart("file") MultipartFile file,
|
||||
@RequestParam String qualificationType,
|
||||
@RequestParam(required = false) String certificateNo,
|
||||
@RequestParam(required = false) LocalDate validTo) {
|
||||
@RequestParam(required = false) LocalDate validTo,
|
||||
@RequestParam(defaultValue = "false") boolean replace) {
|
||||
requirePlatform(user);
|
||||
var stored = storage.store(tenantId, file);
|
||||
try {
|
||||
var response = iamClient.submitPlatform(internalToken,
|
||||
new IamQualificationClient.PlatformQualificationRequest(
|
||||
tenantId, qualificationType, certificateNo, stored.documentUrl(), validTo));
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
String message = response == null || response.message() == null || response.message().isBlank()
|
||||
? "IAM qualification service is unavailable" : response.message();
|
||||
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code()), message);
|
||||
tenantId, qualificationType, certificateNo, stored.documentUrl(), validTo, replace));
|
||||
if (response == null) {
|
||||
throw new IllegalStateException("IAM qualification response is empty");
|
||||
}
|
||||
if (!response.successful()) {
|
||||
ResultCode code = ResultCode.of(response.code());
|
||||
String message = response.message() == null || response.message().isBlank()
|
||||
? code.message() : response.message();
|
||||
storage.delete(stored);
|
||||
throw new BizException(code, message);
|
||||
}
|
||||
if (response.data() == null) {
|
||||
throw new IllegalStateException("IAM qualification response data is empty");
|
||||
}
|
||||
var data = response.data();
|
||||
if (!stored.documentUrl().equals(data.replacedDocumentUrl())) {
|
||||
storage.deleteByDocumentUrl(tenantId, data.replacedDocumentUrl());
|
||||
}
|
||||
return ApiResponse.success(new QualificationFileController.UploadResponse(
|
||||
data.qualificationId(), data.reviewId(), data.tenantStatus(), stored.documentUrl()));
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
storage.delete(stored);
|
||||
if (exception instanceof BizException business) throw business;
|
||||
LOGGER.error("IAM 平台资质登记结果未知,保留已写入文件等待重试或对账,tenantId={},documentUrl={}",
|
||||
tenantId, stored.documentUrl(), exception);
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM qualification service is unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/{fileName}")
|
||||
@PreAuthorize("hasAuthority('platform:customer:review')")
|
||||
@PreAuthorize("hasAuthority('platform:customer:view')")
|
||||
public ResponseEntity<org.springframework.core.io.Resource> download(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long tenantId, @PathVariable String fileName) {
|
||||
|
||||
@@ -39,24 +39,26 @@ public class QualificationFileController {
|
||||
new IamQualificationClient.QualificationRequest(tenantId, "BUSINESS_LICENSE",
|
||||
certificateNo, stored.documentUrl()));
|
||||
if (response == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Qualification workflow failed");
|
||||
throw new IllegalStateException("IAM qualification response is empty");
|
||||
}
|
||||
if (!response.successful()) {
|
||||
ResultCode code = ResultCode.of(response.code());
|
||||
String message = response.message() == null || response.message().isBlank()
|
||||
? code.message() : response.message();
|
||||
storage.delete(stored);
|
||||
throw new BizException(code, message);
|
||||
}
|
||||
if (response.data() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Qualification workflow failed");
|
||||
throw new IllegalStateException("IAM qualification response data is empty");
|
||||
}
|
||||
var data = response.data();
|
||||
return ApiResponse.success(new UploadResponse(data.qualificationId(), data.reviewId(),
|
||||
data.tenantStatus(), stored.documentUrl()));
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
storage.delete(stored);
|
||||
if (exception instanceof BizException business) throw business;
|
||||
LOGGER.error("IAM 资质登记调用失败", exception);
|
||||
LOGGER.error("IAM 资质登记结果未知,保留已写入文件等待重试或对账,tenantId={},documentUrl={}",
|
||||
tenantId, stored.documentUrl(), exception);
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM qualification service is unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,20 @@ public class LocalQualificationStorage {
|
||||
try { Files.deleteIfExists(file.path()); } catch (IOException ignored) { }
|
||||
}
|
||||
|
||||
public void deleteByDocumentUrl(long tenantId, String documentUrl) {
|
||||
if (documentUrl == null || documentUrl.isBlank()) return;
|
||||
String prefix = "/api/merchant/v1/files/qualifications/";
|
||||
if (!documentUrl.startsWith(prefix)) return;
|
||||
String fileName = documentUrl.substring(prefix.length());
|
||||
if (!fileName.matches("[0-9a-fA-F-]{36}\\.(jpg|png|pdf)")) return;
|
||||
Path directory = tenantDirectory(tenantId);
|
||||
Path target = directory.resolve(fileName).normalize();
|
||||
if (!target.startsWith(directory)) return;
|
||||
try { Files.deleteIfExists(target); } catch (IOException exception) {
|
||||
LOGGER.warn("旧资质文件清理失败,tenantId={},fileName={}", tenantId, fileName, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Path tenantDirectory(long tenantId) {
|
||||
Path directory = qualificationRoot.resolve(Long.toString(tenantId)).normalize();
|
||||
if (!directory.startsWith(qualificationRoot)) {
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
package com.baiyue.file.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.file.client.IamQualificationClient;
|
||||
import com.baiyue.file.service.LocalQualificationStorage;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
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.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("企业资质文件接口契约功能")
|
||||
class QualificationFileContractTest {
|
||||
@@ -39,4 +60,139 @@ class QualificationFileContractTest {
|
||||
.anyMatch(MediaType.MULTIPART_FORM_DATA_VALUE::equals);
|
||||
assertTrue(hasMultipartPost);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformQualificationPreviewUsesTheCustomerViewPermission() throws Exception {
|
||||
var method = PlatformQualificationFileController.class.getDeclaredMethod(
|
||||
"download", AuthenticatedUser.class, long.class, String.class);
|
||||
|
||||
assertEquals("hasAuthority('platform:customer:view')", method.getAnnotation(PreAuthorize.class).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformReplacementIsForwardedAndCleansThePreviousFileAfterIamSucceeds() {
|
||||
var storage = mock(LocalQualificationStorage.class);
|
||||
var iamClient = mock(IamQualificationClient.class);
|
||||
var controller = platformController(storage, iamClient);
|
||||
var file = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||
var stored = new LocalQualificationStorage.StoredFile(Path.of("new.pdf"), "new.pdf",
|
||||
"/api/merchant/v1/files/qualifications/11111111-1111-1111-1111-111111111111.pdf");
|
||||
var oldUrl = "/api/merchant/v1/files/qualifications/22222222-2222-2222-2222-222222222222.pdf";
|
||||
when(storage.store(10001L, file)).thenReturn(stored);
|
||||
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", oldUrl)));
|
||||
|
||||
var response = controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null,
|
||||
LocalDate.of(2030, 12, 31), true);
|
||||
|
||||
var ordered = inOrder(storage, iamClient);
|
||||
ordered.verify(storage).store(10001L, file);
|
||||
var requestCaptor = org.mockito.ArgumentCaptor.forClass(
|
||||
IamQualificationClient.PlatformQualificationRequest.class);
|
||||
ordered.verify(iamClient).submitPlatform(eq("internal-token"), requestCaptor.capture());
|
||||
ordered.verify(storage).deleteByDocumentUrl(10001L, oldUrl);
|
||||
assertTrue(requestCaptor.getValue().replace());
|
||||
assertEquals(stored.documentUrl(), requestCaptor.getValue().documentUrl());
|
||||
assertEquals(stored.documentUrl(), response.data().documentUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformReplacementNeverDeletesTheCurrentUpload() {
|
||||
var storage = mock(LocalQualificationStorage.class);
|
||||
var iamClient = mock(IamQualificationClient.class);
|
||||
var controller = platformController(storage, iamClient);
|
||||
var file = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||
var currentUrl = "/api/merchant/v1/files/qualifications/11111111-1111-1111-1111-111111111111.pdf";
|
||||
var stored = new LocalQualificationStorage.StoredFile(Path.of("new.pdf"), "new.pdf", currentUrl);
|
||||
when(storage.store(10001L, file)).thenReturn(stored);
|
||||
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", currentUrl)));
|
||||
|
||||
controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, true);
|
||||
|
||||
verify(storage, never()).deleteByDocumentUrl(10001L, currentUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformTransportFailurePreservesTheFileBecauseTheIamOutcomeIsUnknown() {
|
||||
var storage = mock(LocalQualificationStorage.class);
|
||||
var iamClient = mock(IamQualificationClient.class);
|
||||
var controller = platformController(storage, iamClient);
|
||||
var file = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||
var stored = storedFile();
|
||||
when(storage.store(10001L, file)).thenReturn(stored);
|
||||
when(iamClient.submitPlatform(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
|
||||
|
||||
var exception = assertThrows(BizException.class,
|
||||
() -> controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, true));
|
||||
|
||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
||||
verify(storage, never()).delete(stored);
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitPlatformBusinessRejectionDeletesTheUnreferencedFile() {
|
||||
var storage = mock(LocalQualificationStorage.class);
|
||||
var iamClient = mock(IamQualificationClient.class);
|
||||
var controller = platformController(storage, iamClient);
|
||||
var file = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||
var stored = storedFile();
|
||||
when(storage.store(10001L, file)).thenReturn(stored);
|
||||
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.failure(ResultCode.CONFLICT, "duplicate", null, null));
|
||||
|
||||
var exception = assertThrows(BizException.class,
|
||||
() -> controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, false));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
verify(storage).delete(stored);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantTransportFailureAlsoPreservesTheFileForReconciliation() {
|
||||
var storage = mock(LocalQualificationStorage.class);
|
||||
var iamClient = mock(IamQualificationClient.class);
|
||||
var controller = merchantController(storage, iamClient);
|
||||
var file = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||
var stored = storedFile();
|
||||
when(storage.store(10001L, file)).thenReturn(stored);
|
||||
when(iamClient.submit(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
|
||||
|
||||
var exception = assertThrows(BizException.class,
|
||||
() -> controller.upload(merchantUser(), file, null));
|
||||
|
||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
||||
verify(storage, never()).delete(stored);
|
||||
}
|
||||
|
||||
private static PlatformQualificationFileController platformController(
|
||||
LocalQualificationStorage storage, IamQualificationClient iamClient) {
|
||||
var controller = new PlatformQualificationFileController();
|
||||
ReflectionTestUtils.setField(controller, "storage", storage);
|
||||
ReflectionTestUtils.setField(controller, "iamClient", iamClient);
|
||||
ReflectionTestUtils.setField(controller, "internalToken", "internal-token");
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static QualificationFileController merchantController(
|
||||
LocalQualificationStorage storage, IamQualificationClient iamClient) {
|
||||
var controller = new QualificationFileController();
|
||||
ReflectionTestUtils.setField(controller, "storage", storage);
|
||||
ReflectionTestUtils.setField(controller, "iamClient", iamClient);
|
||||
ReflectionTestUtils.setField(controller, "internalToken", "internal-token");
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static LocalQualificationStorage.StoredFile storedFile() {
|
||||
return new LocalQualificationStorage.StoredFile(Path.of("new.pdf"), "new.pdf",
|
||||
"/api/merchant/v1/files/qualifications/11111111-1111-1111-1111-111111111111.pdf");
|
||||
}
|
||||
|
||||
private static AuthenticatedUser platformUser() {
|
||||
return new AuthenticatedUser(1L, 0L, 0L, 1L, "admin", "PLATFORM", "WEB", Set.of());
|
||||
}
|
||||
|
||||
private static AuthenticatedUser merchantUser() {
|
||||
return new AuthenticatedUser(2L, 10001L, 10001L, 2L, "merchant", "MERCHANT", "WEB", Set.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@@ -63,6 +64,30 @@ class LocalQualificationStorageTest {
|
||||
storage.load(10001L, stored.fileName()).getFile().toPath().toAbsolutePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletesTheReplacedQualificationByItsValidatedDocumentUrl() {
|
||||
var storage = new LocalQualificationStorage(storageRoot.toString());
|
||||
storage.initialize();
|
||||
var upload = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4\ncontent".getBytes());
|
||||
var stored = storage.store(10001L, upload);
|
||||
|
||||
storage.deleteByDocumentUrl(10001L, stored.documentUrl());
|
||||
|
||||
assertFalse(Files.exists(stored.path()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotDeleteAQualificationForAnUntrustedDocumentUrl() {
|
||||
var storage = new LocalQualificationStorage(storageRoot.toString());
|
||||
storage.initialize();
|
||||
var upload = new MockMultipartFile("file", "license.pdf", "application/pdf", "%PDF-1.4\ncontent".getBytes());
|
||||
var stored = storage.store(10001L, upload);
|
||||
|
||||
storage.deleteByDocumentUrl(10001L, "/untrusted/" + stored.fileName());
|
||||
|
||||
assertTrue(Files.exists(stored.path()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacySeedPathCannotBePretendedToBeAStoredFile() {
|
||||
var storage = new LocalQualificationStorage(storageRoot.toString());
|
||||
|
||||
Reference in New Issue
Block a user