mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
feat(pigx-upms-biz): 增加飞书用户身份绑定功能
- 在 SysUserMapper.xml 中添加对飞书用户的查询支持 - 新增 FeishuLoginHandlerTests 测试类,验证飞书登录处理逻辑 - 测试覆盖飞书身份的代码交换和用户信息查询功能 (cherry picked from commit 2a0796cce45efd4e0444b0e2a6ef0a9e530dd226)
This commit is contained in:
1 parent
75ec99389c
commit
1dc58c60cf
10 files changed
+324
-70
No files matched your search
@@ -339,6 +339,7 @@ INSERT INTO `sys_dict_item` VALUES (95, 29, '0', '黑名单', 'sensitive_type',
|
||||
INSERT INTO `sys_dict_item` VALUES (96, 29, '1', '白名单', 'sensitive_type', '失败', NULL, 3, 'admin', ' ', '2023-10-27 10:30:47', NULL, NULL, '0', 1);
|
||||
INSERT INTO `sys_dict_item` VALUES (97,2,'MINI','小程序登录','social_type','小程序登录',NULL,3,' ',' ','2022-02-18 13:56:25','2022-02-18 13:56:28',NULL,'0',1);
|
||||
INSERT INTO `sys_dict_item` VALUES (98,2,'clarity','clarity','social_type','clarity',NULL,3,' ',' ','2022-02-18 13:56:25','2022-02-18 13:56:28',NULL,'0',1);
|
||||
INSERT INTO `sys_dict_item` VALUES (100,2,'FEISHU','企业飞书','social_type','企业飞书扫码登录',NULL,5,' ',' ','2026-09-18 00:00:00','2026-09-18 00:00:00',NULL,'0',1);
|
||||
INSERT INTO `sys_dict_item` VALUES (99,2,'umami','umami','social_type','umami',NULL,4,' ',' ','2026-06-23 00:00:00','2026-06-23 00:00:00',NULL,'0',1);
|
||||
COMMIT;
|
||||
|
||||
|
||||
@@ -320,6 +320,7 @@ INSERT INTO `sys_dict_item` VALUES (95, 29, '0', '黑名单', 'sensitive_type',
|
||||
INSERT INTO `sys_dict_item` VALUES (96, 29, '1', '白名单', 'sensitive_type', '失败', NULL, 3, 'admin', ' ', '2023-10-27 10:30:47', NULL, NULL, '0', 1);
|
||||
INSERT INTO `sys_dict_item` VALUES (97,2,'MINI','小程序登录','social_type','小程序登录',NULL,3,' ',' ','2022-02-18 13:56:25','2022-02-18 13:56:28',NULL,'0',1);
|
||||
INSERT INTO `sys_dict_item` VALUES (98,2,'clarity','clarity','social_type','clarity',NULL,3,' ',' ','2022-02-18 13:56:25','2022-02-18 13:56:28',NULL,'0',1);
|
||||
INSERT INTO `sys_dict_item` VALUES (100,2,'FEISHU','企业飞书','social_type','企业飞书扫码登录',NULL,5,' ',' ','2026-09-18 00:00:00','2026-09-18 00:00:00',NULL,'0',1);
|
||||
INSERT INTO `sys_dict_item` VALUES (99,2,'umami','umami','social_type','umami',NULL,4,' ',' ','2026-06-23 00:00:00','2026-06-23 00:00:00',NULL,'0',1);
|
||||
COMMIT;
|
||||
|
||||
|
||||
+3
@@ -60,6 +60,9 @@ public enum LoginTypeEnum {
|
||||
*/
|
||||
DINGTALK("DINGTALK", "钉钉"),
|
||||
|
||||
/** 企业飞书 */
|
||||
FEISHU("FEISHU", "企业飞书"),
|
||||
|
||||
/**
|
||||
* 企业微信
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 企业飞书扫码登录
|
||||
|
||||
使用企业自建飞书应用,通过官方授权页面扫码,UPMS 使用 OAuth v3 换取用户凭证,再用 `open_id` 查询 `sys_user_social` 中已绑定的系统账号。
|
||||
|
||||
## 配置
|
||||
|
||||
1. 存量系统在 UPMS 数据库执行 `db/upgrade_feishu_login.sql`,刷新 `social_type` 字典缓存。全新系统的 `0pigxx_boot.sql`、`2pigxx.sql` 已包含 `FEISHU` 字典项。
|
||||
2. 在飞书开发者后台创建并启用企业自建应用,发布版本,确保使用者位于应用可用范围。
|
||||
3. 在飞书应用安全设置中登记回调地址,例如 `https://admin.example.com/#/authredirect`。
|
||||
4. 在管理端的第三方应用配置中选择「企业飞书」,填写 App ID、App Secret、同一个完整回调地址。回调地址必须与当前管理端同源,hash 为 `#/authredirect`,不携带查询参数;扩展字段留空。
|
||||
5. 用户先以现有方式登录系统,在个人中心绑定「企业飞书」,完成扫码授权。之后可在登录页选择「企业飞书」扫码登录,也可在个人中心解绑。
|
||||
|
||||
前后端必须使用完全相同的回调地址。飞书会把 `code` 和 `state` 放在 `#` 前面,现有回调组件同时支持 query 和 hash 参数。飞书授权状态有效期为五分钟且只能消费一次,拒绝授权或过期后需要重新发起。
|
||||
|
||||
未绑定身份不会自动注册,也不会按手机号或邮箱匹配系统账号。应用仅获取基础身份,不申请手机号、邮箱、通讯录或离线访问权限。App Secret 仅由后端使用,不出现在公开应用列表中。更换 App ID 后,原应用的 `open_id` 绑定不能直接复用。
|
||||
|
||||
## 验收
|
||||
|
||||
- 已绑定用户扫码后成功登录,权限与原系统账号一致。
|
||||
- 未绑定用户不能登录;个人中心绑定后显示已绑定,解绑后不能再用该身份登录。
|
||||
- 拒绝授权、无效或过期的 code/state 不得登录或写入绑定。
|
||||
- 本地自动化只验证协议和查询路径;真实扫码需要部署配置完成后的飞书企业应用与账号。
|
||||
|
||||
## 官方接口
|
||||
|
||||
- [获取授权码](https://open.feishu.cn/document/authentication-management/access-token/obtain-oauth-code)
|
||||
- [获取 user_access_token(v3)](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token-v3)
|
||||
- [获取用户信息](https://open.feishu.cn/document/server-docs/authentication-management/login-state-management/get)
|
||||
+57
-54
@@ -35,69 +35,72 @@ import java.util.List;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserDTO extends SysUser {
|
||||
|
||||
/**
|
||||
* 微信 openid
|
||||
*/
|
||||
@Schema(description = "微信openid")
|
||||
private String wxOpenid;
|
||||
/**
|
||||
* 微信 openid
|
||||
*/
|
||||
@Schema(description = "微信openid")
|
||||
private String wxOpenid;
|
||||
|
||||
/**
|
||||
* 企业微信 userid
|
||||
*/
|
||||
@Schema(description = "企业微信userid")
|
||||
private String wxCpUserid;
|
||||
/**
|
||||
* 企业微信 userid
|
||||
*/
|
||||
@Schema(description = "企业微信userid")
|
||||
private String wxCpUserid;
|
||||
|
||||
/**
|
||||
* 钉钉 openid
|
||||
*/
|
||||
@Schema(description = "钉钉openid")
|
||||
private String wxDingUserid;
|
||||
/**
|
||||
* 钉钉 openid
|
||||
*/
|
||||
@Schema(description = "钉钉openid")
|
||||
private String wxDingUserid;
|
||||
|
||||
/**
|
||||
* 微信小程序 openid
|
||||
*/
|
||||
@Schema(description = "微信小程序openid")
|
||||
private String miniOpenid;
|
||||
@Schema(description = "飞书open_id")
|
||||
private String feishuOpenid;
|
||||
|
||||
/**
|
||||
* QQ openid
|
||||
*/
|
||||
@Schema(description = "QQ openid")
|
||||
private String qqOpenid;
|
||||
/**
|
||||
* 微信小程序 openid
|
||||
*/
|
||||
@Schema(description = "微信小程序openid")
|
||||
private String miniOpenid;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
@Schema(description = "角色id集合")
|
||||
private List<Long> role;
|
||||
/**
|
||||
* QQ openid
|
||||
*/
|
||||
@Schema(description = "QQ openid")
|
||||
private String qqOpenid;
|
||||
|
||||
/**
|
||||
* 部门id
|
||||
*/
|
||||
@Schema(description = "部门id")
|
||||
private Long deptId;
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
@Schema(description = "角色id集合")
|
||||
private List<Long> role;
|
||||
|
||||
/**
|
||||
* 部门id集合(支持多部门)
|
||||
*/
|
||||
@Schema(description = "部门id集合")
|
||||
private List<Long> deptIds;
|
||||
/**
|
||||
* 部门id
|
||||
*/
|
||||
@Schema(description = "部门id")
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 岗位ID
|
||||
*/
|
||||
private List<Long> post;
|
||||
/**
|
||||
* 部门id集合(支持多部门)
|
||||
*/
|
||||
@Schema(description = "部门id集合")
|
||||
private List<Long> deptIds;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@Schema(description = "新密码")
|
||||
private String newpassword1;
|
||||
/**
|
||||
* 岗位ID
|
||||
*/
|
||||
private List<Long> post;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@Schema(description = "验证码")
|
||||
private String code;
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@Schema(description = "新密码")
|
||||
private String newpassword1;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@Schema(description = "验证码")
|
||||
private String code;
|
||||
|
||||
}
|
||||
+18
-15
@@ -12,22 +12,25 @@ import lombok.Data;
|
||||
@Data
|
||||
public class SysSocialDetailsVO {
|
||||
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
@Schema(description = "appId")
|
||||
private String appId;
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
@Schema(description = "appId")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@Schema(description = "账号类型")
|
||||
private String type;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@Schema(description = "账号类型")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 扩展字段
|
||||
*/
|
||||
@Schema(description = "扩展字段")
|
||||
private String ext;
|
||||
/**
|
||||
* 扩展字段
|
||||
*/
|
||||
@Schema(description = "扩展字段")
|
||||
private String ext;
|
||||
|
||||
@Schema(description = "授权回调地址")
|
||||
private String redirectUrl;
|
||||
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.pig4cloud.pigx.admin.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.pig4cloud.pigx.admin.api.dto.UserDTO;
|
||||
import com.pig4cloud.pigx.admin.api.dto.UserInfo;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysSocialDetails;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysUser;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysUserSocial;
|
||||
import com.pig4cloud.pigx.admin.mapper.SysSocialDetailsMapper;
|
||||
import com.pig4cloud.pigx.admin.mapper.SysUserSocialMapper;
|
||||
import com.pig4cloud.pigx.admin.service.SysUserService;
|
||||
import com.pig4cloud.pigx.common.core.constant.enums.LoginTypeEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 企业自建飞书应用登录处理器。使用 UPMS 配置交换授权码,以应用内 open_id 查询或绑定已有系统账号;不按手机号匹配,也不自动注册用户。
|
||||
*/
|
||||
@Component("FEISHU")
|
||||
@RequiredArgsConstructor
|
||||
public class FeishuLoginHandler extends AbstractLoginHandler {
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
private final SysSocialDetailsMapper sysSocialDetailsMapper;
|
||||
|
||||
private final SysUserSocialMapper sysUserSocialMapper;
|
||||
|
||||
/**
|
||||
* 使用配置中的应用凭证和回调地址交换授权码,再向飞书查询用户身份。
|
||||
* @param code 飞书回调的一次性授权码,不可为空
|
||||
* @return 飞书应用内非空的 open_id
|
||||
* @throws IllegalArgumentException 授权码为空
|
||||
* @throws IllegalStateException 应用配置不完整、飞书返回失败或缺少访问凭证、用户身份
|
||||
* @throws cn.hutool.core.io.IORuntimeException 网络请求失败或超时
|
||||
* @throws cn.hutool.json.JSONException 飞书响应无法解析为 JSON
|
||||
*/
|
||||
@Override
|
||||
public String identify(String code) {
|
||||
if (StrUtil.isBlank(code)) {
|
||||
throw new IllegalArgumentException("飞书授权码不能为空");
|
||||
}
|
||||
SysSocialDetails details = sysSocialDetailsMapper.selectOne(
|
||||
Wrappers.<SysSocialDetails>lambdaQuery().eq(SysSocialDetails::getType, LoginTypeEnum.FEISHU.getType()));
|
||||
if (details == null || StrUtil.hasBlank(details.getAppId(), details.getAppSecret(), details.getRedirectUrl())) {
|
||||
throw new IllegalStateException("请先配置企业飞书 App ID、App Secret 和回调地址");
|
||||
}
|
||||
|
||||
JSONObject token = request(HttpRequest.post("https://accounts.feishu.cn/oauth/v3/token")
|
||||
.form("grant_type", "authorization_code")
|
||||
.form("client_id", details.getAppId())
|
||||
.form("client_secret", details.getAppSecret())
|
||||
.form("code", code)
|
||||
.form("redirect_uri", details.getRedirectUrl()));
|
||||
String accessToken = token.getStr("access_token");
|
||||
if (StrUtil.isBlank(accessToken)) {
|
||||
throw new IllegalStateException("飞书未返回有效访问凭证,请重新扫码");
|
||||
}
|
||||
JSONObject user = request(HttpRequest.get("https://open.feishu.cn/open-apis/authen/v1/user_info")
|
||||
.header("Authorization", "Bearer " + accessToken)).getJSONObject("data");
|
||||
String openId = user == null ? null : user.getStr("open_id");
|
||||
if (StrUtil.isBlank(openId)) {
|
||||
throw new IllegalStateException("飞书未返回用户身份,请重新扫码");
|
||||
}
|
||||
return openId;
|
||||
}
|
||||
|
||||
// 统一限制请求时长并关闭响应;错误信息不包含访问凭证或完整用户信息。
|
||||
JSONObject request(HttpRequest request) {
|
||||
try (HttpResponse response = request.timeout(10000).execute()) {
|
||||
if (!response.isOk()) {
|
||||
throw new IllegalStateException("飞书授权请求失败,请检查应用配置或重新扫码");
|
||||
}
|
||||
JSONObject body = JSONUtil.parseObj(response.body());
|
||||
if (body.getInt("code", -1) != 0) {
|
||||
// 不记录完整响应,避免泄露访问凭证或用户信息。
|
||||
throw new IllegalStateException("飞书授权失败,错误码:" + body.getInt("code", -1));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 FEISHU 类型和 open_id 查询已绑定账号,由用户服务组装角色与权限。
|
||||
* @param openId 飞书应用内的用户标识,允许为空
|
||||
* @return 已绑定的系统用户信息;标识为空或未匹配到有效用户时返回 null
|
||||
*/
|
||||
@Override
|
||||
public UserInfo info(String openId) {
|
||||
if (StrUtil.isBlank(openId)) {
|
||||
return null;
|
||||
}
|
||||
UserDTO query = new UserDTO();
|
||||
query.setFeishuOpenid(openId);
|
||||
return sysUserService.getUserInfo(query).getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已验证的飞书身份绑定到系统用户,沿用社交身份表的唯一约束。 同一用户已有飞书绑定时更新 identifier,否则新增;不创建系统用户。
|
||||
* @param user 待绑定的系统用户,不可为 null,且 userId 必须有效
|
||||
* @param identify 经 identify 验证的非空飞书 open_id
|
||||
* @return 插入或更新影响行数大于零时为 true,否则为 false
|
||||
* @throws IllegalArgumentException 飞书身份为空
|
||||
* @throws org.springframework.dao.DuplicateKeyException 该飞书身份已绑定其他用户
|
||||
*/
|
||||
@Override
|
||||
public Boolean bind(SysUser user, String identify) {
|
||||
if (StrUtil.isBlank(identify)) {
|
||||
throw new IllegalArgumentException("飞书用户身份不能为空");
|
||||
}
|
||||
SysUserSocial existing = sysUserSocialMapper.selectOne(Wrappers.<SysUserSocial>lambdaQuery()
|
||||
.eq(SysUserSocial::getUserId, user.getUserId())
|
||||
.eq(SysUserSocial::getType, LoginTypeEnum.FEISHU.getType()));
|
||||
if (existing != null) {
|
||||
existing.setIdentifier(identify);
|
||||
return sysUserSocialMapper.updateById(existing) > 0;
|
||||
}
|
||||
SysUserSocial social = new SysUserSocial();
|
||||
social.setUserId(user.getUserId());
|
||||
social.setType(LoginTypeEnum.FEISHU.getType());
|
||||
social.setIdentifier(identify);
|
||||
return sysUserSocialMapper.insert(social) > 0;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -84,7 +84,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder();
|
||||
|
||||
private static final Set<String> UNBIND_TYPES = Set.of(LoginTypeEnum.WECHAT.getType(),
|
||||
LoginTypeEnum.WEIXIN_CP.getType(), LoginTypeEnum.DINGTALK.getType());
|
||||
LoginTypeEnum.WEIXIN_CP.getType(), LoginTypeEnum.DINGTALK.getType(), LoginTypeEnum.FEISHU.getType());
|
||||
|
||||
private final SysMenuService sysMenuService;
|
||||
|
||||
|
||||
@@ -68,6 +68,10 @@
|
||||
INNER JOIN sys_user_social dingtalk_social ON dingtalk_social.user_id = u.user_id
|
||||
AND dingtalk_social.type = 'DINGTALK' AND dingtalk_social.identifier = #{query.wxDingUserid}
|
||||
</if>
|
||||
<if test="query.feishuOpenid != null and query.feishuOpenid != ''">
|
||||
INNER JOIN sys_user_social feishu_social ON feishu_social.user_id = u.user_id
|
||||
AND feishu_social.type = 'FEISHU' AND feishu_social.identifier = #{query.feishuOpenid}
|
||||
</if>
|
||||
<if test="query.wxOpenid != null and query.wxOpenid != ''">
|
||||
INNER JOIN sys_user_social wx_social ON wx_social.user_id = u.user_id
|
||||
AND wx_social.type = 'WX' AND wx_social.identifier = #{query.wxOpenid}
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.pig4cloud.pigx.admin.handler;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pig4cloud.pigx.admin.api.dto.UserDTO;
|
||||
import com.pig4cloud.pigx.admin.api.dto.UserInfo;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysSocialDetails;
|
||||
import com.pig4cloud.pigx.admin.mapper.SysSocialDetailsMapper;
|
||||
import com.pig4cloud.pigx.admin.mapper.SysUserSocialMapper;
|
||||
import com.pig4cloud.pigx.admin.service.SysUserService;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class FeishuLoginHandlerTests {
|
||||
|
||||
private final SysSocialDetailsMapper configMapper = mock(SysSocialDetailsMapper.class);
|
||||
|
||||
private final SysUserService users = mock(SysUserService.class);
|
||||
|
||||
private final SysUserSocialMapper bindings = mock(SysUserSocialMapper.class);
|
||||
|
||||
private FeishuLoginHandler handler() {
|
||||
SysSocialDetails config = new SysSocialDetails();
|
||||
config.setAppId("cli_test");
|
||||
config.setAppSecret("test-secret");
|
||||
config.setRedirectUrl("https://example.com/#/authredirect");
|
||||
when(configMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(config);
|
||||
return spy(new FeishuLoginHandler(users, configMapper, bindings));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exchangesCodeAndLooksUpBoundFeishuIdentity() {
|
||||
FeishuLoginHandler handler = handler();
|
||||
UserInfo expected = new UserInfo();
|
||||
when(users.getUserInfo(any(UserDTO.class))).thenReturn(R.ok(expected));
|
||||
doAnswer(invocation -> {
|
||||
HttpRequest request = invocation.getArgument(0);
|
||||
if (request.getUrl().equals("https://accounts.feishu.cn/oauth/v3/token")) {
|
||||
assertThat(request.form()).containsEntry("grant_type", "authorization_code")
|
||||
.containsEntry("client_id", "cli_test")
|
||||
.containsEntry("client_secret", "test-secret")
|
||||
.containsEntry("code", "test-code")
|
||||
.containsEntry("redirect_uri", "https://example.com/#/authredirect");
|
||||
return JSONUtil.parseObj("{\"code\":0,\"access_token\":\"test-token\"}");
|
||||
}
|
||||
assertThat(request.getUrl()).isEqualTo("https://open.feishu.cn/open-apis/authen/v1/user_info");
|
||||
assertThat(request.header("Authorization")).isEqualTo("Bearer test-token");
|
||||
return JSONUtil.parseObj("{\"code\":0,\"data\":{\"open_id\":\"ou_test\"}}");
|
||||
}).when(handler).request(any(HttpRequest.class));
|
||||
assertThat(handler.handle("test-code")).isSameAs(expected);
|
||||
verify(handler, times(2)).request(any(HttpRequest.class));
|
||||
ArgumentCaptor<UserDTO> query = ArgumentCaptor.forClass(UserDTO.class);
|
||||
verify(users).getUserInfo(query.capture());
|
||||
assertThat(query.getValue().getFeishuOpenid()).isEqualTo("ou_test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectedAuthorizationCannotQueryOrBindUser() {
|
||||
FeishuLoginHandler handler = handler();
|
||||
HttpRequest request = mock(HttpRequest.class, RETURNS_SELF);
|
||||
HttpResponse response = mock(HttpResponse.class);
|
||||
when(request.execute()).thenReturn(response);
|
||||
when(response.isOk()).thenReturn(true);
|
||||
when(response.body()).thenReturn("{\"code\":20004}");
|
||||
FeishuLoginHandler transport = new FeishuLoginHandler(users, configMapper, bindings);
|
||||
doAnswer(invocation -> transport.request(request)).when(handler).request(any(HttpRequest.class));
|
||||
assertThatThrownBy(() -> handler.handle("expired-code")).isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("20004");
|
||||
verifyNoInteractions(users, bindings);
|
||||
verify(response).close();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user