|
|
@@ -1,10 +1,16 @@
|
|
|
package cn.yunzhixue.ability.center.examsprint.infrastructure.report.storage;
|
|
|
|
|
|
+import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
|
|
|
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
|
|
|
+import com.azure.core.util.Context;
|
|
|
import com.azure.storage.blob.BlobClient;
|
|
|
import com.azure.storage.blob.BlobContainerClient;
|
|
|
+import com.azure.storage.blob.options.BlobParallelUploadOptions;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
|
|
|
+import org.springframework.boot.test.system.CapturedOutput;
|
|
|
+import org.springframework.boot.test.system.OutputCaptureExtension;
|
|
|
|
|
|
import java.lang.reflect.Constructor;
|
|
|
import java.net.URI;
|
|
|
@@ -15,12 +21,15 @@ import java.time.ZoneOffset;
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
import static org.mockito.ArgumentMatchers.argThat;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
import static org.mockito.Mockito.mock;
|
|
|
import static org.mockito.Mockito.verify;
|
|
|
import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
import static org.mockito.Mockito.when;
|
|
|
|
|
|
+@ExtendWith(OutputCaptureExtension.class)
|
|
|
class AzureBlobExamSprintReportStorageTest {
|
|
|
|
|
|
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-01-03T08:00:00Z"), ZoneOffset.UTC);
|
|
|
@@ -81,6 +90,27 @@ class AzureBlobExamSprintReportStorageTest {
|
|
|
"https://dcjxbtest.blob.core.chinacloudapi.cn/report/file.pdf"));
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void generateDownloadUrlEncodesStorageObjectKeyPathSegmentsAndPreservesPathSeparators() {
|
|
|
+ BlobContainerClient containerClient = mock(BlobContainerClient.class);
|
|
|
+ AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
|
|
|
+ containerClient,
|
|
|
+ "report",
|
|
|
+ "https://dcjxbtest.blob.core.chinacloudapi.cn",
|
|
|
+ FIXED_CLOCK);
|
|
|
+
|
|
|
+ URI downloadUrl = storage.generateDownloadUrl(
|
|
|
+ "reports/outlook/report-123/John Doe-报告 #1?.pdf",
|
|
|
+ Duration.ofMinutes(15));
|
|
|
+
|
|
|
+ assertThat(downloadUrl.toString()).isEqualTo(
|
|
|
+ "https://dcjxbtest.blob.core.chinacloudapi.cn/report/reports/outlook/report-123/John%20Doe-报告%20%231%3F.pdf");
|
|
|
+ assertThat(downloadUrl.getRawPath()).isEqualTo(
|
|
|
+ "/report/reports/outlook/report-123/John%20Doe-报告%20%231%3F.pdf");
|
|
|
+ assertThat(downloadUrl.getRawQuery()).isNull();
|
|
|
+ assertThat(downloadUrl.getRawFragment()).isNull();
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void generateDownloadUrlRejectsBlankStorageObjectKey() {
|
|
|
BlobContainerClient containerClient = mock(BlobContainerClient.class);
|
|
|
@@ -136,10 +166,42 @@ class AzureBlobExamSprintReportStorageTest {
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void uploadWritesUploadedAtMetadataFromInjectedClock() {
|
|
|
+ void uploadUsesReportScopedBlobNameAndSendsHeadersAndMetadataInUploadRequest() {
|
|
|
BlobContainerClient containerClient = mock(BlobContainerClient.class);
|
|
|
BlobClient blobClient = mock(BlobClient.class);
|
|
|
- when(containerClient.getBlobClient("file.pdf")).thenReturn(blobClient);
|
|
|
+ when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
|
|
|
+ AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
|
|
|
+ containerClient,
|
|
|
+ "report",
|
|
|
+ "https://dcjxbtest.blob.core.chinacloudapi.cn",
|
|
|
+ FIXED_CLOCK);
|
|
|
+
|
|
|
+ ExamSprintReportStorage.StoredExamSprintReportFile storedFile = storage.upload(
|
|
|
+ "report-123",
|
|
|
+ ReportType.OUTLOOK,
|
|
|
+ "file.pdf",
|
|
|
+ new byte[]{1, 2, 3},
|
|
|
+ Instant.parse("2026-01-10T00:00:00Z"));
|
|
|
+
|
|
|
+ assertThat(storedFile.storageObjectKey()).isEqualTo("reports/outlook/report-123/file.pdf");
|
|
|
+ assertThat(storedFile.fileName()).isEqualTo("file.pdf");
|
|
|
+ verify(containerClient).getBlobClient("reports/outlook/report-123/file.pdf");
|
|
|
+ verify(blobClient).uploadWithResponse(argThat(options ->
|
|
|
+ options.getLength() == 3
|
|
|
+ && options.getHeaders() != null
|
|
|
+ && "application/pdf".equals(options.getHeaders().getContentType())
|
|
|
+ && "report-123".equals(options.getMetadata().get("reportId"))
|
|
|
+ && "OUTLOOK".equals(options.getMetadata().get("reportType"))
|
|
|
+ && "2026-01-10T00:00:00Z".equals(options.getMetadata().get("expiresAt"))
|
|
|
+ && FIXED_CLOCK.instant().toString().equals(options.getMetadata().get("uploadedAt"))),
|
|
|
+ eq(Context.NONE));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void uploadLogsClientResolutionUploadRequestCompletionAndTotalDuration(CapturedOutput output) {
|
|
|
+ BlobContainerClient containerClient = mock(BlobContainerClient.class);
|
|
|
+ BlobClient blobClient = mock(BlobClient.class);
|
|
|
+ when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
|
|
|
AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
|
|
|
containerClient,
|
|
|
"report",
|
|
|
@@ -153,10 +215,51 @@ class AzureBlobExamSprintReportStorageTest {
|
|
|
new byte[]{1, 2, 3},
|
|
|
Instant.parse("2026-01-10T00:00:00Z"));
|
|
|
|
|
|
- verify(blobClient).setMetadata(argThat(metadata ->
|
|
|
- "report-123".equals(metadata.get("reportId"))
|
|
|
- && "OUTLOOK".equals(metadata.get("reportType"))
|
|
|
- && "2026-01-10T00:00:00Z".equals(metadata.get("expiresAt"))
|
|
|
- && FIXED_CLOCK.instant().toString().equals(metadata.get("uploadedAt"))));
|
|
|
+ assertThat(output.getAll())
|
|
|
+ .contains("exam_sprint_report_azure_storage_upload_stage_completed")
|
|
|
+ .contains("stage=client_resolved")
|
|
|
+ .contains("stage=upload_request_completed")
|
|
|
+ .contains("exam_sprint_report_azure_storage_upload_completed")
|
|
|
+ .contains("reportId=report-123")
|
|
|
+ .contains("reportType=OUTLOOK")
|
|
|
+ .contains("storageObjectKey=reports/outlook/report-123/file.pdf")
|
|
|
+ .contains("blobName=reports/outlook/report-123/file.pdf")
|
|
|
+ .contains("fileName=file.pdf")
|
|
|
+ .contains("pdfByteLength=3")
|
|
|
+ .contains("durationMs=")
|
|
|
+ .doesNotContain("connection-string")
|
|
|
+ .doesNotContain("accountKey")
|
|
|
+ .doesNotContain("account-key");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void uploadDoesNotUseDisplayFileNameAsTheUniqueStorageObjectKey() {
|
|
|
+ BlobContainerClient containerClient = mock(BlobContainerClient.class);
|
|
|
+ BlobClient blobClient = mock(BlobClient.class);
|
|
|
+ when(containerClient.getBlobClient(anyString())).thenReturn(blobClient);
|
|
|
+ AzureBlobExamSprintReportStorage storage = new AzureBlobExamSprintReportStorage(
|
|
|
+ containerClient,
|
|
|
+ "report",
|
|
|
+ "https://dcjxbtest.blob.core.chinacloudapi.cn",
|
|
|
+ FIXED_CLOCK);
|
|
|
+
|
|
|
+ ExamSprintReportStorage.StoredExamSprintReportFile firstStoredFile = storage.upload(
|
|
|
+ "report-123",
|
|
|
+ ReportType.OUTLOOK,
|
|
|
+ "same-display-name.pdf",
|
|
|
+ new byte[]{1},
|
|
|
+ Instant.parse("2026-01-10T00:00:00Z"));
|
|
|
+ ExamSprintReportStorage.StoredExamSprintReportFile secondStoredFile = storage.upload(
|
|
|
+ "report-456",
|
|
|
+ ReportType.OUTLOOK,
|
|
|
+ "same-display-name.pdf",
|
|
|
+ new byte[]{2},
|
|
|
+ Instant.parse("2026-01-10T00:00:00Z"));
|
|
|
+
|
|
|
+ assertThat(firstStoredFile.storageObjectKey()).isEqualTo("reports/outlook/report-123/same-display-name.pdf");
|
|
|
+ assertThat(secondStoredFile.storageObjectKey()).isEqualTo("reports/outlook/report-456/same-display-name.pdf");
|
|
|
+ assertThat(firstStoredFile.storageObjectKey()).isNotEqualTo(secondStoredFile.storageObjectKey());
|
|
|
+ assertThat(firstStoredFile.fileName()).isEqualTo("same-display-name.pdf");
|
|
|
+ assertThat(secondStoredFile.fileName()).isEqualTo("same-display-name.pdf");
|
|
|
}
|
|
|
}
|