feat(app): 应用设置·应用管理——flw_app 白名单 + 学校池同步 + defSave 归属应用校验
- flw_app(sch,app_key uk):学校池(UserBase list-by-school,学校组单点已开通、clientType=null 不过滤端) diff 同步——新增即 enabled=1、改名静默刷新、池外保留;UserBase 不可达降级本地行 - /admin/app/list(触发同步)/ /admin/app/enable;defSave 显式 appKey 须 ∈ 启用集合(不传不拦,升版本继承不受影响) - SQL 备案:flw_app 建表 + UserBase 菜单 107 新增/104 下线(人工执行) - 单测 +8;E2E 13/13(local-debug/four-topics-c-e2e.py) - 关联 SDK 修复:BoaApp.createTime @JsonFormat 对齐 UserBase 时间契约(BasePlatform 单独 commit,Nexus -73)
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.yunhai.flowcenter.entity.FlwApp;
|
||||
import com.yunhai.flowcenter.repository.FlwAppRepository;
|
||||
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||||
import com.yunhai.platform.utils.model.BoaApp;
|
||||
import com.yunhai.platform.utils.model.BoaAppType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 应用设置·应用管理单测(2026-10-10):学校池 diff 同步(新增 enabled=1/改名刷新/池外保留)、
|
||||
* UserBase 不可达降级、开关与跨校守卫。口径拍板:D2 只做最优实现(不做存量 appKey 兜底)、
|
||||
* D3 clientType=null 不过滤端、D7 学校组单点池。
|
||||
*/
|
||||
class FlowAppAdminServiceTest {
|
||||
|
||||
private static final Long SCH = 990001L;
|
||||
|
||||
private FlwAppRepository appRepository;
|
||||
private OpenApiAppClient appClient;
|
||||
private FlowAppAdminService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
appRepository = mock(FlwAppRepository.class);
|
||||
appClient = mock(OpenApiAppClient.class);
|
||||
service = new FlowAppAdminService(appRepository, appClient);
|
||||
when(appRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
}
|
||||
|
||||
private BoaAppType type(BoaApp... apps) {
|
||||
BoaAppType t = new BoaAppType();
|
||||
t.setId(2);
|
||||
t.setName("办公管理");
|
||||
t.setApps(Arrays.asList(apps));
|
||||
return t;
|
||||
}
|
||||
|
||||
private BoaApp app(Integer appKey, String name, Integer orderNumber) {
|
||||
BoaApp a = new BoaApp();
|
||||
a.setAppKey(appKey);
|
||||
a.setName(name);
|
||||
a.setOrderNumber(orderNumber);
|
||||
return a;
|
||||
}
|
||||
|
||||
private FlwApp row(String appKey, String name, int enabled) {
|
||||
FlwApp r = new FlwApp();
|
||||
r.setId((long) appKey.hashCode());
|
||||
r.setSchId(SCH);
|
||||
r.setAppKey(appKey);
|
||||
r.setAppName(name);
|
||||
r.setEnabled(enabled);
|
||||
return r;
|
||||
}
|
||||
|
||||
@Test
|
||||
void sync_池内新应用_插入且默认启用() {
|
||||
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.singletonList(
|
||||
type(app(100002, "通知公告", 1))));
|
||||
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
|
||||
|
||||
service.syncFromSchoolPool(SCH);
|
||||
|
||||
ArgumentCaptor<FlwApp> cap = ArgumentCaptor.forClass(FlwApp.class);
|
||||
verify(appRepository, times(1)).save(cap.capture());
|
||||
assertEquals("100002", cap.getValue().getAppKey());
|
||||
assertEquals("通知公告", cap.getValue().getAppName());
|
||||
assertEquals(1, cap.getValue().getEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sync_改名静默刷新_开关不动_池外行保留() {
|
||||
FlwApp renamed = row("100002", "通知公告", 1);
|
||||
FlwApp untouchedEnabled = row("100008", "请假", 0); // 池外且已停用:保留不动
|
||||
FlwApp untouchedName = row("100010", "旧名", 1); // 池外:名称也不动
|
||||
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.singletonList(
|
||||
type(app(100002, "通知公告(校内别名)", 9))));
|
||||
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH))
|
||||
.thenReturn(Arrays.asList(renamed, untouchedEnabled, untouchedName));
|
||||
|
||||
service.syncFromSchoolPool(SCH);
|
||||
|
||||
verify(appRepository, times(1)).save(any(FlwApp.class));
|
||||
assertEquals("通知公告(校内别名)", renamed.getAppName());
|
||||
assertEquals(1, renamed.getEnabled());
|
||||
assertEquals("旧名", untouchedName.getAppName());
|
||||
assertEquals(0, untouchedEnabled.getEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void list_UserBase不可达_降级返回本地行() {
|
||||
when(appClient.getSchoolApp(any(), any())).thenThrow(new RuntimeException("connect timeout"));
|
||||
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH))
|
||||
.thenReturn(Collections.singletonList(row("100002", "通知公告", 1)));
|
||||
|
||||
List<FlwApp> rows = service.list(SCH);
|
||||
|
||||
assertEquals(1, rows.size());
|
||||
assertEquals("100002", rows.get(0).getAppKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void list_正常链路_先同步再返回() {
|
||||
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.emptyList());
|
||||
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
|
||||
assertEquals(0, service.list(SCH).size());
|
||||
verify(appClient, times(1)).getSchoolApp(990001, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void enable_切换与守卫() {
|
||||
FlwApp r = row("100002", "通知公告", 1);
|
||||
when(appRepository.findById(r.getId())).thenReturn(java.util.Optional.of(r));
|
||||
|
||||
service.enable(SCH, r.getId(), false);
|
||||
assertEquals(0, r.getEnabled());
|
||||
|
||||
service.enable(SCH, r.getId(), true);
|
||||
assertEquals(1, r.getEnabled());
|
||||
|
||||
FlwApp other = row("100002", "x", 1);
|
||||
other.setSchId(2L);
|
||||
when(appRepository.findById(other.getId())).thenReturn(java.util.Optional.of(other));
|
||||
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> service.enable(SCH, other.getId(), true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sync_池为空_零写入() {
|
||||
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.emptyList());
|
||||
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
|
||||
|
||||
service.syncFromSchoolPool(SCH);
|
||||
|
||||
verify(appRepository, never()).save(any());
|
||||
assertTrue(service.list(SCH).isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -37,18 +37,23 @@ class FlowDefAdminServiceSaveTest {
|
||||
private static final String JSON = "{\"nodes\":[],\"edges\":[]}";
|
||||
|
||||
private FlwDefRepository defRepository;
|
||||
private com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
|
||||
private FlowDefAdminService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
appRepository = mock(com.yunhai.flowcenter.repository.FlwAppRepository.class);
|
||||
service = new FlowDefAdminService(defRepository,
|
||||
mock(FlwAssigneeMapRepository.class),
|
||||
mock(FlwRouterRepository.class),
|
||||
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
|
||||
appRepository,
|
||||
mock(FlowEngine.class),
|
||||
new ObjectMapper());
|
||||
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
// 归属应用白名单:非空 appKey 一律放行(校验拦截语义由专门用例覆盖)
|
||||
when(appRepository.isEnabledApp(any(), any())).thenReturn(true);
|
||||
}
|
||||
|
||||
private FlwDef def(long id, String flowGuid, String code, int version, int status) {
|
||||
@@ -168,4 +173,23 @@ class FlowDefAdminServiceSaveTest {
|
||||
() -> service.save(SCH, 9L, "work-leave", "请假", JSON, null, null));
|
||||
assertTrue(e.getMessage().contains("保存冲突"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appKey不在启用白名单_保存被拦() {
|
||||
when(appRepository.isEnabledApp(SCH, "999999")).thenReturn(false);
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> service.save(SCH, null, "work-leave", "请假", JSON, null, "999999"));
|
||||
assertTrue(e.getMessage().contains("应用设置"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appKey不传_白名单校验不触发() {
|
||||
// 升版本继承源线(cleanAppKey=null)不查白名单——存量线升版本不受切换影响
|
||||
when(defRepository.findById(9L)).thenReturn(Optional.of(def(9L, "G", "work-leave", 4, FlwEnums.DefStatus.PUBLISHED.getCode())));
|
||||
when(defRepository.maxVersionByGuid(SCH, "G")).thenReturn(4);
|
||||
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, "G")).thenReturn(Optional.empty());
|
||||
FlwDef saved = service.save(SCH, 9L, "work-leave", "请假", JSON, null, null);
|
||||
org.mockito.Mockito.verify(appRepository, org.mockito.Mockito.never()).isEnabledApp(any(), any());
|
||||
assertNull(saved.getAppKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +483,8 @@ class FlowDiagramServiceTest {
|
||||
mock(com.yunhai.flowcenter.service.FlowRouterAdminService.class),
|
||||
mock(FlwInstanceRepository.class), diagramService,
|
||||
mock(com.yunhai.flowcenter.service.DiagramImageService.class),
|
||||
new ObjectMapper());
|
||||
new ObjectMapper(),
|
||||
mock(com.yunhai.flowcenter.service.FlowAppAdminService.class));
|
||||
}
|
||||
|
||||
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
|
||||
|
||||
Reference in New Issue
Block a user