mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-03 11:10:14 +08:00
Compare commits
1 Commits
codex/rest
...
feat/dingd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
993d0424d5 |
146
astrbot/core/platform/sources/dingtalk/app_registration.py
Normal file
146
astrbot/core/platform/sources/dingtalk/app_registration.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
DEFAULT_DINGTALK_REGISTRATION_BASE_URL = "https://oapi.dingtalk.com"
|
||||
DEFAULT_DINGTALK_REGISTRATION_SOURCE = "DING_DWS_CLAW"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DingtalkAppRegistration:
|
||||
device_code: str
|
||||
user_code: str
|
||||
verification_uri: str
|
||||
verification_uri_complete: str
|
||||
expires_in: int
|
||||
interval: int
|
||||
|
||||
|
||||
def dingtalk_registration_base_url() -> str:
|
||||
return (
|
||||
os.getenv("DINGTALK_REGISTRATION_BASE_URL", "").strip()
|
||||
or DEFAULT_DINGTALK_REGISTRATION_BASE_URL
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def dingtalk_registration_source() -> str:
|
||||
return (
|
||||
os.getenv("DINGTALK_REGISTRATION_SOURCE", "").strip()
|
||||
or DEFAULT_DINGTALK_REGISTRATION_SOURCE
|
||||
)
|
||||
|
||||
|
||||
def _string_field(data: dict[str, Any], key: str) -> str:
|
||||
value = data.get(key)
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _int_field(data: dict[str, Any], key: str, default: int) -> int:
|
||||
value = data.get(key)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, float):
|
||||
return int(value)
|
||||
return default
|
||||
|
||||
|
||||
async def _post_registration(
|
||||
path: str,
|
||||
payload: dict[str, str],
|
||||
) -> tuple[int, dict[str, Any]]:
|
||||
timeout = aiohttp.ClientTimeout(total=15)
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.post(
|
||||
f"{dingtalk_registration_base_url()}{path}",
|
||||
json=payload,
|
||||
) as response:
|
||||
status = response.status
|
||||
data = await response.json(content_type=None)
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError("DingTalk registration response format is invalid")
|
||||
return status, data
|
||||
|
||||
|
||||
def _raise_dingtalk_registration_error(
|
||||
status: int,
|
||||
raw: dict[str, Any],
|
||||
action: str,
|
||||
) -> None:
|
||||
errcode = raw.get("errcode", 0)
|
||||
if status < 400 and int(errcode or 0) == 0:
|
||||
return
|
||||
errmsg = _string_field(raw, "errmsg") or "unknown error"
|
||||
raise RuntimeError(f"[{action}] {errmsg} (errcode={errcode})")
|
||||
|
||||
|
||||
async def request_dingtalk_app_registration() -> DingtalkAppRegistration:
|
||||
status, init_raw = await _post_registration(
|
||||
"/app/registration/init",
|
||||
{"source": dingtalk_registration_source()},
|
||||
)
|
||||
_raise_dingtalk_registration_error(status, init_raw, "init")
|
||||
nonce = _string_field(init_raw, "nonce")
|
||||
if not nonce:
|
||||
raise RuntimeError("[init] missing nonce")
|
||||
|
||||
status, begin_raw = await _post_registration(
|
||||
"/app/registration/begin",
|
||||
{"nonce": nonce},
|
||||
)
|
||||
_raise_dingtalk_registration_error(status, begin_raw, "begin")
|
||||
|
||||
device_code = _string_field(begin_raw, "device_code")
|
||||
verification_uri_complete = _string_field(begin_raw, "verification_uri_complete")
|
||||
if not device_code:
|
||||
raise RuntimeError("[begin] missing device_code")
|
||||
if not verification_uri_complete:
|
||||
raise RuntimeError("[begin] missing verification_uri_complete")
|
||||
|
||||
return DingtalkAppRegistration(
|
||||
device_code=device_code,
|
||||
user_code=_string_field(begin_raw, "user_code"),
|
||||
verification_uri=_string_field(begin_raw, "verification_uri"),
|
||||
verification_uri_complete=verification_uri_complete,
|
||||
expires_in=max(_int_field(begin_raw, "expires_in", 7200), 60),
|
||||
interval=max(_int_field(begin_raw, "interval", 3), 1),
|
||||
)
|
||||
|
||||
|
||||
async def poll_dingtalk_app_registration_once(device_code: str) -> dict[str, Any]:
|
||||
status, raw = await _post_registration(
|
||||
"/app/registration/poll",
|
||||
{"device_code": device_code},
|
||||
)
|
||||
_raise_dingtalk_registration_error(status, raw, "poll")
|
||||
return dingtalk_registration_poll_result(raw)
|
||||
|
||||
|
||||
def dingtalk_registration_poll_result(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
status_raw = _string_field(raw, "status").upper()
|
||||
if status_raw == "WAITING":
|
||||
return {"status": "pending"}
|
||||
if status_raw == "SUCCESS":
|
||||
client_id = _string_field(raw, "client_id")
|
||||
client_secret = _string_field(raw, "client_secret")
|
||||
if not client_id or not client_secret:
|
||||
return {"status": "error", "message": "扫码成功但未获取到钉钉应用凭证"}
|
||||
return {
|
||||
"status": "created",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
}
|
||||
if status_raw == "FAIL":
|
||||
return {
|
||||
"status": "error",
|
||||
"message": _string_field(raw, "fail_reason") or "钉钉扫码创建失败",
|
||||
}
|
||||
if status_raw == "EXPIRED":
|
||||
return {"status": "expired", "message": "钉钉扫码已过期,请重新创建"}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"钉钉扫码创建返回未知状态: {status_raw or 'UNKNOWN'}",
|
||||
}
|
||||
@@ -8,6 +8,10 @@ from quart import request
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.platform import Platform
|
||||
from astrbot.core.platform.sources.dingtalk.app_registration import (
|
||||
poll_dingtalk_app_registration_once,
|
||||
request_dingtalk_app_registration,
|
||||
)
|
||||
from astrbot.core.platform.sources.lark.app_registration import (
|
||||
poll_app_registration_once,
|
||||
request_app_registration,
|
||||
@@ -138,6 +142,8 @@ class PlatformRoute(Route):
|
||||
payload,
|
||||
platform_config,
|
||||
)
|
||||
if platform_type == "dingtalk":
|
||||
return await self._handle_dingtalk_registration(action, payload)
|
||||
|
||||
return Response().error(
|
||||
f"Unsupported platform registration: {platform_type}"
|
||||
@@ -200,6 +206,37 @@ class PlatformRoute(Route):
|
||||
|
||||
return Response().error(f"Unsupported action: {action}").__dict__, 400
|
||||
|
||||
async def _handle_dingtalk_registration(self, action: str, payload: dict):
|
||||
if action == "start":
|
||||
registration = await request_dingtalk_app_registration()
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"status": "pending",
|
||||
"device_code": registration.device_code,
|
||||
"registration_code": registration.device_code,
|
||||
"user_code": registration.user_code,
|
||||
"verification_uri": registration.verification_uri,
|
||||
"verification_uri_complete": registration.verification_uri_complete,
|
||||
"expires_in": registration.expires_in,
|
||||
"interval": registration.interval,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
|
||||
if action == "poll":
|
||||
device_code = str(
|
||||
payload.get("device_code") or payload.get("registration_code") or ""
|
||||
).strip()
|
||||
if not device_code:
|
||||
return Response().error("Missing device_code").__dict__, 400
|
||||
result = await poll_dingtalk_app_registration_once(device_code)
|
||||
return Response().ok(result).__dict__
|
||||
|
||||
return Response().error(f"Unsupported action: {action}").__dict__, 400
|
||||
|
||||
async def _handle_weixin_oc_registration(
|
||||
self,
|
||||
action: str,
|
||||
|
||||
@@ -31,19 +31,19 @@
|
||||
</v-select>
|
||||
<div class="mt-3" v-if="selectedPlatformConfig">
|
||||
<div v-if="isLarkPlatform">
|
||||
<div class="lark-creation-title mt-4 mb-1">
|
||||
<div class="creation-mode-title mt-4 mb-1">
|
||||
{{ tm('registrationAction.mode.title') }}
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="larkCreationMode"
|
||||
class="lark-creation-mode"
|
||||
class="creation-mode-group"
|
||||
hide-details
|
||||
>
|
||||
<v-radio value="scan" :label="tm('registrationAction.mode.scan')"></v-radio>
|
||||
<v-radio value="manual" :label="tm('registrationAction.mode.manual')"></v-radio>
|
||||
<v-radio value="manual" :label="tm('registrationAction.mode.larkManual')"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="larkCreationMode === 'scan'" class="lark-registration-inline mt-3">
|
||||
<div v-if="larkCreationMode === 'scan'" class="registration-inline mt-3">
|
||||
<PlatformRegistrationAction
|
||||
:platform-config="selectedPlatformConfig"
|
||||
:active="larkCreationMode === 'scan'"
|
||||
@@ -54,6 +54,40 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="larkCreationMode === 'manual'" class="mt-2">
|
||||
<div class="platform-action-row">
|
||||
<v-btn color="info" variant="tonal" @click="openTutorial" class="mt-2">
|
||||
<v-icon start>mdi-book-open-variant</v-icon>
|
||||
{{ tm('dialog.viewTutorial') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<AstrBotConfig :iterable="selectedPlatformConfig" :metadata="metadata['platform_group']?.metadata"
|
||||
metadataKey="platform" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isDingtalkPlatform">
|
||||
<div class="creation-mode-title mt-4 mb-1">
|
||||
{{ tm('registrationAction.mode.title') }}
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="dingtalkCreationMode"
|
||||
class="creation-mode-group"
|
||||
hide-details
|
||||
>
|
||||
<v-radio value="scan" :label="tm('registrationAction.mode.scan')"></v-radio>
|
||||
<v-radio value="manual" :label="tm('registrationAction.mode.manual')"></v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="dingtalkCreationMode === 'scan'" class="registration-inline mt-3">
|
||||
<PlatformRegistrationAction
|
||||
:platform-config="selectedPlatformConfig"
|
||||
:active="dingtalkCreationMode === 'scan'"
|
||||
@success="showSuccess"
|
||||
@error="showError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="dingtalkCreationMode === 'manual'" class="mt-2">
|
||||
<div class="platform-action-row">
|
||||
<v-btn color="info" variant="tonal" @click="openTutorial" class="mt-2">
|
||||
<v-icon start>mdi-book-open-variant</v-icon>
|
||||
@@ -390,6 +424,7 @@ export default {
|
||||
selectedPlatformType: null,
|
||||
selectedPlatformConfig: null,
|
||||
larkCreationMode: '',
|
||||
dingtalkCreationMode: '',
|
||||
|
||||
aBConfigRadioVal: '0',
|
||||
selectedAbConfId: 'default',
|
||||
@@ -469,6 +504,16 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isDingtalkPlatform && !this.dingtalkCreationMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isDingtalkPlatform && this.dingtalkCreationMode === 'scan') {
|
||||
if (!this.selectedPlatformConfig?.client_id || !this.selectedPlatformConfig?.client_secret) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isWeixinOcPlatform && !this.selectedPlatformConfig?.weixin_oc_token) {
|
||||
return false;
|
||||
}
|
||||
@@ -511,6 +556,9 @@ export default {
|
||||
},
|
||||
isWeixinOcPlatform() {
|
||||
return this.selectedPlatformConfig?.type === 'weixin_oc';
|
||||
},
|
||||
isDingtalkPlatform() {
|
||||
return this.selectedPlatformConfig?.type === 'dingtalk';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -518,9 +566,11 @@ export default {
|
||||
if (newType && this.platformTemplates[newType]) {
|
||||
this.selectedPlatformConfig = JSON.parse(JSON.stringify(this.platformTemplates[newType]));
|
||||
this.larkCreationMode = '';
|
||||
this.dingtalkCreationMode = '';
|
||||
} else {
|
||||
this.selectedPlatformConfig = null;
|
||||
this.larkCreationMode = '';
|
||||
this.dingtalkCreationMode = '';
|
||||
}
|
||||
},
|
||||
selectedAbConfId(newConfigId) {
|
||||
@@ -606,6 +656,7 @@ export default {
|
||||
this.selectedPlatformType = null;
|
||||
this.selectedPlatformConfig = null;
|
||||
this.larkCreationMode = '';
|
||||
this.dingtalkCreationMode = '';
|
||||
|
||||
this.aBConfigRadioVal = '0';
|
||||
this.selectedAbConfId = 'default';
|
||||
@@ -1181,17 +1232,17 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lark-creation-mode .v-label {
|
||||
.creation-mode-group .v-label {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.lark-creation-title {
|
||||
.creation-mode-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.78);
|
||||
}
|
||||
|
||||
.lark-registration-inline {
|
||||
.registration-inline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -66,6 +66,13 @@ const REGISTRATION_ACTIONS = {
|
||||
successKey: 'registrationAction.weixinOc.created',
|
||||
statusKeyPrefix: 'registrationAction.weixinOc.status',
|
||||
},
|
||||
dingtalk: {
|
||||
endpoint: '/api/platform/registration/dingtalk',
|
||||
icon: 'mdi-qrcode',
|
||||
titleKey: 'registrationAction.dingtalk.title',
|
||||
scanTitleKey: 'registrationAction.dingtalk.scanTitle',
|
||||
successKey: 'registrationAction.dingtalk.created',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -256,6 +263,12 @@ export default {
|
||||
if (data.weixin_oc_base_url) {
|
||||
this.platformConfig.weixin_oc_base_url = data.weixin_oc_base_url;
|
||||
}
|
||||
if (data.client_id) {
|
||||
this.platformConfig.client_id = data.client_id;
|
||||
}
|
||||
if (data.client_secret) {
|
||||
this.platformConfig.client_secret = data.client_secret;
|
||||
}
|
||||
},
|
||||
getStatusText(status) {
|
||||
const normalizedStatus = status || 'idle';
|
||||
|
||||
@@ -152,6 +152,11 @@
|
||||
"error": "Login Failed"
|
||||
}
|
||||
},
|
||||
"dingtalk": {
|
||||
"title": "DingTalk QR Setup",
|
||||
"scanTitle": "Scan with DingTalk on your phone",
|
||||
"created": "Setup Complete"
|
||||
},
|
||||
"start": "Start Setup",
|
||||
"created": "Setup Complete",
|
||||
"startFailed": "Failed to start QR setup",
|
||||
@@ -159,7 +164,8 @@
|
||||
"mode": {
|
||||
"title": "Choose setup method",
|
||||
"scan": "One-click QR setup",
|
||||
"manual": "Manual setup (supports self-hosted Feishu)"
|
||||
"manual": "Manual setup",
|
||||
"larkManual": "Manual setup (supports self-hosted Feishu)"
|
||||
},
|
||||
"scanTitle": "Scan with Feishu on your phone",
|
||||
"status": {
|
||||
|
||||
@@ -152,6 +152,11 @@
|
||||
"error": "Ошибка входа"
|
||||
}
|
||||
},
|
||||
"dingtalk": {
|
||||
"title": "Создание DingTalk по QR",
|
||||
"scanTitle": "Отсканируйте в мобильном DingTalk",
|
||||
"created": "Настройка завершена"
|
||||
},
|
||||
"start": "Начать настройку",
|
||||
"created": "Настройка завершена",
|
||||
"startFailed": "Не удалось начать QR настройку",
|
||||
@@ -159,7 +164,8 @@
|
||||
"mode": {
|
||||
"title": "Выберите способ создания",
|
||||
"scan": "Создать через QR",
|
||||
"manual": "Создать вручную (поддерживает self-hosted Feishu)"
|
||||
"manual": "Создать вручную",
|
||||
"larkManual": "Создать вручную (поддерживает self-hosted Feishu)"
|
||||
},
|
||||
"scanTitle": "Отсканируйте в мобильном Feishu",
|
||||
"status": {
|
||||
|
||||
@@ -152,6 +152,11 @@
|
||||
"error": "登录失败"
|
||||
}
|
||||
},
|
||||
"dingtalk": {
|
||||
"title": "钉钉扫码创建",
|
||||
"scanTitle": "请使用手机钉钉扫码",
|
||||
"created": "创建成功"
|
||||
},
|
||||
"start": "开始创建",
|
||||
"created": "创建成功",
|
||||
"startFailed": "发起扫码创建失败",
|
||||
@@ -159,7 +164,8 @@
|
||||
"mode": {
|
||||
"title": "选择创建方式",
|
||||
"scan": "扫码一键创建",
|
||||
"manual": "手动创建(支持企业自部署飞书平台)"
|
||||
"manual": "手动创建",
|
||||
"larkManual": "手动创建(支持企业自部署飞书平台)"
|
||||
},
|
||||
"scanTitle": "请使用手机飞书扫码",
|
||||
"status": {
|
||||
|
||||
@@ -16,6 +16,20 @@ Proactive message push: Supported.
|
||||
|
||||
## Create and Configure the App
|
||||
|
||||
DingTalk supports two setup methods: one-click QR creation in AstrBot, or manually creating an app in DingTalk Open Platform.
|
||||
|
||||
### Option 1: One-click QR Creation
|
||||
|
||||
AstrBot version requirement: >= v4.25.0.
|
||||
|
||||
Open AstrBot Dashboard -> `Bots` -> `+ Create Bot`, then select `DingTalk`.
|
||||
|
||||
Under `Creation Method`, select `One-click QR setup`, scan the QR code with the DingTalk mobile app, then create or bind a bot on the DingTalk authorization page. After creation succeeds, AstrBot automatically fills in `ClientID` and `ClientSecret`. Click `Save` to finish.
|
||||
|
||||
After QR creation succeeds, continue checking the event subscription, version release, and group installation steps below.
|
||||
|
||||
### Option 2: Manual Creation
|
||||
|
||||
Go to the [DingTalk Open Platform](https://open-dev.dingtalk.com/fe/app), then create an app:
|
||||
|
||||

|
||||
@@ -36,7 +50,7 @@ Go to Credentials & Basic Information, then copy `ClientID` and `ClientSecret`.
|
||||
|
||||
Open AstrBot Dashboard -> `Bots` -> `+ Create Bot`, then create a DingTalk adapter.
|
||||
|
||||
Fill in `ClientID` and `ClientSecret`, then click Save. AstrBot will request authorization from DingTalk Open Platform automatically.
|
||||
If you want AstrBot to create the app for you, select `One-click QR setup` and complete the scan. If you already created the app yourself, select `Manual setup`, fill in `ClientID` and `ClientSecret`, then click Save. AstrBot will request authorization from DingTalk Open Platform automatically.
|
||||
|
||||
Back in DingTalk Open Platform, open Event Subscriptions, select `Stream mode push`, and click Save. If successful, you will see a connected status.
|
||||
|
||||
|
||||
@@ -16,6 +16,20 @@
|
||||
|
||||
## 创建和配置应用
|
||||
|
||||
钉钉支持两种创建方式:在 AstrBot 中扫码一键创建,或在钉钉开放平台手动创建应用。
|
||||
|
||||
### 方式一:扫码一键创建
|
||||
|
||||
需要 AstrBot 版本 >= v4.25.0。
|
||||
|
||||
打开 AstrBot 管理面板 -> `机器人` -> `+ 创建机器人`,选择 `钉钉(DingTalk)`。
|
||||
|
||||
在 `选择创建方式` 中选择 `扫码一键创建`,使用手机钉钉扫描页面中的二维码,并在钉钉页面中创建或绑定机器人。创建成功后,AstrBot 会自动写入 `ClientID` 和 `ClientSecret`,此时点击 `保存` 即可。
|
||||
|
||||
扫码创建完成后,仍建议检查后文的事件订阅、版本发布和拉入群组步骤。
|
||||
|
||||
### 方式二:手动创建
|
||||
|
||||
前往 [钉钉开放平台](https://open-dev.dingtalk.com/fe/app),点击创建应用:
|
||||
|
||||

|
||||
@@ -36,7 +50,7 @@
|
||||
|
||||
打开 AstrBot 管理面板 -> `机器人` -> `+ 创建机器人`,创建一个钉钉适配器。
|
||||
|
||||
将刚刚复制的 `ClientID` 和 `ClientSecret` 填入,点击保存,AstrBot 将会自动向钉钉开放平台请求。
|
||||
如果使用扫码一键创建,选择 `扫码一键创建` 并完成扫码;如果使用自己创建的钉钉应用,选择 `手动创建`,将刚刚复制的 `ClientID` 和 `ClientSecret` 填入。点击保存后,AstrBot 将会自动向钉钉开放平台请求。
|
||||
|
||||
回到钉钉开放平台,点击事件订阅,选择 `Stream 模式推送`,点击保存,如果没有意外情况,将会看到 连接接入成功 字样。
|
||||
|
||||
|
||||
47
tests/test_dingtalk_app_registration.py
Normal file
47
tests/test_dingtalk_app_registration.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from astrbot.core.platform.sources.dingtalk.app_registration import (
|
||||
DEFAULT_DINGTALK_REGISTRATION_BASE_URL,
|
||||
DEFAULT_DINGTALK_REGISTRATION_SOURCE,
|
||||
dingtalk_registration_base_url,
|
||||
dingtalk_registration_poll_result,
|
||||
dingtalk_registration_source,
|
||||
)
|
||||
|
||||
|
||||
def test_dingtalk_registration_defaults(monkeypatch):
|
||||
monkeypatch.delenv("DINGTALK_REGISTRATION_BASE_URL", raising=False)
|
||||
monkeypatch.delenv("DINGTALK_REGISTRATION_SOURCE", raising=False)
|
||||
|
||||
assert dingtalk_registration_base_url() == DEFAULT_DINGTALK_REGISTRATION_BASE_URL
|
||||
assert dingtalk_registration_source() == DEFAULT_DINGTALK_REGISTRATION_SOURCE
|
||||
|
||||
|
||||
def test_dingtalk_registration_poll_result_maps_waiting_and_success():
|
||||
assert dingtalk_registration_poll_result({"status": "WAITING"}) == {
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
assert dingtalk_registration_poll_result(
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"client_id": "client-id",
|
||||
"client_secret": "client-secret",
|
||||
}
|
||||
) == {
|
||||
"status": "created",
|
||||
"client_id": "client-id",
|
||||
"client_secret": "client-secret",
|
||||
}
|
||||
|
||||
|
||||
def test_dingtalk_registration_poll_result_maps_fail_and_expired():
|
||||
assert dingtalk_registration_poll_result(
|
||||
{"status": "FAIL", "fail_reason": "denied"}
|
||||
) == {
|
||||
"status": "error",
|
||||
"message": "denied",
|
||||
}
|
||||
|
||||
assert dingtalk_registration_poll_result({"status": "EXPIRED"}) == {
|
||||
"status": "expired",
|
||||
"message": "钉钉扫码已过期,请重新创建",
|
||||
}
|
||||
Reference in New Issue
Block a user