This commit is contained in:
2026-08-12 19:42:08 +08:00
parent db86e97c0d
commit 47f99befb7
21 changed files with 307 additions and 48 deletions
@@ -0,0 +1,32 @@
package com.baiyue.iam.notification;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class NotificationConfigurationContractTest {
@Test void applicationAndNacosKeepConsumersDisabledAndExposeRecoveryTimeout() throws Exception {
assertSafeDefaults(read(Path.of("iam", "src", "main", "resources", "application.yml"),
Path.of("src", "main", "resources", "application.yml")));
assertSafeDefaults(read(Path.of("config", "nacos", "local", "iam-service.yml"),
Path.of("..", "config", "nacos", "local", "iam-service.yml")));
assertSafeDefaults(read(Path.of("config", "nacos", "cn", "iam-service.yml"),
Path.of("..", "config", "nacos", "cn", "iam-service.yml")));
}
private void assertSafeDefaults(String yaml) {
assertThat(yaml).contains(
"scheduler-enabled: ${NOTIFICATION_SCHEDULER_ENABLED:false}",
"dispatch-timeout-seconds: ${NOTIFICATION_DISPATCH_TIMEOUT_SECONDS:300}",
"mq-consumer-enabled: ${NOTIFICATION_MQ_CONSUMER_ENABLED:false}");
}
private String read(Path reactorPath, Path modulePath) throws Exception {
Path path = Files.exists(reactorPath) ? reactorPath : modulePath;
return Files.readString(path.normalize(), StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,67 @@
package com.baiyue.iam.notification;
import com.baiyue.iam.notification.mapper.NotificationTaskMapper;
import com.baiyue.iam.notification.service.NotificationScheduler;
import com.baiyue.iam.notification.service.NotificationService;
import org.apache.ibatis.annotations.Update;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class NotificationSchedulerRecoveryTest {
@Test void recoverySqlRequiresTimeoutAndNoSuccessfulDeliveryEvidence() throws Exception {
Update retry = NotificationTaskMapper.class
.getMethod("retryTimedOutDispatchingWithoutDeliveryEvidence", java.time.LocalDateTime.class, java.time.LocalDateTime.class)
.getAnnotation(Update.class);
String retrySql = String.join(" ", retry.value()).replaceAll("\\s+", " ").toUpperCase();
assertThat(retrySql).contains("TASK.STATUS = 'DISPATCHING'", "TASK.UPDATED_AT <= #{STALEBEFORE}",
"TASK.SENT_AT IS NULL", "RECIPIENT.STATUS = 'DELIVERED'", "IAM_NOTIFICATION_INBOX")
.contains("SET TASK.STATUS = 'RETRYING'");
Update dead = NotificationTaskMapper.class
.getMethod("deadTimedOutDispatchingWithDeliveryEvidence", java.time.LocalDateTime.class, java.time.LocalDateTime.class)
.getAnnotation(Update.class);
String deadSql = String.join(" ", dead.value()).replaceAll("\\s+", " ").toUpperCase();
assertThat(deadSql).contains("SET TASK.STATUS = 'DEAD'", "TASK.STATUS = 'DISPATCHING'",
"TASK.UPDATED_AT <= #{STALEBEFORE}", "RECIPIENT.STATUS = 'DELIVERED'", "IAM_NOTIFICATION_INBOX",
"DISPATCH_TIMEOUT_WITH_DELIVERY_EVIDENCE");
}
@Test void schedulerRecoversBeforeClaimingScheduledAndRetryingTasks() {
NotificationService service = mock(NotificationService.class);
NotificationScheduler scheduler = new NotificationScheduler();
ReflectionTestUtils.setField(scheduler, "service", service);
ReflectionTestUtils.setField(scheduler, "dispatchTimeoutSeconds", 300L);
when(service.dueScheduledTaskIds()).thenReturn(List.of(11L));
when(service.retryingTaskIds()).thenReturn(List.of(12L));
scheduler.dispatch();
var ordered = inOrder(service);
ordered.verify(service).recoverTimedOutDispatching(300L);
ordered.verify(service).dueScheduledTaskIds();
ordered.verify(service).dispatchScheduled(11L);
ordered.verify(service).retryingTaskIds();
ordered.verify(service).dispatchRetrying(12L);
}
@Test void oneTaskFailureDoesNotBlockRemainingTasksOrRetryQueue() {
NotificationService service = mock(NotificationService.class);
NotificationScheduler scheduler = new NotificationScheduler();
ReflectionTestUtils.setField(scheduler, "service", service);
ReflectionTestUtils.setField(scheduler, "dispatchTimeoutSeconds", 300L);
when(service.dueScheduledTaskIds()).thenReturn(List.of(11L, 12L));
when(service.dispatchScheduled(11L)).thenThrow(new IllegalStateException("failed"));
when(service.retryingTaskIds()).thenReturn(List.of(13L));
scheduler.dispatch();
verify(service).dispatchScheduled(12L);
verify(service).dispatchRetrying(13L);
}
}
@@ -150,6 +150,46 @@ class NotificationServiceTest {
assertThat(error.getValue()).hasSize(1000);
}
@Test void recoversOnlyTasksOlderThanConfiguredDispatchTimeout() {
when(repository.recoverTimedOutDispatching(any(), any())).thenReturn(2);
assertThat(service.recoverTimedOutDispatching(300)).isEqualTo(2);
ArgumentCaptor<LocalDateTime> staleBefore = ArgumentCaptor.forClass(LocalDateTime.class);
ArgumentCaptor<LocalDateTime> now = ArgumentCaptor.forClass(LocalDateTime.class);
verify(repository).recoverTimedOutDispatching(staleBefore.capture(), now.capture());
assertThat(staleBefore.getValue()).isEqualTo(now.getValue().minusSeconds(300));
}
@Test void onlyOneInstanceCanClaimRecoveredTask() {
when(repository.claimRetrying(41L)).thenReturn(true, false);
NotificationTask task = task(41L, "RETRYING");
when(repository.task(41L)).thenReturn(task);
when(repository.recipients(41L)).thenReturn(List.of());
assertThat(service.dispatchRetrying(41L)).isTrue();
assertThat(service.dispatchRetrying(41L)).isFalse();
verify(repository, times(2)).claimRetrying(41L);
verify(repository, times(1)).saveTask(task);
}
@Test void sentTaskCannotBeClaimedForRecoveryDelivery() {
when(repository.claimRetrying(42L)).thenReturn(false);
assertThat(service.dispatchRetrying(42L)).isFalse();
verify(repository, never()).task(42L);
verify(repository, never()).recipients(42L);
}
private NotificationTask task(long id, String status) {
NotificationTask value = new NotificationTask();
value.setId(id); value.setStatus(status); value.setCategory("ORDER"); value.setTitle("A");
value.setRecipientCount(0); value.setAttemptCount(1);
return value;
}
private NotificationEventInbox event(String status, String hash) {
NotificationEventInbox value = new NotificationEventInbox();
value.setId(7L); value.setSourceEventId("event-1"); value.setEventType("order.status.changed");
@@ -1,7 +1,23 @@
package com.baiyue.iam.notification;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class NotificationSqlContractTest {
@Test void definesTablesIsolationIdempotencyAndPermissions() throws Exception {String sql;try(var in=getClass().getResourceAsStream("/db/notification-center.sql")){assertThat(in).isNotNull();sql=new String(in.readAllBytes(),StandardCharsets.UTF_8);}assertThat(sql).contains("iam_notification_template","iam_notification_task","iam_notification_recipient","iam_notification_inbox","iam_notification_event_inbox","uk_notification_task_no","uk_notification_task_idempotency","uk_notification_source_event","uk_notification_recipient","idx_notification_inbox_list","platform:notification:template:status","platform:notification:task:retry","merchant:notification:view");}
@Test void unifiedMigrationContainsNotificationBaselineAndSevenIndexAssertion() throws Exception {
Path direct=Path.of("deploy","sql","头程数据库统一增量-MySQL8.sql");
Path migration=Files.exists(direct)?direct:Path.of("..","deploy","sql","头程数据库统一增量-MySQL8.sql").normalize();
String sql=Files.readString(migration,StandardCharsets.UTF_8);
assertThat(sql).contains("iam_notification_template","iam_notification_task","iam_notification_recipient",
"iam_notification_inbox","iam_notification_event_inbox","uk_notification_template_code",
"uk_notification_task_no","uk_notification_task_idempotency","uk_notification_source_event",
"uk_notification_recipient","uk_notification_inbox_recipient","uk_notification_event",
"assert_notification_center_schema","permission_code='merchant:notification:view'",
"permission_code LIKE 'platform:notification%'");
assertThat(sql).contains("COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema='baiyue_iam'");
assertThat(sql).contains(")) <> 7 THEN");
}
}