test: add M1 broadcast acceptance reproducers

This commit is contained in:
2026-07-23 11:27:53 +08:00
parent f023417aed
commit cd94f4fe9f
3 changed files with 210 additions and 0 deletions
@@ -0,0 +1,161 @@
package org.dromara.aihr.broadcast;
import org.dromara.aihr.broadcast.AihrBroadcastDto.BroadcastListResponse;
import org.dromara.aihr.broadcast.AihrBroadcastDto.BroadcastTargetRequest;
import org.dromara.aihr.broadcast.AihrBroadcastDto.PublishRequest;
import org.dromara.aihr.broadcast.AihrBroadcastDto.PublishResponse;
import org.dromara.aihr.knowledge.domain.AihrKnowledgePrincipal;
import org.dromara.aihr.knowledge.service.AihrKnowledgePrincipalResolver;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.KeyHolder;
import java.lang.reflect.Proxy;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("dev")
class AihrBroadcastM1ServiceTest {
@Test
void targetedPublicationFreezesActiveOrganizationRecipientsAndKeepsUnmatchedSubjectsAuditable() {
TargetingJdbcTemplate jdbcTemplate = new TargetingJdbcTemplate();
jdbcTemplate.targetCandidates = List.of(
Map.of("subject_ref", "EMP-1003", "user_id", 31L),
Map.of("subject_ref", "EMP-1004")
);
AihrBroadcastService service = new AihrBroadcastService(jdbcTemplate, RESOLVER);
PublishResponse response = service.publish(new PublishRequest(
"m1-targeted-1", "夏季服务标准", "请项目客服人员在今日完成学习。", "tenant-a", true,
new BroadcastTargetRequest(List.of("PRJ-FCW"), List.of("物业管家"), List.of())
));
assertEquals(42L, response.id());
assertEquals(1L, response.matchedRecipientCount());
assertEquals(1L, response.unmatchedTargetCount());
assertTrue(jdbcTemplate.targetCandidateSql.contains("from aihr_org_snapshot o"));
assertTrue(jdbcTemplate.targetCandidateSql.contains("binary o.tenant_id = binary u.tenant_id"));
assertTrue(jdbcTemplate.targetCandidateSql.contains("o.employment_status = 'active'"));
assertTrue(jdbcTemplate.targetCandidateSql.contains("o.project_code in (?)"));
assertTrue(jdbcTemplate.targetCandidateSql.contains("o.position_name in (?)"));
assertArrayEquals(new Object[]{"tenant-a", "PRJ-FCW", "物业管家"}, jdbcTemplate.targetCandidateArgs);
assertTrue(jdbcTemplate.updateSqls.stream().anyMatch(sql -> sql.contains("aihr_broadcast_target_rule")));
assertTrue(jdbcTemplate.updateSqls.stream().anyMatch(sql -> sql.contains("aihr_broadcast_target_recipient")));
}
@Test
void requiredUnreadScopeUsesRecipientSnapshotsWhileAllScopeKeepsPublicMessagesVisible() {
TargetingJdbcTemplate jdbcTemplate = new TargetingJdbcTemplate();
AihrBroadcastService service = new AihrBroadcastService(jdbcTemplate, RESOLVER);
BroadcastListResponse requiredUnread = service.messages(1, 20, "REQUIRED_UNREAD");
assertEquals(1, requiredUnread.rows().size());
assertTrue(requiredUnread.rows().get(0).requiredRead());
assertTrue(requiredUnread.rows().get(0).targeted());
assertFalse(requiredUnread.rows().get(0).read());
assertTrue(jdbcTemplate.messageListSql.contains("m.required_read = 1"));
assertTrue(jdbcTemplate.messageListSql.contains("aihr_broadcast_target_recipient"));
assertTrue(jdbcTemplate.messageListSql.contains("tr.user_id is not null"));
service.messages(1, 20, "ALL");
assertFalse(jdbcTemplate.messageListSql.contains("tr.user_id is not null"));
assertTrue(jdbcTemplate.messageListSql.contains("m.status = 'PUBLISHED'"));
}
private static final AihrKnowledgePrincipalResolver RESOLVER = new AihrKnowledgePrincipalResolver(null) {
@Override
public AihrKnowledgePrincipal current() {
return new AihrKnowledgePrincipal(
"tenant-a", 7L, "app_user", "staff-7", Set.of("employee"), Set.of(), "mobile");
}
};
private static final class TargetingJdbcTemplate extends JdbcTemplate {
private List<Map<String, Object>> targetCandidates = List.of();
private String targetCandidateSql;
private Object[] targetCandidateArgs;
private String messageListSql;
private final List<String> updateSqls = new ArrayList<>();
@Override
public <T> T queryForObject(String sql, Class<T> requiredType, Object... args) {
if (sql.contains("from sys_user u")) return requiredType.cast(1L);
return requiredType.cast(1L);
}
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
if (sql.contains("publish_request_key")) return List.of();
if (sql.contains("from aihr_org_snapshot o")) {
targetCandidateSql = sql;
targetCandidateArgs = args;
return targetCandidates.stream().map(values -> map(rowMapper, values)).toList();
}
if (sql.contains("from aihr_broadcast_message m")) {
messageListSql = sql;
return List.of(map(rowMapper, Map.of(
"id", 42L,
"title", "夏季服务标准",
"published_time", Timestamp.valueOf("2026-07-23 09:00:00"),
"read_flag", false,
"required_read", true,
"targeted_flag", true,
"target_reason", "项目、岗位"
)));
}
return List.of();
}
@Override
public int update(String sql, Object... args) {
updateSqls.add(sql);
return 1;
}
@Override
public int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) {
generatedKeyHolder.getKeyList().add(Map.of("id", 42L));
return 1;
}
private static <T> T map(RowMapper<T> rowMapper, Map<String, Object> values) {
try {
return rowMapper.mapRow(resultSet(values), 0);
} catch (SQLException ex) {
throw new AssertionError(ex);
}
}
private static ResultSet resultSet(Map<String, Object> values) {
return (ResultSet) Proxy.newProxyInstance(
TargetingJdbcTemplate.class.getClassLoader(), new Class<?>[]{ResultSet.class}, (proxy, method, args) -> {
String name = method.getName();
Object value = args == null || args.length == 0 ? null : values.get(args[0]);
if ("getString".equals(name)) return value == null ? null : value.toString();
if ("getLong".equals(name)) return value == null ? 0L : ((Number) value).longValue();
if ("getBoolean".equals(name)) return value instanceof Boolean bool && bool;
if ("getTimestamp".equals(name)) return value;
if ("getObject".equals(name)) return value;
if ("wasNull".equals(name)) return value == null;
if ("toString".equals(name)) return values.toString();
throw new UnsupportedOperationException(name);
});
}
}
}