Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47cccc9ca5 | ||
|
|
5c97c57c76 | ||
|
|
8335ef780c |
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org/>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.danyang.match.platform.auth.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.danyang.match.platform.auth.provider.WechatLoginProvider;
|
||||
import com.danyang.match.platform.auth.token.JwtTokenService;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.user.entity.User;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 回归守卫:刷新 Token 在 Redis 中登记的 TTL 必须与 refresh JWT 自身有效期一致
|
||||
* ({@code JwtTokenService#getRefreshExpirationMs},默认 14 天),而不是
|
||||
* {@code getExpirationMs() * 2}(4 小时)。
|
||||
*
|
||||
* <p>此前 AuthService 用 {@code getExpirationMs() * 2} 写入,导致 4h 后 Redis 记录消失、
|
||||
* 刷新即失效,用户被迫频繁重新登录。本测试钉死两处写入点(登录 / 刷新)。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthServiceRefreshTtlTest {
|
||||
|
||||
@Mock private UserMapper userMapper;
|
||||
@Mock private WechatLoginProvider wechatLoginProvider;
|
||||
@Mock private JwtTokenService jwtTokenService;
|
||||
@Mock private StringRedisTemplate redisTemplate;
|
||||
@Mock private com.danyang.match.platform.audit.mapper.AuditLogMapper auditLogMapper;
|
||||
@Mock private com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityMapper localEligibilityMapper;
|
||||
@Mock private com.danyang.match.platform.capability.service.CapabilityService capabilityService;
|
||||
@Mock private com.danyang.match.platform.profile.mapper.UserProfileMapper userProfileMapper;
|
||||
@Mock private com.danyang.match.platform.review.mapper.ReviewTaskMapper reviewTaskMapper;
|
||||
@Mock private ValueOperations<String, String> valueOps;
|
||||
|
||||
@InjectMocks private AuthService authService;
|
||||
|
||||
private static final long ACCESS_MS = 7_200_000L; // 2h
|
||||
private static final long REFRESH_MS = 1_209_600_000L; // 14d
|
||||
|
||||
/** 捕获 refresh token 在 Redis 中的登记 TTL。 */
|
||||
private Duration captureRefreshTtl() {
|
||||
ArgumentCaptor<Duration> cap = ArgumentCaptor.forClass(Duration.class);
|
||||
verify(valueOps).set(eq("auth:refresh_token:u1"), anyString(), cap.capture());
|
||||
return cap.getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void wechatLoginStoresRefreshTokenWithRefreshExpiration() {
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOps);
|
||||
when(wechatLoginProvider.getCodeSession("code")).thenReturn(Map.of("openid", "o1"));
|
||||
User user = new User();
|
||||
user.setId("u1");
|
||||
user.setAccountStatus(Constants.ACCOUNT_STATUS_NORMAL);
|
||||
user.setPhone(null);
|
||||
when(userMapper.findByWechatOpenid("o1")).thenReturn(Optional.of(user));
|
||||
when(jwtTokenService.generateAccessToken("u1", null)).thenReturn("access");
|
||||
when(jwtTokenService.generateRefreshToken("u1")).thenReturn("refresh");
|
||||
when(jwtTokenService.getExpirationMs()).thenReturn(ACCESS_MS);
|
||||
when(jwtTokenService.getRefreshExpirationMs()).thenReturn(REFRESH_MS);
|
||||
when(auditLogMapper.insert(any())).thenReturn(1);
|
||||
when(userMapper.updateById(any())).thenReturn(1);
|
||||
|
||||
authService.wechatLogin("code");
|
||||
|
||||
Duration ttl = captureRefreshTtl();
|
||||
assertEquals(Duration.ofMillis(REFRESH_MS), ttl, "登录:刷新 Token TTL 必须等于 refresh 有效期(14 天)");
|
||||
assertNotEquals(Duration.ofMillis(ACCESS_MS * 2), ttl, "回归:不得再写成 access*2(4h)导致频繁登出");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshTokenStoresRefreshTokenWithRefreshExpiration() {
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOps);
|
||||
when(jwtTokenService.validateRefreshToken("rt")).thenReturn(true);
|
||||
when(jwtTokenService.getUserIdFromRefreshToken("rt")).thenReturn("u1");
|
||||
when(redisTemplate.opsForValue().get("auth:refresh_token:u1")).thenReturn("rt");
|
||||
when(jwtTokenService.generateAccessToken("u1", null)).thenReturn("newAccess");
|
||||
when(jwtTokenService.generateRefreshToken("u1")).thenReturn("newRefresh");
|
||||
when(jwtTokenService.getExpirationMs()).thenReturn(ACCESS_MS);
|
||||
when(jwtTokenService.getRefreshExpirationMs()).thenReturn(REFRESH_MS);
|
||||
|
||||
authService.refreshToken("u1", "rt");
|
||||
|
||||
Duration ttl = captureRefreshTtl();
|
||||
assertEquals(Duration.ofMillis(REFRESH_MS), ttl, "刷新:刷新 Token TTL 必须等于 refresh 有效期(14 天)");
|
||||
assertNotEquals(Duration.ofMillis(ACCESS_MS * 2), ttl, "回归:不得再写成 access*2(4h)导致频繁登出");
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.danyang.match.platform.photo;
|
||||
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
import com.danyang.match.platform.photo.entity.UserPhoto;
|
||||
import com.danyang.match.platform.photo.mapper.UserPhotoMapper;
|
||||
import com.danyang.match.platform.photo.service.PhotoService;
|
||||
import com.danyang.match.platform.shared.config.FeatureFlagConfig;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException;
|
||||
import com.danyang.match.platform.shared.policy.ProfilePolicy;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 回归守卫:照片直传落库(此前「传了白传」——upload-token 之后没有任何端点写 user_photos)。
|
||||
* <ul>
|
||||
* <li>非法的 fileKey(不归属本人)必须被归属护栏拒绝;</li>
|
||||
* <li>对象尚未上传完成(存储不存在)必须被存在性护栏拒绝;</li>
|
||||
* <li>合法且已上传的直传必须真正落库(写入 user_photos 记录)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
class PhotoServiceConfirmUploadTest {
|
||||
|
||||
private final UserPhotoMapper photoMapper = mock(UserPhotoMapper.class);
|
||||
private final AmazonS3 amazonS3 = mock(AmazonS3.class);
|
||||
private final FeatureFlagConfig featureFlags = mock(FeatureFlagConfig.class);
|
||||
private final ProfilePolicy profilePolicy = mock(ProfilePolicy.class);
|
||||
private final UserMapper userMapper = mock(UserMapper.class);
|
||||
private final AmazonS3 amazonS3Signer = mock(AmazonS3.class);
|
||||
|
||||
private final PhotoService photoService =
|
||||
new PhotoService(photoMapper, amazonS3, featureFlags, profilePolicy, userMapper, amazonS3Signer);
|
||||
|
||||
@Test
|
||||
void illegalFileKeyRejected() {
|
||||
BusinessException ex = assertThrows(BusinessException.class,
|
||||
() -> photoService.confirmDirectUpload("u1", "evil/key.jpg", "avatar"));
|
||||
assertEquals("非法的文件标识", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingObjectRejected() {
|
||||
when(amazonS3.doesObjectExist(any(), eq("photos/u1/x.jpg"))).thenReturn(false);
|
||||
BusinessException ex = assertThrows(BusinessException.class,
|
||||
() -> photoService.confirmDirectUpload("u1", "photos/u1/x.jpg", "lifestyle"));
|
||||
assertEquals("文件尚未上传完成", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validUploadPersistsPhoto() {
|
||||
when(amazonS3.doesObjectExist(any(), eq("photos/u1/p.jpg"))).thenReturn(true);
|
||||
when(photoMapper.countActiveProfilePhotos("u1")).thenReturn(0L);
|
||||
when(profilePolicy.getMaxPhotoCount()).thenReturn(6);
|
||||
when(photoMapper.insert(any())).thenReturn(1);
|
||||
|
||||
UserPhoto photo = photoService.confirmDirectUpload("u1", "photos/u1/p.jpg", "lifestyle");
|
||||
|
||||
assertNotNull(photo, "直传必须真实落库,不得白传");
|
||||
assertEquals("u1", photo.getUserId());
|
||||
assertEquals("photos/u1/p.jpg", photo.getFileKey());
|
||||
assertEquals("lifestyle", photo.getPhotoType());
|
||||
assertEquals(Constants.REVIEW_STATUS_PENDING, photo.getReviewStatus());
|
||||
verify(photoMapper).insert(any(UserPhoto.class));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
@@ -98,4 +99,78 @@ class JwtAuthFilterTest {
|
||||
request.addHeader("Authorization", "Bearer " + token);
|
||||
return request;
|
||||
}
|
||||
|
||||
// ===== 本轮修复回归:管理端 HttpOnly Cookie(dy_op_at)兜底鉴权 =====
|
||||
|
||||
@Test
|
||||
void cookieFallbackAuthenticatesOperatorWhenNoHeader() throws Exception {
|
||||
String token = "cookie-op-token";
|
||||
when(jwtTokenService.validateToken(token)).thenReturn(true);
|
||||
when(jwtTokenService.getUserIdFromToken(token)).thenReturn("operator-1");
|
||||
when(jwtTokenService.parseToken(token)).thenReturn(claims);
|
||||
when(claims.get("type", String.class)).thenReturn("operator_access");
|
||||
when(permissionService.getPermissions("operator-1")).thenReturn(List.of("content.edit"));
|
||||
|
||||
filter.doFilter(cookieRequest(token), new MockHttpServletResponse(), new MockFilterChain());
|
||||
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertNotNull(authentication);
|
||||
assertInstanceOf(OperatorPrincipal.class, authentication.getPrincipal());
|
||||
assertEquals("operator-1", ((OperatorPrincipal) authentication.getPrincipal()).id());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieFallbackAuthenticatesUserWhenNoHeader() throws Exception {
|
||||
String token = "cookie-user-token";
|
||||
when(jwtTokenService.validateToken(token)).thenReturn(true);
|
||||
when(jwtTokenService.getUserIdFromToken(token)).thenReturn("user-1");
|
||||
when(jwtTokenService.parseToken(token)).thenReturn(claims);
|
||||
when(claims.get("type", String.class)).thenReturn("access");
|
||||
|
||||
filter.doFilter(cookieRequest(token), new MockHttpServletResponse(), new MockFilterChain());
|
||||
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertNotNull(authentication);
|
||||
assertInstanceOf(UserPrincipal.class, authentication.getPrincipal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieWithInvalidTokenReturns401() throws Exception {
|
||||
String token = "bad-cookie-token";
|
||||
when(jwtTokenService.validateToken(token)).thenReturn(false);
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
filter.doFilter(cookieRequest(token), response, new MockFilterChain());
|
||||
|
||||
assertEquals(401, response.getStatus());
|
||||
assertNull(SecurityContextHolder.getContext().getAuthentication());
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerTakesPrecedenceOverCookie() throws Exception {
|
||||
// Authorization 头里是有效 user token;Cookie 里是无效 token,应被忽略(Cookie 仅作兜底)
|
||||
String headerToken = "header-token";
|
||||
when(jwtTokenService.validateToken(headerToken)).thenReturn(true);
|
||||
when(jwtTokenService.getUserIdFromToken(headerToken)).thenReturn("user-1");
|
||||
when(jwtTokenService.parseToken(headerToken)).thenReturn(claims);
|
||||
when(claims.get("type", String.class)).thenReturn("access");
|
||||
when(jwtTokenService.validateToken("bad-cookie")).thenReturn(false);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/content/operator/articles");
|
||||
request.addHeader("Authorization", "Bearer " + headerToken);
|
||||
request.setCookies(new Cookie("dy_op_at", "bad-cookie"));
|
||||
|
||||
filter.doFilter(request, new MockHttpServletResponse(), new MockFilterChain());
|
||||
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
assertNotNull(authentication);
|
||||
assertInstanceOf(UserPrincipal.class, authentication.getPrincipal());
|
||||
assertEquals("user-1", ((UserPrincipal) authentication.getPrincipal()).id());
|
||||
}
|
||||
|
||||
private MockHttpServletRequest cookieRequest(String token) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/content/operator/articles");
|
||||
request.setCookies(new Cookie("dy_op_at", token));
|
||||
return request;
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.danyang.match.platform.verification.provider;
|
||||
|
||||
import com.danyang.match.platform.shared.enums.ApiResponseCode;
|
||||
import com.danyang.match.platform.shared.exception.GlobalExceptionHandler.BusinessException;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* RealIdentityVerificationProvider 回归守卫:
|
||||
* <ul>
|
||||
* <li>伪造身份证号(校验位不符)必须在发起任何外部调用前被本地强校验拦截;</li>
|
||||
* <li>配置缺失(apiUrl / apiKey 为空)必须 fail-fast,绝不静默放行任意证件;</li>
|
||||
* <li>第三方核验 fail-closed:响应未明确「通过」一律视为未通过。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用 JDK 内置 {@link HttpServer} 模拟第三方核验端点,无需任何额外依赖。
|
||||
*/
|
||||
class RealIdentityVerificationProviderTest {
|
||||
|
||||
// 按 GB 11643-1999 手工计算得到的合法校验位身份证(前 17 位 + 校验位 '8')
|
||||
private static final String VALID_ID = "110101199003073458";
|
||||
// 校验位应为 '8',此处写成 'X' → 非法
|
||||
private static final String INVALID_ID = "11010119900307345X";
|
||||
|
||||
private HttpServer server;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (server != null) {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private RealIdentityVerificationProvider newProvider(String apiUrl, String apiKey) throws Exception {
|
||||
RealIdentityVerificationProvider p = new RealIdentityVerificationProvider();
|
||||
setField(p, "apiUrl", apiUrl);
|
||||
setField(p, "apiKey", apiKey);
|
||||
return p;
|
||||
}
|
||||
|
||||
private void setField(Object target, String name, String value) throws Exception {
|
||||
Field f = RealIdentityVerificationProvider.class.getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
f.set(target, value);
|
||||
}
|
||||
|
||||
/** 启动本地模拟端点,返回其 URL。 */
|
||||
private String startServer(int statusCode, String body) throws Exception {
|
||||
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||
server.createContext("/verify", exchange -> {
|
||||
byte[] resp = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(statusCode, resp.length);
|
||||
exchange.getResponseBody().write(resp);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
return "http://localhost:" + server.getAddress().getPort() + "/verify";
|
||||
}
|
||||
|
||||
@Test
|
||||
void forgedIdRejectedBeforeAnyNetworkCall() throws Exception {
|
||||
// apiUrl / apiKey 仅作占位(非空,避免误触 fail-fast 分支);
|
||||
// 校验位不符应直接判未通过,且不应发起任何外部请求
|
||||
RealIdentityVerificationProvider provider = newProvider("http://localhost:1/verify", "k");
|
||||
Map<String, Object> result = provider.verify("张三", INVALID_ID);
|
||||
assertFalse((Boolean) result.get("verified"), "校验位不符的伪造证件号必须被本地强校验判定为未通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingConfigFailsFast() throws Exception {
|
||||
RealIdentityVerificationProvider provider = newProvider("", "");
|
||||
BusinessException ex = assertThrows(BusinessException.class,
|
||||
() -> provider.verify("张三", VALID_ID));
|
||||
assertEquals(ApiResponseCode.SERVER_ERROR, ex.getCode(),
|
||||
"缺第三方配置时必须 fail-fast 而非放行任意证件");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validIdWithThirdPartySuccessVerified() throws Exception {
|
||||
String url = startServer(200, "{\"success\":true}");
|
||||
RealIdentityVerificationProvider provider = newProvider(url, "k");
|
||||
Map<String, Object> result = provider.verify("张三", VALID_ID);
|
||||
assertTrue((Boolean) result.get("verified"), "校验位合法 + 第三方明确通过 = 通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validIdWithThirdPartyFailureNotVerified() throws Exception {
|
||||
String url = startServer(200, "{\"success\":false}");
|
||||
RealIdentityVerificationProvider provider = newProvider(url, "k");
|
||||
Map<String, Object> result = provider.verify("张三", VALID_ID);
|
||||
assertFalse((Boolean) result.get("verified"), "第三方未明确「通过」 = 未通过(fail-closed)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validIdWithThirdPartyErrorNotVerified() throws Exception {
|
||||
String url = startServer(500, "{\"message\":\"internal\"}");
|
||||
RealIdentityVerificationProvider provider = newProvider(url, "k");
|
||||
Map<String, Object> result = provider.verify("张三", VALID_ID);
|
||||
assertFalse((Boolean) result.get("verified"), "第三方非 2xx 响应 = 未通过(fail-closed)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 纯函数回归测试:枚举码 → 中文映射(修复「枚举码直渲未映射中文」)
|
||||
// 运行:node ui-tests/profileLabels.test.mjs
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const { serviceTypeLabel, labelOf } = require('../miniapp/constants/profileLabels.js')
|
||||
|
||||
let passed = 0
|
||||
function check(name, actual, expected) {
|
||||
assert.equal(actual, expected, `${name}: 期望 "${expected}",实际 "${actual}"`)
|
||||
passed++
|
||||
}
|
||||
|
||||
// ===== 红娘服务类型映射(本次修复点:matchmaker-workbench 卡片曾直渲枚举码)=====
|
||||
check('recommend_partner', serviceTypeLabel('recommend_partner'), '推荐对象')
|
||||
check('review_preference', serviceTypeLabel('review_preference'), '择偶指导')
|
||||
check('contact_intention', serviceTypeLabel('contact_intention'), '牵线意向')
|
||||
check('未知码回退原值', serviceTypeLabel('mystery_code'), 'mystery_code')
|
||||
check('空串返回空', serviceTypeLabel(''), '')
|
||||
check('null 返回空', serviceTypeLabel(null), '')
|
||||
|
||||
// ===== 通用 labelOf(既有字典,回归不退化)=====
|
||||
check('labelOf education', labelOf('educationCode', 'bachelor'), '本科')
|
||||
check('labelOf marital', labelOf('maritalStatus', 'never_married'), '未婚')
|
||||
check('labelOf 未知码回退', labelOf('maritalStatus', 'xxx'), 'xxx')
|
||||
|
||||
console.log(`✅ profileLabels 回归测试全部通过(${passed} 项)`)
|
||||
Reference in New Issue
Block a user