|
@@ -71,7 +71,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
.doesNotContain(removedHtmlPreviewMethodName());
|
|
.doesNotContain(removedHtmlPreviewMethodName());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 覆盖创建展望报告的上游词汇报文场景,当提交有效 payload 时,应保存 OUTLOOK 报告并保留 StudentName 和 StageVocabulary。 */
|
|
|
|
|
|
|
+ /** 覆盖创建展望报告的上游词汇报文场景,当提交有效 payload 时,应保存 OUTLOOK 报告并归一化 StudentName/studentName。 */
|
|
|
@Test
|
|
@Test
|
|
|
void createOutlookReportStoresOutlookTypeAndReturnsReportId() {
|
|
void createOutlookReportStoresOutlookTypeAndReturnsReportId() {
|
|
|
TestRepository repository = new TestRepository();
|
|
TestRepository repository = new TestRepository();
|
|
@@ -87,6 +87,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
UnmodeledReportContent content = (UnmodeledReportContent) saved.content();
|
|
UnmodeledReportContent content = (UnmodeledReportContent) saved.content();
|
|
|
JsonNode savedPayload = (JsonNode) content.source();
|
|
JsonNode savedPayload = (JsonNode) content.source();
|
|
|
assertThat(savedPayload.path("StudentName").asText()).isEqualTo("20260318测试");
|
|
assertThat(savedPayload.path("StudentName").asText()).isEqualTo("20260318测试");
|
|
|
|
|
+ assertThat(savedPayload.path("studentName").asText()).isEqualTo("20260318测试");
|
|
|
assertThat(savedPayload.path("StageVocabulary").asInt()).isEqualTo(10);
|
|
assertThat(savedPayload.path("StageVocabulary").asInt()).isEqualTo(10);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -107,6 +108,70 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
assertThat(dispatchedReportIds).containsExactly(response.reportId());
|
|
assertThat(dispatchedReportIds).containsExactly(response.reportId());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** 覆盖 mixed-field 展望报文场景,当 StudentName 为空白或 null 且 studentName 有效时,应允许通过并归一化到 lowercase 值。 */
|
|
|
|
|
+ @ParameterizedTest(name = "{0}")
|
|
|
|
|
+ @MethodSource("mixedOutlookPayloadsWithInvalidCanonicalStudentName")
|
|
|
|
|
+ void createOutlookReportNormalizesLowercaseStudentNameWhenCanonicalStudentNameIsInvalid(
|
|
|
|
|
+ String caseName,
|
|
|
|
|
+ Consumer<ObjectNode> mutatePayload) throws Exception {
|
|
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
|
|
+ DefaultExamSprintReportApplicationService service = service(repository, reportId -> { }, storage);
|
|
|
|
|
+ ObjectNode payload = callerVocabularyPayload().deepCopy();
|
|
|
|
|
+ payload.put("studentName", "冯亿豪");
|
|
|
|
|
+ mutatePayload.accept(payload);
|
|
|
|
|
+
|
|
|
|
|
+ var response = service.createOutlookReport(payload);
|
|
|
|
|
+
|
|
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
|
|
+ JsonNode savedPayload = (JsonNode) ((UnmodeledReportContent) saved.content()).source();
|
|
|
|
|
+ assertThat(savedPayload.path("StudentName").asText()).isEqualTo("冯亿豪");
|
|
|
|
|
+ assertThat(savedPayload.path("studentName").asText()).isEqualTo("冯亿豪");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 覆盖 canonical StudentName 类型边界场景,当 StudentName 为非 textual 且无有效 lowercase fallback 时,应在保存前校验失败。 */
|
|
|
|
|
+ @ParameterizedTest(name = "{0}")
|
|
|
|
|
+ @MethodSource("invalidCanonicalStudentNameJsonTypes")
|
|
|
|
|
+ void createOutlookReportRejectsNonTextualCanonicalStudentNameWithoutValidLowercaseFallback(
|
|
|
|
|
+ String caseName,
|
|
|
|
|
+ Consumer<ObjectNode> mutatePayload) throws Exception {
|
|
|
|
|
+ ObjectNode payload = callerVocabularyPayload().deepCopy();
|
|
|
|
|
+ mutatePayload.accept(payload);
|
|
|
|
|
+
|
|
|
|
|
+ assertCreateOutlookReportRejectsInvalidPayload(payload);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 覆盖 lowercase studentName 类型边界场景,当它是唯一可用来源但为非 textual 时,应在保存前校验失败。 */
|
|
|
|
|
+ @ParameterizedTest(name = "{0}")
|
|
|
|
|
+ @MethodSource("invalidLowercaseStudentNameJsonTypes")
|
|
|
|
|
+ void createOutlookReportRejectsNonTextualLowercaseStudentNameWhenItIsOnlyAvailableSource(
|
|
|
|
|
+ String caseName,
|
|
|
|
|
+ Consumer<ObjectNode> mutatePayload) throws Exception {
|
|
|
|
|
+ ObjectNode payload = callerVocabularyPayload().deepCopy();
|
|
|
|
|
+ payload.putNull("StudentName");
|
|
|
|
|
+ mutatePayload.accept(payload);
|
|
|
|
|
+
|
|
|
|
|
+ assertCreateOutlookReportRejectsInvalidPayload(payload);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 覆盖 canonical 优先场景,当 StudentName 有效而 lowercase studentName 为非 textual 时,应继续使用 canonical 值。 */
|
|
|
|
|
+ @ParameterizedTest(name = "{0}")
|
|
|
|
|
+ @MethodSource("invalidLowercaseStudentNameJsonTypes")
|
|
|
|
|
+ void createOutlookReportUsesCanonicalStudentNameWhenLowercaseStudentNameIsNonTextual(
|
|
|
|
|
+ String caseName,
|
|
|
|
|
+ Consumer<ObjectNode> mutatePayload) throws Exception {
|
|
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
|
|
+ DefaultExamSprintReportApplicationService service = service(repository, reportId -> { }, new TestStorage());
|
|
|
|
|
+ ObjectNode payload = callerVocabularyPayload().deepCopy();
|
|
|
|
|
+ mutatePayload.accept(payload);
|
|
|
|
|
+
|
|
|
|
|
+ var response = service.createOutlookReport(payload);
|
|
|
|
|
+
|
|
|
|
|
+ JsonNode savedPayload = (JsonNode) ((UnmodeledReportContent) repository.findById(response.reportId()).orElseThrow().content()).source();
|
|
|
|
|
+ assertThat(savedPayload.path("StudentName").asText()).isEqualTo("20260318测试");
|
|
|
|
|
+ assertThat(savedPayload.path("studentName").asText()).isEqualTo("20260318测试");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 覆盖非上游词汇展望报文被移除的场景,当只提交非官方结构时,应在保存前校验失败。 */
|
|
/** 覆盖非上游词汇展望报文被移除的场景,当只提交非官方结构时,应在保存前校验失败。 */
|
|
|
@Test
|
|
@Test
|
|
|
void createOutlookReportRejectsNonContractPayloadBeforeSaving() throws Exception {
|
|
void createOutlookReportRejectsNonContractPayloadBeforeSaving() throws Exception {
|
|
@@ -171,7 +236,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
.containsExactly("number", "bear", "popular", "importance");
|
|
.containsExactly("number", "bear", "popular", "importance");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 覆盖同步创建展望报告场景,当有效上游词汇报文生成成功时,应上传 PDF 并返回下载地址。 */
|
|
|
|
|
|
|
+ /** 覆盖同步创建展望报告场景,当 StudentName 与 studentName 冲突时,应以 StudentName 为准生成文件名并归一化保存。 */
|
|
|
@Test
|
|
@Test
|
|
|
void createOutlookReportSyncGeneratesUploadAndReturnsDownloadUrl() {
|
|
void createOutlookReportSyncGeneratesUploadAndReturnsDownloadUrl() {
|
|
|
TestRepository repository = new TestRepository();
|
|
TestRepository repository = new TestRepository();
|
|
@@ -195,13 +260,16 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
|
|
assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
|
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
|
|
assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
|
|
|
- String expectedFileName = "冯亿豪-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
|
|
|
|
+ String expectedFileName = "20260318测试-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
|
assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
|
assertThat(storage.generatedKeys).containsExactly(expectedFileName);
|
|
assertThat(storage.generatedKeys).containsExactly(expectedFileName);
|
|
|
|
|
+ JsonNode savedPayload = (JsonNode) ((UnmodeledReportContent) saved.content()).source();
|
|
|
|
|
+ assertThat(savedPayload.path("StudentName").asText()).isEqualTo("20260318测试");
|
|
|
|
|
+ assertThat(savedPayload.path("studentName").asText()).isEqualTo("20260318测试");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 覆盖同步创建展望报告的调用方词汇报文场景,当 lowercase studentName 缺失时,应回退到 reportId 生成文件名。 */
|
|
|
|
|
|
|
+ /** 覆盖同步创建展望报告的调用方词汇报文场景,当只有 StudentName 时,应直接使用 StudentName 生成文件名。 */
|
|
|
@Test
|
|
@Test
|
|
|
void createOutlookReportSyncAcceptsCallerVocabularyPayloadAndReturnsDownloadUrl() throws Exception {
|
|
void createOutlookReportSyncAcceptsCallerVocabularyPayloadAndReturnsDownloadUrl() throws Exception {
|
|
|
TestRepository repository = new TestRepository();
|
|
TestRepository repository = new TestRepository();
|
|
@@ -220,7 +288,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
|
|
assertThat(saved.reportType()).isEqualTo(ReportType.OUTLOOK);
|
|
|
assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
|
|
assertThat(saved.generationStatus()).isEqualTo(ReportGenerationStatus.SUCCESS);
|
|
|
- String expectedFileName = response.reportId() + "-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
|
|
|
|
+ String expectedFileName = "20260318测试-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
|
assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
|
assertThat(storage.generatedKeys).containsExactly(expectedFileName);
|
|
assertThat(storage.generatedKeys).containsExactly(expectedFileName);
|
|
@@ -230,9 +298,9 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
.contains(FIXED_CLOCK.instant().toString());
|
|
.contains(FIXED_CLOCK.instant().toString());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 覆盖同步创建展望报告的调用方词汇报文场景,当 lowercase studentName 为空白时,应回退到 reportId 生成文件名。 */
|
|
|
|
|
|
|
+ /** 覆盖同步创建展望报告的调用方词汇报文场景,当 lowercase studentName 为空白但 StudentName 有效时,应继续使用 StudentName。 */
|
|
|
@Test
|
|
@Test
|
|
|
- void createOutlookReportSyncFallsBackToReportIdWhenLowercaseStudentNameIsBlank() throws Exception {
|
|
|
|
|
|
|
+ void createOutlookReportSyncUsesCanonicalStudentNameWhenLowercaseStudentNameIsBlank() throws Exception {
|
|
|
TestRepository repository = new TestRepository();
|
|
TestRepository repository = new TestRepository();
|
|
|
TestStorage storage = new TestStorage();
|
|
TestStorage storage = new TestStorage();
|
|
|
DefaultExamSprintReportApplicationService service = service(
|
|
DefaultExamSprintReportApplicationService service = service(
|
|
@@ -247,11 +315,14 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
var response = service.createOutlookReportSync(payload);
|
|
var response = service.createOutlookReportSync(payload);
|
|
|
|
|
|
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
- String expectedFileName = response.reportId() + "-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
|
|
|
|
+ String expectedFileName = "20260318测试-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
|
|
assertThat(response.downloadUrl()).isEqualTo("/api/exam-sprint/reports/" + response.reportId() + "/download");
|
|
|
assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
|
assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
|
assertThat(storage.generatedKeys).containsExactly(expectedFileName);
|
|
assertThat(storage.generatedKeys).containsExactly(expectedFileName);
|
|
|
|
|
+ JsonNode savedPayload = (JsonNode) ((UnmodeledReportContent) saved.content()).source();
|
|
|
|
|
+ assertThat(savedPayload.path("StudentName").asText()).isEqualTo("20260318测试");
|
|
|
|
|
+ assertThat(savedPayload.path("studentName").asText()).isEqualTo("20260318测试");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 覆盖同步创建成果报告场景,当有效 achievement payload 生成成功时,应上传 PDF 并返回下载地址。 */
|
|
/** 覆盖同步创建成果报告场景,当有效 achievement payload 生成成功时,应上传 PDF 并返回下载地址。 */
|
|
@@ -405,7 +476,7 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
.doesNotContain("dispatcher unavailable");
|
|
.doesNotContain("dispatcher unavailable");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 覆盖展望报告 payload 防御性复制场景,当调用方提交后继续修改 StudentName 时,已保存内容应保持原值。 */
|
|
|
|
|
|
|
+ /** 覆盖展望报告 payload 防御性复制场景,当调用方提交后继续修改 StudentName/studentName 时,已保存内容应保持归一化原值。 */
|
|
|
@Test
|
|
@Test
|
|
|
void createOutlookReportCopiesPayloadBeforeSaving() {
|
|
void createOutlookReportCopiesPayloadBeforeSaving() {
|
|
|
TestRepository repository = new TestRepository();
|
|
TestRepository repository = new TestRepository();
|
|
@@ -415,11 +486,13 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
var response = service.createOutlookReport(payload);
|
|
var response = service.createOutlookReport(payload);
|
|
|
|
|
|
|
|
payload.put("StudentName", "王同学");
|
|
payload.put("StudentName", "王同学");
|
|
|
|
|
+ payload.put("studentName", "小写别名");
|
|
|
|
|
|
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
UnmodeledReportContent content = (UnmodeledReportContent) saved.content();
|
|
UnmodeledReportContent content = (UnmodeledReportContent) saved.content();
|
|
|
JsonNode savedPayload = (JsonNode) content.source();
|
|
JsonNode savedPayload = (JsonNode) content.source();
|
|
|
assertThat(savedPayload.path("StudentName").asText()).isEqualTo("20260318测试");
|
|
assertThat(savedPayload.path("StudentName").asText()).isEqualTo("20260318测试");
|
|
|
|
|
+ assertThat(savedPayload.path("studentName").asText()).isEqualTo("20260318测试");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 覆盖查询成功报告场景,当报告已生成且未过期时,应返回下载地址并记录查询完成日志。 */
|
|
/** 覆盖查询成功报告场景,当报告已生成且未过期时,应返回下载地址并记录查询完成日志。 */
|
|
@@ -812,6 +885,50 @@ class ExamSprintReportApplicationServiceTest {
|
|
|
(Consumer<ObjectNode>) payload -> payload.withObject("examUnknownWordsHitStatus").putArray("hitWords").add(123)));
|
|
(Consumer<ObjectNode>) payload -> payload.withObject("examUnknownWordsHitStatus").putArray("hitWords").add(123)));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static Stream<Arguments> mixedOutlookPayloadsWithInvalidCanonicalStudentName() {
|
|
|
|
|
+ return Stream.of(
|
|
|
|
|
+ Arguments.of("blank StudentName falls back to lowercase studentName",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> payload.put("StudentName", " \t \n ")),
|
|
|
|
|
+ Arguments.of("null StudentName falls back to lowercase studentName",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> payload.putNull("StudentName")));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Stream<Arguments> invalidCanonicalStudentNameJsonTypes() {
|
|
|
|
|
+ return Stream.of(
|
|
|
|
|
+ Arguments.of("number StudentName is rejected without lowercase fallback",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> {
|
|
|
|
|
+ payload.put("StudentName", 123);
|
|
|
|
|
+ payload.remove("studentName");
|
|
|
|
|
+ }),
|
|
|
|
|
+ Arguments.of("boolean StudentName is rejected without lowercase fallback",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> {
|
|
|
|
|
+ payload.put("StudentName", true);
|
|
|
|
|
+ payload.remove("studentName");
|
|
|
|
|
+ }),
|
|
|
|
|
+ Arguments.of("object StudentName is rejected without lowercase fallback",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> {
|
|
|
|
|
+ payload.set("StudentName", OBJECT_MAPPER.createObjectNode().put("value", "冯亿豪"));
|
|
|
|
|
+ payload.remove("studentName");
|
|
|
|
|
+ }),
|
|
|
|
|
+ Arguments.of("array StudentName is rejected without lowercase fallback",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> {
|
|
|
|
|
+ payload.set("StudentName", OBJECT_MAPPER.createArrayNode().add("冯亿豪"));
|
|
|
|
|
+ payload.remove("studentName");
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Stream<Arguments> invalidLowercaseStudentNameJsonTypes() {
|
|
|
|
|
+ return Stream.of(
|
|
|
|
|
+ Arguments.of("number studentName is rejected as fallback source",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> payload.put("studentName", 123)),
|
|
|
|
|
+ Arguments.of("boolean studentName is rejected as fallback source",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> payload.put("studentName", true)),
|
|
|
|
|
+ Arguments.of("object studentName is rejected as fallback source",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> payload.set("studentName", OBJECT_MAPPER.createObjectNode().put("value", "冯亿豪"))),
|
|
|
|
|
+ Arguments.of("array studentName is rejected as fallback source",
|
|
|
|
|
+ (Consumer<ObjectNode>) payload -> payload.set("studentName", OBJECT_MAPPER.createArrayNode().add("冯亿豪"))));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private void assertCreateOutlookReportRejectsInvalidPayload(JsonNode payload) {
|
|
private void assertCreateOutlookReportRejectsInvalidPayload(JsonNode payload) {
|
|
|
TestRepository repository = new TestRepository();
|
|
TestRepository repository = new TestRepository();
|
|
|
boolean[] dispatched = {false};
|
|
boolean[] dispatched = {false};
|