|
|
@@ -84,6 +84,75 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void createOutlookReportAcceptsCallerVocabularyPayloadAndDispatches() throws Exception {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
+ List<String> dispatchedReportIds = new ArrayList<>();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(repository, dispatchedReportIds::add, storage);
|
|
|
+
|
|
|
+ var response = service.createOutlookReport(callerVocabularyPayload());
|
|
|
+
|
|
|
+ assertThat(response.reportId()).isNotBlank();
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
+ assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
|
|
|
+ assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
|
|
|
+ assertThat(dispatchedReportIds).containsExactly(response.reportId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createOutlookReportKeepsStructuredPayloadPathWhenOnlyStageVocabularyFieldIsAdded() {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
+ List<String> dispatchedReportIds = new ArrayList<>();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(repository, dispatchedReportIds::add, storage);
|
|
|
+ ObjectNode payload = validOutlookPayload().deepCopy();
|
|
|
+ payload.put("StageVocabulary", 4200);
|
|
|
+
|
|
|
+ var response = service.createOutlookReport(payload);
|
|
|
+
|
|
|
+ assertThat(response.reportId()).isNotBlank();
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
+ assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
|
|
|
+ assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
|
|
|
+ assertThat(dispatchedReportIds).containsExactly(response.reportId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createOutlookReportAcceptsCallerVocabularyPayloadWithNoMasteredPaperWords() throws Exception {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
+ List<String> dispatchedReportIds = new ArrayList<>();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(repository, dispatchedReportIds::add, storage);
|
|
|
+ ObjectNode payload = callerVocabularyPayload().deepCopy();
|
|
|
+ payload.putArray("TestPaperMastedWords");
|
|
|
+ payload.put("TestPaperMastedWordCount", 0);
|
|
|
+
|
|
|
+ var response = service.createOutlookReport(payload);
|
|
|
+
|
|
|
+ assertThat(response.reportId()).isNotBlank();
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
+ assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
|
|
|
+ assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.PENDING);
|
|
|
+ assertThat(dispatchedReportIds).containsExactly(response.reportId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createOutlookReportRejectsCallerVocabularyPayloadWithoutStudentWordsLatest() throws Exception {
|
|
|
+ ObjectNode invalidPayload = callerVocabularyPayload();
|
|
|
+ invalidPayload.remove("StudentWordsLatest");
|
|
|
+
|
|
|
+ assertCreateOutlookReportRejectsInvalidPayload(invalidPayload);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createOutlookReportRejectsCallerVocabularyPayloadWithNullStudentWordLatest() throws Exception {
|
|
|
+ ObjectNode invalidPayload = callerVocabularyPayload().deepCopy();
|
|
|
+ ((com.fasterxml.jackson.databind.node.ArrayNode) invalidPayload.get("StudentWordsLatest")).addNull();
|
|
|
+
|
|
|
+ assertCreateOutlookReportRejectsInvalidPayload(invalidPayload);
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void createAchievementReportStoresAchievementTypeAndReturnsReportId() {
|
|
|
TestRepository repository = new TestRepository();
|
|
|
@@ -127,6 +196,32 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
assertThat(storage.generatedKeys).containsExactly(saved.storageObjectKey());
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void createOutlookReportSyncAcceptsCallerVocabularyPayloadAndReturnsDownloadUrl() throws Exception {
|
|
|
+ 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(callerVocabularyPayload());
|
|
|
+
|
|
|
+ assertThat(response.reportId()).isNotBlank();
|
|
|
+ assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
+ assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
|
|
|
+ assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
|
|
|
+ assertThat(saved.storageObjectKey()).isEqualTo("exam-sprint-outlook-report-" + response.reportId() + ".pdf");
|
|
|
+ assertThat(storage.generatedKeys).containsExactly(saved.storageObjectKey());
|
|
|
+ assertThat(storage.uploadedContents).containsOnlyKeys(saved.storageObjectKey());
|
|
|
+ assertThat(new String(storage.uploadedContents.get(saved.storageObjectKey()).bytes(), StandardCharsets.UTF_8))
|
|
|
+ .contains("preview:20260318测试:")
|
|
|
+ .contains(FIXED_CLOCK.instant().toString());
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void createAchievementReportSyncGeneratesUploadAndReturnsDownloadUrl() {
|
|
|
TestRepository repository = new TestRepository();
|
|
|
@@ -583,6 +678,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
"每次精听后补 5 组同义替换并做口头复述。"),
|
|
|
new OutlookExamSprintReportPayload.HighFrequencyVocabularyChart(
|
|
|
77,
|
|
|
+ 70,
|
|
|
62,
|
|
|
"Common 高频词覆盖较广,但易混词记忆不牢。"),
|
|
|
new OutlookExamSprintReportPayload.VocabularyFrequencyBandChart(
|
|
|
@@ -611,6 +707,41 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
19)));
|
|
|
}
|
|
|
|
|
|
+ private ObjectNode callerVocabularyPayload() throws Exception {
|
|
|
+ return (ObjectNode) OBJECT_MAPPER.readTree("""
|
|
|
+ {
|
|
|
+ "StudentName": "20260318测试",
|
|
|
+ "StudentStage": 2,
|
|
|
+ "StageName": "初中",
|
|
|
+ "StageVocabulary": 10,
|
|
|
+ "StageExaminName": "中考",
|
|
|
+ "StageImportant": 3,
|
|
|
+ "StudentWordsLatest": [
|
|
|
+ {"WordId": 1, "MeanId": 1, "WordSpell": "w1", "WordFrequency": 1, "Mastery": 1.0, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 2, "MeanId": 1, "WordSpell": "w2", "WordFrequency": 2, "Mastery": 0.5, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 3, "MeanId": 1, "WordSpell": "w3", "WordFrequency": 3, "Mastery": 0.2, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 4, "MeanId": 1, "WordSpell": "w4", "WordFrequency": 4, "Mastery": 0.4, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 5, "MeanId": 1, "WordSpell": "w5", "WordFrequency": 5, "Mastery": 0.6, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 6, "MeanId": 1, "WordSpell": "w6", "WordFrequency": 6, "Mastery": 0.8, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 7, "MeanId": 1, "WordSpell": "w7", "WordFrequency": 7, "Mastery": 0.1, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 8, "MeanId": 1, "WordSpell": "w8", "WordFrequency": 8, "Mastery": 0.2, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 9, "MeanId": 1, "WordSpell": "w9", "WordFrequency": 9, "Mastery": 0.3, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"},
|
|
|
+ {"WordId": 10, "MeanId": 1, "WordSpell": "w10", "WordFrequency": 10, "Mastery": 0.4, "ReviewTimes": 1, "Reliability": 2, "CreateTime": "2026-03-18T15:28:25.5813874+08:00"}
|
|
|
+ ],
|
|
|
+ "MastedWordCount": 4,
|
|
|
+ "UnMastedWordCount": 6,
|
|
|
+ "ExamineStrangeWordCount": 3,
|
|
|
+ "TestPaperWordIdArray": [1, 2, 3, 4, 5],
|
|
|
+ "TestPaperTitle": "文章2.jpg",
|
|
|
+ "TestPaperUnMasterWords": ["lot", "father", "catch"],
|
|
|
+ "TestPaperMastedWords": ["a", "the"],
|
|
|
+ "TestPaperMastedWordCount": 2,
|
|
|
+ "TestPaperWordCount": 5,
|
|
|
+ "Complex": false
|
|
|
+ }
|
|
|
+ """);
|
|
|
+ }
|
|
|
+
|
|
|
private ObjectNode validAchievementPayload() {
|
|
|
return (ObjectNode) OBJECT_MAPPER.valueToTree(new AchievementExamSprintReportPayload(
|
|
|
"高考英语临考突击学习成果报告",
|
|
|
@@ -741,6 +872,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
private final List<String> generatedKeys = new ArrayList<>();
|
|
|
private final List<String> deletedKeys = new ArrayList<>();
|
|
|
private final List<String> deleteFailures = new ArrayList<>();
|
|
|
+ private final ConcurrentMap<String, StoredExamSprintReportContent> uploadedContents = new ConcurrentHashMap<>();
|
|
|
private RuntimeException generateDownloadUrlFailure;
|
|
|
private RuntimeException downloadFailure;
|
|
|
|
|
|
@@ -751,6 +883,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
String fileName,
|
|
|
byte[] pdfBytes,
|
|
|
Instant expiresAt) {
|
|
|
+ uploadedContents.put(fileName, new StoredExamSprintReportContent(fileName, pdfBytes, "application/pdf"));
|
|
|
return new StoredExamSprintReportFile(fileName, fileName);
|
|
|
}
|
|
|
|
|
|
@@ -768,7 +901,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
if (downloadFailure != null) {
|
|
|
throw downloadFailure;
|
|
|
}
|
|
|
- return Optional.empty();
|
|
|
+ return Optional.ofNullable(uploadedContents.get(storageObjectKey));
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -814,9 +947,13 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
|
|
|
@Override
|
|
|
public String render(ReportContent content, Instant generatedAt) {
|
|
|
- String title = content instanceof AchievementReportContent achievementContent
|
|
|
- ? achievementContent.reportTitle()
|
|
|
- : "";
|
|
|
+ String title = "";
|
|
|
+ if (content instanceof AchievementReportContent achievementContent) {
|
|
|
+ title = achievementContent.reportTitle();
|
|
|
+ } else if (content instanceof UnmodeledReportContent unmodeledContent
|
|
|
+ && unmodeledContent.source() instanceof JsonNode payload) {
|
|
|
+ title = payload.path("reportMetadata").path("learnerName").asText(payload.path("StudentName").asText(""));
|
|
|
+ }
|
|
|
return "<html><body>preview:" + title + ":" + generatedAt + "</body></html>";
|
|
|
}
|
|
|
}
|