This commit is contained in:
dzc committed 2026-10-29 23:50:19 +08:00
1 parent 8b5b564292
commit 9347410909
8 files changed
+257 -7

No files matched your search

+9 -1
View File
@@ -76,14 +76,22 @@ Base64URL无填充编码的UTF-8文件名
file-access:
public-image-referer-prefixes:
- https://www.pjzhhq.cn/mobile/
- https://yyfb.idw.jinhua.gov.cn:6443/
```
只对 GET/HEAD 图片读取生效:请求带有单个 `Referer`,其协议、主机、端口与前缀匹配,且路径位于 `/mobile/` 下时,JPG/JPEG/PNG/GIF/WebP/BMP/SVG/ICO 可以不带 appId 或签名读取。页面 URL 的 `#/` 不会随 HTTP Referer 发送。来源命中的图片请求整体免签,因此旧页面携带的过期签名也不阻止加载。重复关键参数、非法文件名和浏览器页面导航仍优先拒绝;PDF、APK、日志等文件及上传接口不享受此例外。
只对 GET/HEAD 图片读取生效:请求带有单个 `Referer`,其协议、主机、有效端口与配置匹配时,按以下两种范围判断:
- 带目录的配置(如 `/mobile/`)仍要求 Referer 路径位于该目录下,目录必须以 `/` 结尾。
- 根来源配置(如 `https://yyfb.idw.jinhua.gov.cn:6443/`,末尾 `/` 可省略)允许该来源下所有页面,包括浏览器跨域策略只发送根来源的请求。端口必须匹配;未写端口的 HTTPS 表示 443,不会匹配 6443。根来源不能再限制到某个应用目录。
命中后,JPG/JPEG/PNG/GIF/WebP/BMP/SVG/ICO 可以不带 appId 或签名读取。页面 URL 的 `#/` 不会随 HTTP Referer 发送,也不能写进配置。跨域策略如果省略路径,单纯配置长目录不会自动退回根来源匹配。来源命中的图片请求整体免签,因此旧页面携带的过期签名也不阻止加载。重复关键参数、非法文件名和浏览器页面导航仍优先拒绝;PDF、APK、日志等文件及上传接口不享受此例外。
这是独立的来源白名单,保留或省略 `clients.h5` 都可使用;不能仅根据 `appId=h5` 放行。新版本允许省略整个 H5 客户端配置,但空密钥、错误密钥配置仍报错,其他五类客户端的必要配置保持严格检查。若删除共享配置中的 H5 条目,所有加载旧版公共配置类的服务也需要同步更新;保留现有 H5 配置同样可以启用来源兼容。
配置默认空列表;显式清空列表即可恢复图片签名要求。必须部署新 UPMS 和对应的公共依赖后再启用配置,不能用旧后端只删除 H5 密钥。Referer 必须由浏览器和代理保留;没有 Referer 的匿名请求仍返回 403。Referer 可被脚本模拟,这是一层兼容性的来源检查。成功响应私有缓存最多 5 分钟,`Vary` 额外区分 Referer,代理不得忽略该差异复用放行响应。
根来源支持需要新版 `pjcloud-common-core` 和 UPMS 一起构建发布。建议将这份图片来源白名单放在 UPMS 自己的配置中;若通过共享配置下发给其他启用文件访问配置的服务,这些服务也必须先更新公共依赖,旧版校验类会拒绝根来源。此改动不要求更新 H5 或更换任何密钥。
## 上传身份与文件类型
现有登录鉴权及 `@Inner` 内部调用限制保留。每次 POST multipart 上传只允许一个名为 `file` 的文件部分,通过下列请求头传递身份,不把上传凭据放进 URL 或业务字段:
+4 -1
View File
@@ -8,10 +8,13 @@ file-access:
public-base-url: https://files.example.com/api
allowed-origins:
- https://legacy-files.example.com
# Optional: old H5 needs no update; only image reads from this Referer prefix bypass signatures.
# Optional: old H5 needs no update; only GET/HEAD image reads bypass signatures.
# Remove this list or set [] to keep all browser sources subject to signing.
public-image-referer-prefixes:
- https://www.pjzhhq.cn/mobile/
# Root origin allows every page on this exact scheme/host/port (trailing slash optional).
# Use only when cross-origin browser requests omit the Referer path; requires updated common-core/UPMS.
# - https://yyfb.idw.jinhua.gov.cn:6443/
legacy-dart:
enabled: true
extensions: [jpg, jpeg, png, gif, webp, bmp, svg, ico]
+10
View File
@@ -140,3 +140,13 @@ Java 定向测试及各端构建命令见 [接入与发布说明](README.md)。
- 实际检查线上 `/mobile/` 返回 200,未发现禁用 Referer 的响应头或 HTML meta;这不代替后端部署后的真机验收。
精确前缀已加入本机被忽略的生成配置,其余配置和密钥保留。生产 Nacos 和线上服务未修改;需要部署新 UPMS/公共依赖并加载来源白名单,H5 无需重新发布。Referer 可被脚本模拟,此功能为应用级来源兼容。
## 政务 H5 根来源兼容补充
2026-10-29 实际检查政务 H5 的图片请求:页面位于 `https://yyfb.idw.jinhua.gov.cn:6443/web/mgop/gov-open/zj/2002546649/reserved/index.html#/`,但浏览器使用 `strict-origin-when-cross-origin`,发往文件中心的 Referer 仅为 `https://yyfb.idw.jinhua.gov.cn:6443/`。因此原目录前缀无法命中。
现有 `public-image-referer-prefixes` 同时支持目录前缀和显式根来源;根来源末尾 `/` 可省略,允许同协议、同主机、同有效端口下所有页面的图片请求。目录配置仍严格匹配目录,不会因 Referer 省略路径而自动扩大范围。仅允许 GET/HEAD 图片、阻止浏览器导航、上传鉴权、私有缓存和 Vary 规则保持不变。
- 执行 `mvn -pl pjcloud-upms/pjcloud-upms-biz -am test -Dtest=FileAccessTest,FileAccessMobileTest,FileUploadGuardTest -Dsurefire.failIfNoSpecifiedTests=false`:23 项通过,0 失败、错误或跳过,Maven 构建成功。
- 新增测试覆盖实际根 Referer、完整页面 Referer、无末尾斜杠的配置、默认/自定义端口、域名伪造、错误协议、重复/缺失 Referer、目录范围不扩大、关闭白名单、导航及非图片拒绝、上传仍需鉴权、GET/HEAD 请求到存储的边界。
- 本机验证日志为 `/tmp/pj-file-access-origin-tests.log`。未修改线上配置或发布服务,线上恢复情况需在新版 UPMS 和对应公共依赖发布后确认。若根来源配置共享给其他启用文件配置的服务,也需先更新这些服务的公共依赖。
@@ -16,6 +16,7 @@ public class FileAccessProperties implements InitializingBean {
private boolean blockBrowserNavigation = true;
private String publicBaseUrl = "";
private List<String> allowedOrigins = new ArrayList<>();
/** An empty or root URI path allows the whole origin; other paths must end with '/'. */
private List<String> publicImageRefererPrefixes = new ArrayList<>();
private Map<String, Client> clients = new LinkedHashMap<>();
private LegacyDart legacyDart = new LegacyDart();
@@ -80,7 +81,7 @@ public class FileAccessProperties implements InitializingBean {
String path = uri.getRawPath();
if (uri.getHost() == null || !("https".equalsIgnoreCase(uri.getScheme()) || "http".equalsIgnoreCase(uri.getScheme()))
|| uri.getUserInfo() != null || uri.getRawQuery() != null || uri.getRawFragment() != null
|| path == null || path.length() <= 1 || !path.endsWith("/") || path.contains("%")
|| path == null || (!path.isEmpty() && !path.endsWith("/")) || path.contains("%")
|| !path.equals(uri.normalize().getRawPath())) throw new IllegalArgumentException();
} catch (RuntimeException e) { throw new IllegalStateException("Invalid public image Referer prefix"); }
}
@@ -99,9 +99,13 @@ public class FileAccessGuard {
String path = new URI(null, null, referer.getPath(), null).normalize().getPath();
for (String configured : properties.getPublicImageRefererPrefixes()) {
URI prefix = URI.create(configured);
// Cross-origin browser requests can retain only the origin in Referer.
// Only an explicitly configured root allows every path on that origin.
boolean wholeOrigin = prefix.getRawPath().isEmpty() || "/".equals(prefix.getRawPath());
if (prefix.getScheme().equalsIgnoreCase(referer.getScheme())
&& prefix.getHost().equalsIgnoreCase(referer.getHost()) && port(prefix) == port(referer)
&& referer.getRawPath().startsWith(prefix.getRawPath()) && path.startsWith(prefix.getPath())) return true;
&& (wholeOrigin || (referer.getRawPath().startsWith(prefix.getRawPath())
&& path.startsWith(prefix.getPath())))) return true;
}
} catch (IllegalArgumentException | URISyntaxException ignored) { }
return false;
@@ -261,9 +261,10 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
dataScope.setScopeUserName(null);
IPage<UserVO> list = baseMapper.getUserVosPage(page, userDTO, dataScope);
if (ObjectUtil.isNotEmpty(list.getRecords())) {
List<Long> userIds = list.getRecords().stream().map(UserVO::getUserId).distinct().toList();
List<Long> userIds = list.getRecords().stream().map(UserVO::getUserId).filter(Objects::nonNull).distinct().toList();
if (ObjectUtil.isNotEmpty(userIds)) {
SysUserFaceDto faceDto = new SysUserFaceDto();
faceDto.setUserIds(userIds);
List<SysUserFaceVo> faceList = sysUserFaceMapper.groupConcatFaces(faceDto);
DataUtil.convertFacesList(list.getRecords(), CollectionUtils.convertMap(faceList, SysUserFaceVo::getUserId));
}
@@ -292,9 +293,10 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
public IPage<UserVO> innerPage(Page page, UserDTO userDTO) {
IPage<UserVO> list = baseMapper.getUserVosPage(page, userDTO, null);
if (ObjectUtil.isNotEmpty(list.getRecords())) {
List<Long> userIds = list.getRecords().stream().map(UserVO::getUserId).distinct().toList();
List<Long> userIds = list.getRecords().stream().map(UserVO::getUserId).filter(Objects::nonNull).distinct().toList();
if (ObjectUtil.isNotEmpty(userIds)) {
SysUserFaceDto faceDto = new SysUserFaceDto();
faceDto.setUserIds(userIds);
List<SysUserFaceVo> faceList = sysUserFaceMapper.groupConcatFaces(faceDto);
DataUtil.convertFacesList(list.getRecords(), CollectionUtils.convertMap(faceList, SysUserFaceVo::getUserId));
}
@@ -18,6 +18,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
class FileAccessMobileTest {
static final String MOBILE = "https://www.pjzhhq.cn/mobile/";
static final String GOVERNMENT_ORIGIN = "https://yyfb.idw.jinhua.gov.cn:6443";
static final String GOVERNMENT_PATH = "/web/mgop/gov-open/zj/2002546649/";
FileAccessProperties settings() {
var p = properties("enforce");
p.getClients().remove("h5");
@@ -58,6 +60,82 @@ class FileAccessMobileTest {
var p = settings(); p.setPublicImageRefererPrefixes(List.of());
assertEquals(403, new FileAccessGuard(p, null, CLOCK).evaluate(image("GET", "a.png", MOBILE)).status());
}
@Test void rootOriginsAllowCrossOriginBrowserReferersWithoutWideningDirectoryPrefixes() {
for (String configured : List.of(GOVERNMENT_ORIGIN, GOVERNMENT_ORIGIN + "/")) {
var p = settings(); p.setPublicImageRefererPrefixes(List.of(MOBILE, configured));
assertDoesNotThrow(p::validate);
var guard = new FileAccessGuard(p, null, CLOCK);
for (String method : List.of("GET", "HEAD")) {
for (String referer : List.of(GOVERNMENT_ORIGIN, GOVERNMENT_ORIGIN + "/",
GOVERNMENT_ORIGIN + GOVERNMENT_PATH + "reserved/index.html", GOVERNMENT_ORIGIN + "/another-app/",
"https://YYFB.IDW.JINHUA.GOV.CN:6443/")) {
var request = image(method, "a.png", referer);
request.addHeader("Sec-Fetch-Mode", "no-cors"); request.addHeader("Sec-Fetch-Dest", "image");
assertEquals("public_image_referer", guard.evaluate(request).reason(), referer);
}
}
assertEquals(200, guard.evaluate(image("GET", "a.png", MOBILE)).status());
assertEquals(403, guard.evaluate(image("GET", "a.png", "https://www.pjzhhq.cn/")).status());
p.setPublicImageRefererPrefixes(List.of(GOVERNMENT_ORIGIN + GOVERNMENT_PATH));
assertEquals(403, guard.evaluate(image("GET", "a.png", GOVERNMENT_ORIGIN + "/")).status());
assertEquals(200, guard.evaluate(image("GET", "a.png", GOVERNMENT_ORIGIN + GOVERNMENT_PATH + "reserved/index.html")).status());
p.setPublicImageRefererPrefixes(List.of());
assertEquals(403, guard.evaluate(image("GET", "a.png", GOVERNMENT_ORIGIN + "/")).status());
}
var p = settings(); p.setPublicImageRefererPrefixes(List.of("https://yyfb.idw.jinhua.gov.cn/"));
var guard = new FileAccessGuard(p, null, CLOCK);
assertEquals(200, guard.evaluate(image("GET", "a.png", "https://yyfb.idw.jinhua.gov.cn:443/")).status());
assertEquals(403, guard.evaluate(image("GET", "a.png", GOVERNMENT_ORIGIN + "/")).status());
}
@Test void rootOriginsStillRequireASingleRefererWithExactSchemeHostAndPort() {
var p = settings(); p.setPublicImageRefererPrefixes(List.of(GOVERNMENT_ORIGIN + "/"));
var guard = new FileAccessGuard(p, null, CLOCK);
for (String referer : List.of("https://yyfb.idw.jinhua.gov.cn/", "https://yyfb.idw.jinhua.gov.cn:443/",
"https://yyfb.idw.jinhua.gov.cn:6444/", "http://yyfb.idw.jinhua.gov.cn:6443/",
"https://yyfb.idw.jinhua.gov.cn.evil.example:6443/", "https://other.idw.jinhua.gov.cn:6443/",
"https://yyfb.idw.jinhua.gov.cn:6443@evil.example/", "https://user@yyfb.idw.jinhua.gov.cn:6443/",
"https://evil.example/?next=" + GOVERNMENT_ORIGIN, GOVERNMENT_ORIGIN + "/#/",
GOVERNMENT_ORIGIN + "/%5c", GOVERNMENT_ORIGIN + "/, " + GOVERNMENT_ORIGIN + "/", "not a URL")) {
assertEquals(403, guard.evaluate(image("GET", "a.png", referer)).status(), referer);
}
var request = image("GET", "a.png", null); request.addHeader("Origin", GOVERNMENT_ORIGIN);
assertEquals(403, guard.evaluate(request).status());
request.addHeader("Referer", GOVERNMENT_ORIGIN + "/"); request.addHeader("Referer", GOVERNMENT_ORIGIN + "/");
assertEquals(403, guard.evaluate(request).status());
}
@Test void rootOriginExceptionOnlyReachesStorageForGetAndHeadImages() throws Exception {
var p = settings(); p.setPublicImageRefererPrefixes(List.of(GOVERNMENT_ORIGIN + "/"));
var service = mock(SysFileService.class); var guard = new FileAccessGuard(p, null, CLOCK);
var mvc = MockMvcBuilders.standaloneSetup(new SysFileController(service, guard, p, mock(FileUploadGuard.class))).build();
for (String file : List.of("a.pdf", "a.apk", "a.log", "a.zip", "a.mp4")) {
mvc.perform(get("/sys-file/oss/file").param("fileName", file).header("Referer", GOVERNMENT_ORIGIN + "/"))
.andExpect(status().isForbidden()).andExpect(header().string("Cache-Control", "no-store"));
}
for (String destination : List.of("document", "iframe", "frame")) {
mvc.perform(get("/sys-file/oss/file").param("fileName", "a.png").header("Referer", GOVERNMENT_ORIGIN + "/")
.header("Sec-Fetch-Dest", destination)).andExpect(status().isForbidden());
}
mvc.perform(get("/sys-file/oss/file").param("fileName", "a.png").header("Referer", GOVERNMENT_ORIGIN + "/")
.header("Sec-Fetch-Mode", "navigate")).andExpect(status().isForbidden());
for (String field : List.of("fileName", "appId", "kid", "ts", "sign")) {
var request = image("GET", "a.png", GOVERNMENT_ORIGIN + "/"); request.setParameter(field, "a.png", "b.png");
assertEquals(400, guard.evaluate(request).status(), field);
}
assertEquals(400, guard.evaluate(image("GET", "a.png\n", GOVERNMENT_ORIGIN + "/")).status());
for (String method : List.of("POST", "PUT", "DELETE")) {
assertEquals(403, guard.evaluate(image(method, "a.png", GOVERNMENT_ORIGIN + "/")).status());
}
var upload = new FileUploadGuard(p, (a, k, n, ttl) -> true, null, CLOCK);
assertEquals(403, upload.evaluate(image("POST", "a.png", GOVERNMENT_ORIGIN + "/"), "upload").status());
verifyZeroInteractions(service);
for (var request : List.of(get("/sys-file/oss/file"), head("/sys-file/oss/file"))) {
mvc.perform(request.param("fileName", "a.png").header("Referer", GOVERNMENT_ORIGIN + "/")
.header("Sec-Fetch-Mode", "no-cors").header("Sec-Fetch-Dest", "image"))
.andExpect(status().isOk()).andExpect(header().string("Cache-Control", "private, max-age=300"))
.andExpect(header().string("Vary", "User-Agent, Referer, Sec-Fetch-Mode, Sec-Fetch-Dest"));
}
verify(service, times(2)).getFile(eq("a.png"), any());
}
@Test void navigationDocumentsMalformedRequestsAndUploadsKeepTheirExistingRestrictions() {
var p = settings(); var guard = new FileAccessGuard(p, null, CLOCK);
for (String file : List.of("a.pdf", "a.apk", "a.log", "a.zip", "a.mp4")) {
@@ -86,7 +164,9 @@ class FileAccessMobileTest {
assertThrows(IllegalStateException.class, incomplete::validate);
var raw = image("GET", "a.png", null); raw.setParameter("appId", "h5");
assertEquals(400, new FileAccessGuard(settings(), null, CLOCK).evaluate(raw).status());
for (String prefix : List.of("https://www.pjzhhq.cn/", MOBILE + "#/", MOBILE + "?x=1", "https://user@www.pjzhhq.cn/mobile/", "https://www.pjzhhq.cn/mobile", "https://www.pjzhhq.cn/a/../mobile/")) {
for (String prefix : List.of("", "//www.pjzhhq.cn/", "ftp://www.pjzhhq.cn/", MOBILE + "#/", MOBILE + "?x=1",
GOVERNMENT_ORIGIN + "/#/", GOVERNMENT_ORIGIN + "?x=1", "https://user@yyfb.idw.jinhua.gov.cn:6443/",
"https://user@www.pjzhhq.cn/mobile/", "https://www.pjzhhq.cn/mobile", "https://www.pjzhhq.cn/a/../mobile/", "https://www.pjzhhq.cn/%2f/")) {
var p = settings(); p.setPublicImageRefererPrefixes(List.of(prefix)); assertThrows(IllegalStateException.class, p::validate);
}
var p = settings(); p.setPublicImageRefererPrefixes(List.of(MOBILE + "#/"));
@@ -0,0 +1,142 @@
package com.bsd.pjcloud.admin.service.impl;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.admin.api.dto.SysUserFaceDto;
import com.bsd.pjcloud.admin.api.dto.UserDTO;
import com.bsd.pjcloud.admin.api.vo.SysUserFaceVo;
import com.bsd.pjcloud.admin.api.vo.UserVO;
import com.bsd.pjcloud.admin.mapper.SysUserFaceMapper;
import com.bsd.pjcloud.admin.mapper.SysUserMapper;
import com.bsd.pjcloud.admin.service.SysFileService;
import com.bsd.pjcloud.admin.utils.flow.FlowUtil;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.sql.PreparedStatement;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/** 分页只补全当前页的人脸引用,避免每一页重新聚合整个人脸表。 */
class SysUserPageFaceQueryTest {
private final SysUserMapper users = mock(SysUserMapper.class);
private final SysUserFaceMapper faces = mock(SysUserFaceMapper.class);
private final SysFileService files = mock(SysFileService.class);
private final FlowUtil flow = mock(FlowUtil.class);
private SysUserServiceImpl service;
enum Query {
ADMIN, INTERNAL;
IPage<UserVO> execute(SysUserServiceImpl service, Page<UserVO> page, UserDTO query) {
return this == ADMIN ? service.getUsersWithRolePage(page, query) : service.innerPage(page, query);
}
}
@BeforeEach
void setup() {
service = new SysUserServiceImpl(null, null, null, null, null, null, faces,
null, null, null, null, null, files, flow);
ReflectionTestUtils.setField(service, "baseMapper", users);
}
@ParameterizedTest
@EnumSource(Query.class)
void faceLookupUsesOnlyDistinctCurrentPageIdsAndKeepsRawImages(Query method) throws Exception {
var page = new Page<UserVO>(2, 5);
var query = queryWithoutApproval();
var rows = List.of(user(101L), user(202L), user(101L), user(null), user(303L));
var result = new Page<UserVO>(2, 5).setRecords(rows).setTotal(120);
when(users.getUserVosPage(same(page), same(query), nullable(DataScope.class))).thenReturn(result);
String firstImages = "/admin/sys-file/oss/file?fileName=first.jpg,/admin/sys-file/oss/file?fileName=second.png";
String secondImages = "https://files.example/api/admin/sys-file/oss/file?fileName=third.jpg";
when(faces.groupConcatFaces(any())).thenReturn(List.of(face(202L, secondImages), face(101L, firstImages)));
IPage<UserVO> actual = method.execute(service, page, query);
var captured = ArgumentCaptor.forClass(SysUserFaceDto.class);
verify(faces).groupConcatFaces(captured.capture());
assertEquals(List.of(101L, 202L, 303L), captured.getValue().getUserIds());
assertSame(result, actual);
assertSame(rows, actual.getRecords());
assertEquals(120, actual.getTotal());
assertEquals(firstImages, rows.get(0).getFaceImgs());
assertEquals(secondImages, rows.get(1).getFaceImgs());
assertEquals(firstImages, rows.get(2).getFaceImgs());
assertNull(rows.get(3).getFaceImgs());
assertNull(rows.get(4).getFaceImgs());
verifyZeroInteractions(files, flow);
// Parse the production XML and bind the captured DTO, proving the filter reaches SQL.
var configuration = new MybatisConfiguration();
String resource = "mapper/SysUserFaceMapper.xml";
try (var input = getClass().getClassLoader().getResourceAsStream(resource)) {
assertNotNull(input);
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
}
var statement = configuration.getMappedStatement("com.bsd.pjcloud.admin.mapper.SysUserFaceMapper.groupConcatFaces");
Map<String, Object> parameters = Map.of("pa", captured.getValue());
var bound = statement.getBoundSql(parameters);
String sql = bound.getSql().replaceAll("\\s+", " ");
assertTrue(sql.matches("(?s).*user_id\\s+in\\s*\\(\\s*\\?\\s*,\\s*\\?\\s*,\\s*\\?\\s*\\).*"), sql);
assertEquals(3, bound.getParameterMappings().size());
var prepared = mock(PreparedStatement.class);
configuration.newParameterHandler(statement, parameters, bound).setParameters(prepared);
verify(prepared).setLong(1, 101L);
verify(prepared).setLong(2, 202L);
verify(prepared).setLong(3, 303L);
verifyNoMoreInteractions(prepared);
}
@ParameterizedTest
@EnumSource(Query.class)
void emptyPageNeverQueriesFaces(Query method) {
assertNoFaceLookup(method, List.of());
}
@ParameterizedTest
@EnumSource(Query.class)
void pageWithoutAnyValidUserIdNeverFallsBackToUnfilteredFaceQuery(Query method) {
assertNoFaceLookup(method, List.of(user(null), user(null)));
}
private void assertNoFaceLookup(Query method, List<UserVO> rows) {
var page = new Page<UserVO>(1, 10);
var query = queryWithoutApproval();
var result = new Page<UserVO>(1, 10).setRecords(rows);
when(users.getUserVosPage(same(page), same(query), nullable(DataScope.class))).thenReturn(result);
assertSame(result, method.execute(service, page, query));
verifyZeroInteractions(faces, files, flow);
}
private UserDTO queryWithoutApproval() {
var query = new UserDTO();
query.setIsFindCheckUser(false);
return query;
}
private UserVO user(Long id) {
var user = new UserVO();
user.setUserId(id);
return user;
}
private SysUserFaceVo face(Long id, String images) {
var face = new SysUserFaceVo();
face.setUserId(id);
face.setFaceImgs(images);
return face;
}
}