优化
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package com.baiyue.iam.notification;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class NotificationOpenApiContractTest {
|
||||
@Test
|
||||
void parsesAndExposesFrozenManagementAndInboxContracts() throws IOException {
|
||||
String document;
|
||||
try (var stream = getClass().getResourceAsStream("/openapi/notification-v1.yaml")) {
|
||||
assertThat(stream).isNotNull();
|
||||
document = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
Map<String, Object> api = new Yaml().load(document);
|
||||
assertThat(api).containsEntry("openapi", "3.0.3");
|
||||
Map<String, Object> paths = map(api.get("paths"));
|
||||
assertThat(paths).containsKeys(
|
||||
"/{app}/v1/notifications", "/{app}/v1/notifications/unread-count",
|
||||
"/{app}/v1/notifications/{id}", "/{app}/v1/notifications/{id}/read",
|
||||
"/{app}/v1/notifications/read-all", "/platform/v1/notifications",
|
||||
"/platform/v1/notification-templates", "/platform/v1/notification-templates/{id}",
|
||||
"/platform/v1/notification-templates/{id}/status", "/platform/v1/notification-tasks",
|
||||
"/platform/v1/notification-tasks/{id}/cancel", "/platform/v1/notification-tasks/{id}/retry");
|
||||
assertThat(paths).doesNotContainKey("/merchant/v1/notifications:create");
|
||||
|
||||
assertRequestBody(paths, "/platform/v1/notifications", "post");
|
||||
assertRequestBody(paths, "/platform/v1/notification-templates", "post");
|
||||
assertRequestBody(paths, "/platform/v1/notification-templates/{id}", "put");
|
||||
assertRequestBody(paths, "/platform/v1/notification-templates/{id}/status", "put");
|
||||
for (String path : List.of("/platform/v1/notification-templates/{id}",
|
||||
"/platform/v1/notification-templates/{id}/status",
|
||||
"/platform/v1/notification-tasks/{id}/cancel",
|
||||
"/platform/v1/notification-tasks/{id}/retry")) {
|
||||
Map<String, Object> operation = map(map(paths.get(path)).values().iterator().next());
|
||||
assertThat(list(operation.get("parameters"))).anySatisfy(value ->
|
||||
assertThat(map(value)).containsEntry("$ref", "#/components/parameters/IdPath"));
|
||||
}
|
||||
|
||||
Map<String, Object> components = map(api.get("components"));
|
||||
Map<String, Object> schemas = map(components.get("schemas"));
|
||||
assertThat(schemas).containsKeys("CreateNotificationRequest", "CreateTemplateRequest",
|
||||
"UpdateTemplateRequest", "ChangeTemplateStatusRequest", "InboxPageResponse",
|
||||
"InboxDetailResponse", "UnreadCountResponse", "ReadAllResponse",
|
||||
"TemplateItem", "TemplateDetailResponse", "TaskItem", "TaskDetailResponse");
|
||||
for (String schema : List.of("CreateNotificationRequest", "CreateTemplateRequest",
|
||||
"UpdateTemplateRequest", "ChangeTemplateStatusRequest", "InboxItem", "TemplateItem",
|
||||
"TaskItem", "InboxPageResponse", "TemplateDetailResponse", "TaskDetailResponse")) {
|
||||
assertThat(list(map(schemas.get(schema)).get("required"))).isNotEmpty();
|
||||
}
|
||||
assertThat(document).contains("tenantId is a backend compatibility field")
|
||||
.contains("DISABLED cannot return to ACTIVE")
|
||||
.contains("does not support deleting or clearing read messages")
|
||||
.contains("name: taskNo").contains("name: templateCode").contains("name: title");
|
||||
}
|
||||
|
||||
private void assertRequestBody(Map<String, Object> paths, String path, String method) {
|
||||
assertThat(map(map(paths.get(path)).get(method))).containsKey("requestBody");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> map(Object value) { return (Map<String, Object>) value; }
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Object> list(Object value) { return (List<Object>) value; }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.baiyue.iam.notification;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.notification.dto.NotificationDtos.*;
|
||||
import com.baiyue.iam.notification.entity.NotificationRecipient;
|
||||
import com.baiyue.iam.notification.entity.NotificationTask;
|
||||
import com.baiyue.iam.notification.entity.NotificationTemplate;
|
||||
import com.baiyue.iam.notification.repository.NotificationRepository;
|
||||
import com.baiyue.iam.notification.service.NotificationRecipientResolver;
|
||||
import com.baiyue.iam.notification.service.NotificationService;
|
||||
import com.baiyue.iam.notification.service.NotificationTemplateRenderer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class NotificationServiceTest {
|
||||
private final NotificationRepository repository = mock(NotificationRepository.class);
|
||||
private final NotificationRecipientResolver resolver = mock(NotificationRecipientResolver.class);
|
||||
private final NotificationService service = new NotificationService();
|
||||
|
||||
@BeforeEach void setup() {
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "renderer", new NotificationTemplateRenderer());
|
||||
ReflectionTestUtils.setField(service, "resolver", resolver);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
}
|
||||
|
||||
@Test void createsTaskAndImmutableRecipientSnapshot() {
|
||||
List<NotificationRecipient> stored = new ArrayList<>();
|
||||
doAnswer(call -> { stored.add(call.getArgument(0)); return null; }).when(repository).insertRecipient(any());
|
||||
when(repository.recipients(anyLong())).thenAnswer(call -> List.copyOf(stored));
|
||||
NotificationTemplate template = activeTemplate();
|
||||
when(repository.templateByCode(0, "ORDER_STATUS_CHANGED")).thenReturn(template);
|
||||
when(resolver.resolve(any())).thenReturn(List.of(
|
||||
new NotificationRecipientResolver.ResolvedRecipient(9, 11, 12, "User", "MERCHANT")));
|
||||
|
||||
var request = new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED", RecipientType.MEMBERSHIP,
|
||||
List.of(11L), null, Map.of("order_no", "A", "status", "READY"), null, "idem-key-1", null);
|
||||
TaskListItem result = service.create(request, 99);
|
||||
|
||||
assertThat(result.status()).isEqualTo("SENT");
|
||||
verify(repository).insertTask(any());
|
||||
verify(repository).insertRecipient(any());
|
||||
verify(repository).insertInbox(any());
|
||||
ArgumentCaptor<NotificationTask> task = ArgumentCaptor.forClass(NotificationTask.class);
|
||||
verify(repository).saveTask(task.capture());
|
||||
assertThat(task.getValue().getTitle()).isEqualTo("A");
|
||||
assertThat(stored).hasSize(1);
|
||||
assertThat(stored.get(0).getStatus()).isEqualTo("DELIVERED");
|
||||
}
|
||||
|
||||
@Test void returnsOriginalTaskForRepeatedIdempotencyKey() {
|
||||
NotificationTask existing = new NotificationTask(); existing.setId(5L); existing.setTaskNo("NT5");
|
||||
existing.setCategory("ORDER"); existing.setTitle("A"); existing.setStatus("SENT"); existing.setRecipientCount(1);
|
||||
when(repository.taskByIdempotency("idem-key-1")).thenReturn(existing);
|
||||
var request = new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED", RecipientType.MEMBERSHIP,
|
||||
List.of(11L), null, Map.of(), null, "idem-key-1", null);
|
||||
assertThat(service.create(request, 99).duplicated()).isTrue();
|
||||
verify(repository, never()).insertTask(any());
|
||||
}
|
||||
|
||||
@Test void disabledTemplateCannotReturnToActive() {
|
||||
NotificationTemplate template = new NotificationTemplate(); template.setId(1L);
|
||||
template.setStatus("DISABLED"); template.setVersion(3);
|
||||
when(repository.template(1L)).thenReturn(template);
|
||||
assertThatThrownBy(() -> service.changeTemplateStatus(1L,
|
||||
new ChangeTemplateStatusRequest("ACTIVE", 3), 9))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_INVALID_STATE");
|
||||
}
|
||||
|
||||
private NotificationTemplate activeTemplate() {
|
||||
NotificationTemplate value = new NotificationTemplate(); value.setId(1L);
|
||||
value.setTemplateCode("ORDER_STATUS_CHANGED"); value.setCategory("ORDER");
|
||||
value.setTitleTemplate("{{order_no}}"); value.setBodyTemplate("{{status}}");
|
||||
value.setVariableWhitelistJson("[\"order_no\",\"status\"]"); value.setStatus("ACTIVE");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.baiyue.iam.notification;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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_idempotency","uk_notification_source_event","uk_notification_recipient","idx_notification_inbox_list","platform:notification:template:status","platform:notification:task:retry","merchant:notification:view");}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.iam.notification;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.notification.service.NotificationTemplateRenderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
class NotificationTemplateRendererTest {
|
||||
private final NotificationTemplateRenderer renderer=new NotificationTemplateRenderer();
|
||||
@Test void rendersOnlyWhitelistedVariables(){assertThat(renderer.render("Order {{order_no}} is {{status}}",Set.of("order_no","status"),Map.of("order_no","A1","status","ready"))).isEqualTo("Order A1 is ready");}
|
||||
@Test void rejectsMissingAndUnknownVariables(){assertThatThrownBy(()->renderer.render("{{order_no}}",Set.of("order_no"),Map.of())).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_MISSING");assertThatThrownBy(()->renderer.render("ok",Set.of(),Map.of("secret","x"))).isInstanceOf(BizException.class).hasMessageContaining("NOTIFICATION_VARIABLE_NOT_ALLOWED");}
|
||||
}
|
||||
Reference in New Issue
Block a user