test: add M1 broadcast acceptance reproducers
This commit is contained in:
+161
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
const read = (path: string) => readFileSync(resolve(process.cwd(), path), 'utf8');
|
||||||
|
|
||||||
|
describe('M1 银城大喇叭发布契约', () => {
|
||||||
|
it('管理端携带必读和定向范围,并使用受控目标选项', () => {
|
||||||
|
const api = read('src/api/aihr/broadcast.ts');
|
||||||
|
const page = read('src/views/content/broadcast.vue');
|
||||||
|
const pending = read('src/utils/broadcast-pending.ts');
|
||||||
|
|
||||||
|
expect(api).toContain('requiredRead: boolean;');
|
||||||
|
expect(api).toContain('targets: BroadcastTargetRequest;');
|
||||||
|
expect(api).toContain("'/api/aihr/broadcast/admin/target-options'");
|
||||||
|
expect(page).toContain('v-model="form.requiredRead"');
|
||||||
|
expect(page).toContain('v-model="form.targets.projectCodes"');
|
||||||
|
expect(page).toContain('v-model="form.targets.positionNames"');
|
||||||
|
expect(page).toContain('v-model="form.targets.positionLevels"');
|
||||||
|
expect(page).toContain('loadBroadcastTargetOptions');
|
||||||
|
expect(page).toContain('matchedRecipientCount');
|
||||||
|
expect(pending).toContain('requiredRead: boolean;');
|
||||||
|
expect(pending).toContain('targets: BroadcastTargetRequest;');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||||
|
|
||||||
|
test('M1 员工端可筛选与我有关和必读未读消息,并保留公开频道', async () => {
|
||||||
|
const [service, list, detail] = await Promise.all([
|
||||||
|
read('../src/services/broadcast.ts'),
|
||||||
|
read('../src/pages/user/broadcast/index.vue'),
|
||||||
|
read('../src/pages/user/broadcast/detail.vue')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.match(service, /export type BroadcastMessageScope = 'ALL' \| 'RELATED' \| 'REQUIRED_UNREAD';/);
|
||||||
|
assert.match(service, /scope=\$\{scope\}/);
|
||||||
|
assert.match(list, /const scope = ref<BroadcastMessageScope>\('ALL'\)/);
|
||||||
|
assert.match(list, /scopeOptions/);
|
||||||
|
assert.match(list, /@click="changeScope\(option\.value\)"/);
|
||||||
|
assert.match(list, /listBroadcastMessages\(1, pageSize, scope\.value\)/);
|
||||||
|
assert.match(list, /item\.requiredRead/);
|
||||||
|
assert.match(list, /item\.targeted/);
|
||||||
|
assert.match(detail, /item\.requiredRead/);
|
||||||
|
assert.match(detail, /item\.targeted/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user