|
|
@@ -5,6 +5,7 @@ import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportG
|
|
|
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
|
|
|
import cn.yunzhixue.ability.center.examsprint.contracts.report.OutlookExamSprintReportPayload;
|
|
|
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
|
|
|
+import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportPdfGenerator;
|
|
|
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
|
|
|
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
|
|
|
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
|
|
|
@@ -21,6 +22,7 @@ import org.junit.jupiter.params.provider.Arguments;
|
|
|
import org.junit.jupiter.params.provider.MethodSource;
|
|
|
|
|
|
import java.net.URI;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
import java.time.Clock;
|
|
|
import java.time.Duration;
|
|
|
import java.time.Instant;
|
|
|
@@ -71,6 +73,80 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
assertThat(saved.payload().path("reportTitle").asText()).isEqualTo("高考英语临考突击学习成果报告");
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void createOutlookReportSyncGeneratesUploadAndReturnsDownloadUrl() {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(
|
|
|
+ repository,
|
|
|
+ reportId -> {
|
|
|
+ throw new IllegalStateException("sync create must not dispatch async generation");
|
|
|
+ },
|
|
|
+ storage);
|
|
|
+
|
|
|
+ var response = service.createOutlookReportSync(validOutlookPayload());
|
|
|
+
|
|
|
+ assertThat(response.reportId()).isNotBlank();
|
|
|
+ assertThat(response.reportType()).isEqualTo(ExamSprintReportType.OUTLOOK);
|
|
|
+ assertThat(response.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
|
|
|
+ assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
|
|
|
+ assertThat(response.previewHtmlUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/preview/html");
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
+ assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
|
|
|
+ assertThat(saved.storageObjectKey()).isEqualTo("exam-sprint-outlook-report-" + response.reportId() + ".pdf");
|
|
|
+ assertThat(storage.generatedKeys).containsExactly(saved.storageObjectKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createAchievementReportSyncGeneratesUploadAndReturnsDownloadUrl() {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(
|
|
|
+ repository,
|
|
|
+ reportId -> {
|
|
|
+ throw new IllegalStateException("sync create must not dispatch async generation");
|
|
|
+ },
|
|
|
+ storage);
|
|
|
+
|
|
|
+ var response = service.createAchievementReportSync(validAchievementPayload());
|
|
|
+
|
|
|
+ assertThat(response.reportId()).isNotBlank();
|
|
|
+ assertThat(response.reportType()).isEqualTo(ExamSprintReportType.ACHIEVEMENT);
|
|
|
+ assertThat(response.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
|
|
|
+ assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
|
|
|
+ assertThat(response.previewHtmlUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/preview/html");
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
+ assertThat(saved.generationStatus()).isEqualTo(ExamSprintReportGenerationStatus.SUCCESS);
|
|
|
+ assertThat(saved.storageObjectKey()).isEqualTo("exam-sprint-achievement-report-" + response.reportId() + ".pdf");
|
|
|
+ assertThat(storage.generatedKeys).containsExactly(saved.storageObjectKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createOutlookReportSyncRejectsInvalidPayloadBeforeSaving() {
|
|
|
+ assertCreateOutlookReportSyncRejectsInvalidPayload(OBJECT_MAPPER.nullNode());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createOutlookReportSyncThrowsDownloadUnavailableAndKeepsFailedReportWhenGenerationFails() {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(
|
|
|
+ repository,
|
|
|
+ reportId -> { },
|
|
|
+ new TestStorage(),
|
|
|
+ List.of(new FailingRenderer()),
|
|
|
+ html -> html.getBytes(StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ assertThatThrownBy(() -> service.createOutlookReportSync(validOutlookPayload()))
|
|
|
+ .isInstanceOf(BusinessException.class)
|
|
|
+ .extracting(exception -> ((BusinessException) exception).getErrorCode())
|
|
|
+ .isEqualTo(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
|
|
|
+
|
|
|
+ assertThat(repository.storage.values())
|
|
|
+ .singleElement()
|
|
|
+ .extracting(ExamSprintReport::generationStatus)
|
|
|
+ .isEqualTo(ExamSprintReportGenerationStatus.FAILED);
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void createAchievementReportRejectsInvalidAchievementPayloadBeforeSaving() {
|
|
|
ObjectNode invalidPayload = validAchievementPayload().deepCopy();
|
|
|
@@ -287,9 +363,29 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
TestRepository repository,
|
|
|
ExamSprintReportGenerationDispatcher dispatcher,
|
|
|
TestStorage storage) {
|
|
|
+ return service(
|
|
|
+ repository,
|
|
|
+ dispatcher,
|
|
|
+ storage,
|
|
|
+ List.of(new PreviewTestRenderer()),
|
|
|
+ html -> html.getBytes(StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+
|
|
|
+ private DefaultExamSprintReportApplicationService service(
|
|
|
+ TestRepository repository,
|
|
|
+ ExamSprintReportGenerationDispatcher dispatcher,
|
|
|
+ TestStorage storage,
|
|
|
+ List<ExamSprintReportRenderer> renderers,
|
|
|
+ ExamSprintReportPdfGenerator pdfGenerator) {
|
|
|
return new DefaultExamSprintReportApplicationService(
|
|
|
repository,
|
|
|
dispatcher,
|
|
|
+ new ExamSprintReportGenerationPipeline(
|
|
|
+ repository,
|
|
|
+ renderers,
|
|
|
+ pdfGenerator,
|
|
|
+ storage,
|
|
|
+ FIXED_CLOCK),
|
|
|
storage,
|
|
|
properties(),
|
|
|
FIXED_CLOCK,
|
|
|
@@ -437,6 +533,23 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
assertThat(dispatched[0]).isFalse();
|
|
|
}
|
|
|
|
|
|
+ private void assertCreateOutlookReportSyncRejectsInvalidPayload(JsonNode payload) {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ boolean[] dispatched = {false};
|
|
|
+ DefaultExamSprintReportApplicationService service = service(
|
|
|
+ repository,
|
|
|
+ reportId -> dispatched[0] = true,
|
|
|
+ new TestStorage());
|
|
|
+
|
|
|
+ assertThatThrownBy(() -> service.createOutlookReportSync(payload))
|
|
|
+ .isInstanceOf(BusinessException.class)
|
|
|
+ .extracting(exception -> ((BusinessException) exception).getErrorCode())
|
|
|
+ .isEqualTo(ErrorCode.VALIDATION_ERROR);
|
|
|
+
|
|
|
+ assertThat(repository.storage).isEmpty();
|
|
|
+ assertThat(dispatched[0]).isFalse();
|
|
|
+ }
|
|
|
+
|
|
|
private static class TestRepository implements ExamSprintReportRepository {
|
|
|
private final ConcurrentMap<String, ExamSprintReport> storage = new ConcurrentHashMap<>();
|
|
|
|
|
|
@@ -525,4 +638,17 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
return "<html><body>preview:" + payload.path("reportTitle").asText() + ":" + generatedAt + "</body></html>";
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private static class FailingRenderer implements ExamSprintReportRenderer {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean supports(ExamSprintReportType reportType) {
|
|
|
+ return reportType == ExamSprintReportType.OUTLOOK;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String render(JsonNode payload, Instant generatedAt) {
|
|
|
+ throw new IllegalStateException("renderer exploded");
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|