@oj-module/cli 0.2.49 → 0.2.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/devkit/README.md +8 -6
- package/devkit/SKILL.md +32 -12
- package/devkit/manual.md +6 -3
- package/devkit/wecom-daikaifa.md +95 -1
- package/devkit/wecom-internal-app.md +643 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/init.ts +5 -4
- package/templates/config.yaml +28 -6
- package/templates/openapi.json +174 -2
- package/templates/src/auth/api/_shared/wecom/admin.ts +87 -7
- package/templates/src/auth/api/_shared/wecom/config.ts +256 -46
- package/templates/src/auth/api/_shared/wecom/contacts.ts +7 -1
- package/templates/src/auth/api/_shared/wecom/events.ts +25 -6
- package/templates/src/auth/api/_shared/wecom/login.ts +21 -13
- package/templates/src/auth/api/_shared/wecom/store.ts +61 -12
- package/templates/src/auth/api/_shared/wecom/sync.ts +11 -1
- package/templates/src/auth/api/migrations/0003__credential_kind.mysql.sql +3 -0
- package/templates/src/auth/api/migrations/0003__credential_kind.pg.sql +3 -0
- package/templates/src/auth/api/migrations/0003__credential_kind.sql +9 -0
- package/templates/src/auth/api/schema.yaml +2 -0
- package/templates/src/auth/api/wecom/admin/api.ts +112 -11
- package/templates/src/auth/api/wecom/api.ts +102 -48
- package/templates/src/auth/api/wecom/sync/api.ts +21 -5
- package/templates/src/auth/web/__client/api.schemas.ts +22 -1
- package/templates/src/auth/web/__client/api.ts +47 -1
- package/templates/src/system/web/locales/en-US.json +24 -1
- package/templates/src/system/web/locales/zh-CN.json +24 -1
- package/templates/src/system/web/pages/departments/sync-bar.tsx +32 -7
- package/templates/src/system/web/pages/wecom/index.tsx +164 -46
- package/templates/tests/wecom-admin.spec.ts +7 -7
- package/templates/tests/wecom-connector.spec.ts +7 -6
- package/templates/tests/wecom-corp.spec.ts +523 -0
- package/templates/tests/wecom-endpoints.spec.ts +10 -4
- package/templates/tests/wecom-org-sync.spec.ts +7 -1
- package/vendor/host-versions.json +3 -3
|
@@ -18,6 +18,10 @@ interface SyncView {
|
|
|
18
18
|
connected: boolean
|
|
19
19
|
authMode: number
|
|
20
20
|
profileStatus: "active" | "disabled" | null
|
|
21
|
+
/** 已解析到的应用 id:多应用/共存企业的分辨锚点,start / cancel 要原样回传 */
|
|
22
|
+
agentId: string | null
|
|
23
|
+
/** suite=代开发 / corp=企业内部应用(自建):自建应用没有「授权模式」这回事 */
|
|
24
|
+
credentialKind: "suite" | "corp" | null
|
|
21
25
|
status: "idle" | "running" | "done" | "failed" | "cancelled"
|
|
22
26
|
phase: "departments" | "members" | "reconcile"
|
|
23
27
|
stats: { departments: number, members: number, skipped: number, reconciled: boolean }
|
|
@@ -36,25 +40,45 @@ const MIN_GAP_MS = 6000;
|
|
|
36
40
|
/** 一轮点击最多推多少片(12 秒/片 ≈ 12 分钟):兜底防呆,正常企业两三次就到底 */
|
|
37
41
|
const MAX_SLICES = 60;
|
|
38
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 已解析到的应用 id。同一家企业有两行档案(代开发 + 自建,迁移期常见)时后端要求带 agent,
|
|
45
|
+
* 而这个值只有后端知道:首次 status 不带(后端按「本站自建应用优先」解析),拿到后记住,
|
|
46
|
+
* 后续 start / cancel 原样回传。放模块级是为了跨组件实例复用,不参与渲染。
|
|
47
|
+
*/
|
|
48
|
+
let agentParam: string | null = null;
|
|
49
|
+
|
|
50
|
+
function withAgent(url: string): string {
|
|
51
|
+
return agentParam ? `${url}?agent=${encodeURIComponent(agentParam)}` : url;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function agentBody(): { agent?: string } {
|
|
55
|
+
return agentParam ? { agent: agentParam } : {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function remember(view: SyncView): SyncView {
|
|
59
|
+
agentParam = view.agentId ?? agentParam;
|
|
60
|
+
return view;
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
async function fetchStatus(): Promise<SyncView> {
|
|
40
|
-
const env = await request.get("auth/wecom/sync/status").json<{ code: number, data?: SyncView }>();
|
|
64
|
+
const env = await request.get(withAgent("auth/wecom/sync/status")).json<{ code: number, data?: SyncView }>();
|
|
41
65
|
if (!env.data)
|
|
42
66
|
throw new Error("企业微信同步状态读取失败");
|
|
43
|
-
return env.data;
|
|
67
|
+
return remember(env.data);
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
async function pushSlice(): Promise<SyncView> {
|
|
47
|
-
const env = await request.post("auth/wecom/sync/start", { json:
|
|
71
|
+
const env = await request.post("auth/wecom/sync/start", { json: agentBody() }).json<{ code: number, data?: SyncView }>();
|
|
48
72
|
if (!env.data)
|
|
49
73
|
throw new Error("企业微信同步未能启动");
|
|
50
|
-
return env.data;
|
|
74
|
+
return remember(env.data);
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
async function cancelSync(): Promise<SyncView> {
|
|
54
|
-
const env = await request.post("auth/wecom/sync/cancel", { json:
|
|
78
|
+
const env = await request.post("auth/wecom/sync/cancel", { json: agentBody() }).json<{ code: number, data?: SyncView }>();
|
|
55
79
|
if (!env.data)
|
|
56
80
|
throw new Error("取消同步失败");
|
|
57
|
-
return env.data;
|
|
81
|
+
return remember(env.data);
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
function sleep(ms: number): Promise<void> {
|
|
@@ -180,7 +204,8 @@ export function WecomSyncBar({ onFinished }: { onFinished: () => void }) {
|
|
|
180
204
|
if (!view || !view.connected)
|
|
181
205
|
return null;
|
|
182
206
|
|
|
183
|
-
|
|
207
|
+
// 成员授权卡片只对代开发成立:自建应用没有「授权模式」,authMode 恒 0(双保险,防上游写脏数据)
|
|
208
|
+
if (view.credentialKind !== "corp" && view.authMode !== 0) {
|
|
184
209
|
return (
|
|
185
210
|
<Alert
|
|
186
211
|
type="warning"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { request, useUserStore } from "@oj-module/runtime";
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import { App, Button, Card, Descriptions, Popconfirm, QRCode, Space, Table, Tag, Typography } from "antd";
|
|
3
|
+
import { Alert, App, Button, Card, Descriptions, Popconfirm, QRCode, Space, Table, Tag, Typography } from "antd";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
5
|
import { useTranslation } from "react-i18next";
|
|
6
6
|
|
|
@@ -16,7 +16,9 @@ interface AppRow {
|
|
|
16
16
|
env: string
|
|
17
17
|
corp_name: string | null
|
|
18
18
|
status: "active" | "disabled"
|
|
19
|
-
/**
|
|
19
|
+
/** 应用来源:suite=代开发 / corp=企业内部应用(自建) */
|
|
20
|
+
credential_kind: "suite" | "corp"
|
|
21
|
+
/** 0=管理员授权 / 1=成员授权:成员授权下这家企业的应用做不了组织架构同步;自建行恒 0 */
|
|
20
22
|
auth_mode: number
|
|
21
23
|
installed_at: number
|
|
22
24
|
update_time: number
|
|
@@ -28,6 +30,23 @@ interface InstallInfo {
|
|
|
28
30
|
expiresIn: number
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** GET /auth/wecom/admin/overview:本部署配了哪几套凭据、回调地址填什么 */
|
|
34
|
+
interface Overview {
|
|
35
|
+
kinds: Array<"suite" | "corp">
|
|
36
|
+
configError: string | null
|
|
37
|
+
callbackUrl: string | null
|
|
38
|
+
corpId: string | null
|
|
39
|
+
agentId: string | null
|
|
40
|
+
corpCallbackConfigured: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ConnectResult {
|
|
44
|
+
corpId: string
|
|
45
|
+
agentId: string
|
|
46
|
+
departments: number
|
|
47
|
+
members: number
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
/**
|
|
32
51
|
* 端点不在本模块的生成 client 里(`__client` 按模块切分),故直接用 runtime 的 ky 出口。
|
|
33
52
|
* 路径不带前导 `/`:与 request 的 baseURL 拼接口径一致(fetchLogin 同款)。
|
|
@@ -37,6 +56,11 @@ async function fetchApps(): Promise<AppRow[]> {
|
|
|
37
56
|
return env.data ?? [];
|
|
38
57
|
}
|
|
39
58
|
|
|
59
|
+
async function fetchOverview(): Promise<Overview> {
|
|
60
|
+
const env = await request.get("auth/wecom/admin/overview").json<{ code: number, data?: Overview }>();
|
|
61
|
+
return env.data ?? { kinds: [], configError: null, callbackUrl: null, corpId: null, agentId: null, corpCallbackConfigured: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
async function setStatus(input: { corpId: string, agentId: string, status: "active" | "disabled" }): Promise<{ updated: number, revokedSessions: number }> {
|
|
41
65
|
const env = await request.post("auth/wecom/admin/status", { json: input }).json<{ code: number, data?: { updated: number, revokedSessions: number } }>();
|
|
42
66
|
return env.data ?? { updated: 0, revokedSessions: 0 };
|
|
@@ -49,12 +73,19 @@ async function createInstall(): Promise<InstallInfo> {
|
|
|
49
73
|
return env.data;
|
|
50
74
|
}
|
|
51
75
|
|
|
76
|
+
async function connectCorp(): Promise<ConnectResult> {
|
|
77
|
+
const env = await request.post("auth/wecom/admin/connect", { json: {} }).json<{ code: number, data?: ConnectResult }>();
|
|
78
|
+
if (!env.data)
|
|
79
|
+
throw new Error("企业微信未返回接入结果");
|
|
80
|
+
return env.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
52
83
|
/**
|
|
53
|
-
*
|
|
54
|
-
* ·
|
|
55
|
-
*
|
|
56
|
-
* ·
|
|
57
|
-
*
|
|
84
|
+
* 企微后台(superadmin 独占),两块卡片按「本部署配了哪套凭据」决定显不显示:
|
|
85
|
+
* · 代开发:已授权企业 × 应用清单、启停、开通入口(点「生成开通链接」→ 后端调
|
|
86
|
+
* get_customized_auth_url 换 qrcode_url,企业管理员扫/开即授权,由 create_auth 回调落库);
|
|
87
|
+
* · 企业内部应用(自建):本企业的那一份凭据 + 回调地址 + 「验证并接入」。
|
|
88
|
+
* 两套可以同时启用(`wecom_allow_coexist: 'true'`),此时两张卡片并列,清单里按「来源」区分。
|
|
58
89
|
*/
|
|
59
90
|
export default function WecomAdminPage() {
|
|
60
91
|
const { t } = useTranslation();
|
|
@@ -83,6 +114,14 @@ export default function WecomAdminPage() {
|
|
|
83
114
|
retry: false,
|
|
84
115
|
});
|
|
85
116
|
|
|
117
|
+
// 概览读的是**部署常量**而不是库:配置错的时候清单可能是空的,而这里必须还能说清「缺什么」
|
|
118
|
+
const overviewQuery = useQuery({
|
|
119
|
+
queryKey: ["system", "wecom-overview"],
|
|
120
|
+
queryFn: fetchOverview,
|
|
121
|
+
enabled: isSuper,
|
|
122
|
+
retry: false,
|
|
123
|
+
});
|
|
124
|
+
|
|
86
125
|
const statusMut = useMutation({
|
|
87
126
|
mutationFn: setStatus,
|
|
88
127
|
onSuccess: (r) => {
|
|
@@ -106,9 +145,24 @@ export default function WecomAdminPage() {
|
|
|
106
145
|
onError: e => message.error(explainError(e, t)),
|
|
107
146
|
});
|
|
108
147
|
|
|
148
|
+
const connectMut = useMutation({
|
|
149
|
+
mutationFn: connectCorp,
|
|
150
|
+
onSuccess: (r) => {
|
|
151
|
+
message.success(t("system:wecom.corp.connectOk", { depts: r.departments, members: r.members }));
|
|
152
|
+
queryClient.invalidateQueries({ queryKey: ["system", "wecom-apps"] });
|
|
153
|
+
queryClient.invalidateQueries({ queryKey: ["system", "wecom-overview"] });
|
|
154
|
+
},
|
|
155
|
+
onError: e => message.error(explainError(e, t)),
|
|
156
|
+
});
|
|
157
|
+
|
|
109
158
|
if (!isSuper)
|
|
110
159
|
return <ForbiddenPanel t={t} />;
|
|
111
160
|
|
|
161
|
+
const overview = overviewQuery.data;
|
|
162
|
+
const kinds = overview?.kinds ?? [];
|
|
163
|
+
const hasSuite = kinds.includes("suite");
|
|
164
|
+
const hasCorp = kinds.includes("corp");
|
|
165
|
+
|
|
112
166
|
const columns = [
|
|
113
167
|
{
|
|
114
168
|
title: t("system:wecom.col.corp"),
|
|
@@ -120,16 +174,26 @@ export default function WecomAdminPage() {
|
|
|
120
174
|
</div>
|
|
121
175
|
),
|
|
122
176
|
},
|
|
177
|
+
{
|
|
178
|
+
// 来源列:共存部署里清单会同时出现两类行,不标出来就分不清「这行是谁装进来的」
|
|
179
|
+
title: t("system:wecom.col.source"),
|
|
180
|
+
dataIndex: "credential_kind",
|
|
181
|
+
render: (v: AppRow["credential_kind"]) => (v === "corp"
|
|
182
|
+
? <Tag color="blue">{t("system:wecom.source.corp")}</Tag>
|
|
183
|
+
: <Tag>{t("system:wecom.source.suite")}</Tag>),
|
|
184
|
+
},
|
|
123
185
|
{ title: t("system:wecom.col.agent"), dataIndex: "agent_id" },
|
|
124
186
|
{ title: t("system:wecom.col.env"), dataIndex: "env", render: (v: string) => <Tag>{v}</Tag> },
|
|
125
187
|
{
|
|
126
188
|
// 权限三态:成员授权是「装了也同步不了」的硬约束(企微侧通讯录接口一律 60011),
|
|
127
|
-
//
|
|
189
|
+
// 摆在清单上,服务商不用点进去试一遍才知道。自建应用没有这个概念,显示空值。
|
|
128
190
|
title: t("system:wecom.col.authMode"),
|
|
129
191
|
dataIndex: "auth_mode",
|
|
130
|
-
render: (v: number) => (
|
|
131
|
-
? <
|
|
132
|
-
:
|
|
192
|
+
render: (v: number, row: AppRow) => (row.credential_kind === "corp"
|
|
193
|
+
? <Typography.Text type="secondary">{t("system:wecom.authModeNone")}</Typography.Text>
|
|
194
|
+
: v === 0
|
|
195
|
+
? <Tag color="green">{t("system:wecom.authModeAdmin")}</Tag>
|
|
196
|
+
: <Tag color="orange">{t("system:wecom.authModeMember")}</Tag>),
|
|
133
197
|
},
|
|
134
198
|
{
|
|
135
199
|
title: t("system:wecom.col.status"),
|
|
@@ -166,41 +230,95 @@ export default function WecomAdminPage() {
|
|
|
166
230
|
|
|
167
231
|
return (
|
|
168
232
|
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
233
|
+
{overview?.configError && (
|
|
234
|
+
<Alert
|
|
235
|
+
type="error"
|
|
236
|
+
showIcon
|
|
237
|
+
message={t("system:wecom.configErrorTitle")}
|
|
238
|
+
description={overview.configError}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
{hasSuite && hasCorp && (
|
|
242
|
+
<Alert type="info" showIcon message={t("system:wecom.coexistHint")} />
|
|
243
|
+
)}
|
|
244
|
+
{hasSuite && (
|
|
245
|
+
<Card
|
|
246
|
+
title={t("system:wecom.title")}
|
|
247
|
+
extra={(
|
|
248
|
+
<Button type="primary" loading={installMut.isPending} onClick={() => installMut.mutate()}>
|
|
249
|
+
{t("system:wecom.genInstall")}
|
|
250
|
+
</Button>
|
|
251
|
+
)}
|
|
252
|
+
>
|
|
253
|
+
<Typography.Paragraph type="secondary">{t("system:wecom.hint")}</Typography.Paragraph>
|
|
254
|
+
{install && (
|
|
255
|
+
<div style={{ display: "flex", gap: 24, alignItems: "flex-start", flexWrap: "wrap" }}>
|
|
256
|
+
{/* 二维码用 antd 内置 QRCode(同一份共享 antd,不额外引二维码依赖):
|
|
257
|
+
企业管理员多半在手机上收通知,扫码比复制链接快 */}
|
|
258
|
+
<QRCode
|
|
259
|
+
value={install.installUrl}
|
|
260
|
+
size={160}
|
|
261
|
+
type="svg"
|
|
262
|
+
bordered
|
|
263
|
+
status={expired ? "expired" : "active"}
|
|
264
|
+
onRefresh={() => installMut.mutate()}
|
|
265
|
+
/>
|
|
266
|
+
<Descriptions column={1} size="small" bordered style={{ flex: 1, minWidth: 320 }}>
|
|
267
|
+
<Descriptions.Item label={t("system:wecom.installUrl")}>
|
|
268
|
+
<Typography.Text copyable={{ text: install.installUrl }}>{install.installUrl}</Typography.Text>
|
|
269
|
+
</Descriptions.Item>
|
|
270
|
+
<Descriptions.Item label={t("system:wecom.callbackUrl")}>
|
|
271
|
+
<Typography.Text copyable={{ text: install.callbackUrl }}>{install.callbackUrl}</Typography.Text>
|
|
272
|
+
</Descriptions.Item>
|
|
273
|
+
<Descriptions.Item label={t("system:wecom.expiresIn")}>
|
|
274
|
+
{t("system:wecom.expiresValue", { count: Math.round(install.expiresIn / 60) })}
|
|
275
|
+
</Descriptions.Item>
|
|
276
|
+
</Descriptions>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</Card>
|
|
280
|
+
)}
|
|
281
|
+
{hasCorp && (
|
|
282
|
+
<Card
|
|
283
|
+
title={t("system:wecom.corp.title")}
|
|
284
|
+
extra={(
|
|
285
|
+
<Popconfirm
|
|
286
|
+
title={t("system:wecom.corp.connectConfirm")}
|
|
287
|
+
onConfirm={() => connectMut.mutate()}
|
|
288
|
+
>
|
|
289
|
+
<Button type="primary" loading={connectMut.isPending}>
|
|
290
|
+
{t("system:wecom.corp.connect")}
|
|
291
|
+
</Button>
|
|
292
|
+
</Popconfirm>
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
<Typography.Paragraph type="secondary">{t("system:wecom.corp.hint")}</Typography.Paragraph>
|
|
296
|
+
<Descriptions column={1} size="small" bordered>
|
|
297
|
+
<Descriptions.Item label={t("system:wecom.corp.corpId")}>
|
|
298
|
+
<Typography.Text>{overview?.corpId ?? "-"}</Typography.Text>
|
|
299
|
+
</Descriptions.Item>
|
|
300
|
+
<Descriptions.Item label={t("system:wecom.corp.agentId")}>
|
|
301
|
+
<Typography.Text>{overview?.agentId ?? "-"}</Typography.Text>
|
|
302
|
+
</Descriptions.Item>
|
|
303
|
+
<Descriptions.Item label={t("system:wecom.corp.callbackUrl")}>
|
|
304
|
+
{overview?.callbackUrl
|
|
305
|
+
? <Typography.Text copyable={{ text: overview.callbackUrl }}>{overview.callbackUrl}</Typography.Text>
|
|
306
|
+
: "-"}
|
|
307
|
+
</Descriptions.Item>
|
|
308
|
+
<Descriptions.Item label={t("system:wecom.corp.callbackConfigured")}>
|
|
309
|
+
{overview?.corpCallbackConfigured
|
|
310
|
+
? <Tag color="green">{t("system:wecom.corp.callbackYes")}</Tag>
|
|
311
|
+
: <Tag color="orange">{t("system:wecom.corp.callbackNo")}</Tag>}
|
|
312
|
+
</Descriptions.Item>
|
|
313
|
+
</Descriptions>
|
|
314
|
+
<Typography.Paragraph type="secondary" style={{ marginTop: 12, marginBottom: 0 }}>
|
|
315
|
+
{t("system:wecom.corp.callbackHint")}
|
|
316
|
+
</Typography.Paragraph>
|
|
317
|
+
<Typography.Paragraph type="warning" style={{ marginBottom: 0 }}>
|
|
318
|
+
{t("system:wecom.corp.connectHint")}
|
|
319
|
+
</Typography.Paragraph>
|
|
320
|
+
</Card>
|
|
321
|
+
)}
|
|
204
322
|
<Card title={t("system:wecom.appsTitle")}>
|
|
205
323
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
|
206
324
|
{t("system:wecom.authModeHint")}
|
|
@@ -15,7 +15,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
|
15
15
|
import { assertSuperAdmin, invalidateAllUsers } from "../src/_platform/api/_shared/service/infra/access";
|
|
16
16
|
import { listApps, setAppStatus } from "../src/auth/api/_shared/wecom/admin";
|
|
17
17
|
import { createWecomClient, runtimeCache, runtimeHttp } from "../src/auth/api/_shared/wecom/client";
|
|
18
|
-
import {
|
|
18
|
+
import { readSuiteConfig } from "../src/auth/api/_shared/wecom/config";
|
|
19
19
|
import { installDb } from "./fixture";
|
|
20
20
|
|
|
21
21
|
const g = globalThis as any;
|
|
@@ -123,7 +123,7 @@ function fakeClient(): { client: any, invalidated: string[] } {
|
|
|
123
123
|
const deps = { store: { encrypt: (p: string) => `gcm(${p})`, decrypt: (b: string) => b, now: () => NOW } };
|
|
124
124
|
|
|
125
125
|
/** createInstall 的薄包装:省掉动态 import 的样板 */
|
|
126
|
-
async function createInstallForTest(cfg: ReturnType<typeof
|
|
126
|
+
async function createInstallForTest(cfg: ReturnType<typeof readSuiteConfig>, client: ReturnType<typeof createWecomClient>, state: string) {
|
|
127
127
|
const { createInstall } = await import("../src/auth/api/_shared/wecom/admin");
|
|
128
128
|
return createInstall(cfg, client, state);
|
|
129
129
|
}
|
|
@@ -274,7 +274,7 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
|
|
|
274
274
|
|
|
275
275
|
it("provider_token → get_customized_auth_url → qrcode_url(即开通链接),且绝不调 get_pre_auth_code", async () => {
|
|
276
276
|
const { calls, bodies } = stubUpstream();
|
|
277
|
-
const cfg =
|
|
277
|
+
const cfg = readSuiteConfig();
|
|
278
278
|
const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
|
|
279
279
|
|
|
280
280
|
const r = await createInstallForTest(cfg, client, "st_1");
|
|
@@ -297,7 +297,7 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
|
|
|
297
297
|
|
|
298
298
|
it("调用方没传 state 时仍发默认 state(避免 40058 state missing)", async () => {
|
|
299
299
|
const { bodies } = stubUpstream();
|
|
300
|
-
const cfg =
|
|
300
|
+
const cfg = readSuiteConfig();
|
|
301
301
|
const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
|
|
302
302
|
await createInstallForTest(cfg, client, "");
|
|
303
303
|
const body = bodies.find(b => b && b.templateid_list) as any;
|
|
@@ -307,14 +307,14 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
|
|
|
307
307
|
|
|
308
308
|
it("缺 provider 凭证(wecom_provider_id / wecom_provider_secret)→ 400 并点名缺哪两个 vars", async () => {
|
|
309
309
|
installVars({ wecom_provider_id: "", wecom_provider_secret: "" });
|
|
310
|
-
const cfg =
|
|
310
|
+
const cfg = readSuiteConfig();
|
|
311
311
|
const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
|
|
312
312
|
await expect(createInstallForTest(cfg, client, "")).rejects.toMatchObject({ status: 400 });
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
it("上游 errcode(如 provider_secret 错)→ 502(可重试语义,不是 500)", async () => {
|
|
316
316
|
g.fetch = async () => ({ ok: true, json: async () => ({ errcode: 40001, errmsg: "invalid provider secret" }) });
|
|
317
|
-
const cfg =
|
|
317
|
+
const cfg = readSuiteConfig();
|
|
318
318
|
const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
|
|
319
319
|
await expect(createInstallForTest(cfg, client, "")).rejects.toMatchObject({ status: 502 });
|
|
320
320
|
});
|
|
@@ -330,7 +330,7 @@ describe("P3-C:开通入口(createInstall,代开发 get_customized_auth_ur
|
|
|
330
330
|
throw new Error(`stub fetch 未预期:${url}`);
|
|
331
331
|
},
|
|
332
332
|
});
|
|
333
|
-
const cfg =
|
|
333
|
+
const cfg = readSuiteConfig();
|
|
334
334
|
const client = createWecomClient({ http: runtimeHttp(), cache: runtimeCache() });
|
|
335
335
|
await expect(createInstallForTest(cfg, client, "st_1")).rejects.toMatchObject({
|
|
336
336
|
status: 502,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
eventUrlOf,
|
|
22
22
|
loginRedirectOf,
|
|
23
23
|
readCallbackConfig,
|
|
24
|
-
|
|
24
|
+
readSuiteConfig,
|
|
25
25
|
siteRootOf,
|
|
26
26
|
} from "../src/auth/api/_shared/wecom/config";
|
|
27
27
|
import { envOfState, handleSuiteEvent } from "../src/auth/api/_shared/wecom/events";
|
|
@@ -105,6 +105,7 @@ function profile(suiteCorpAgent: { corp?: string, agent?: string, env?: string,
|
|
|
105
105
|
agentId: suiteCorpAgent.agent ?? "100001",
|
|
106
106
|
env: suiteCorpAgent.env ?? "prod",
|
|
107
107
|
corpName: "测试企业",
|
|
108
|
+
credentialKind: "suite" as const,
|
|
108
109
|
permanentCode: suiteCorpAgent.code ?? "perm_a",
|
|
109
110
|
};
|
|
110
111
|
}
|
|
@@ -757,7 +758,7 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
|
|
|
757
758
|
installVars({ wecom_suite_id: "", wecom_callback_token: "", wecom_enc_key: "" });
|
|
758
759
|
const msg = (() => {
|
|
759
760
|
try {
|
|
760
|
-
|
|
761
|
+
readSuiteConfig();
|
|
761
762
|
return "";
|
|
762
763
|
}
|
|
763
764
|
catch (e) {
|
|
@@ -789,7 +790,7 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
|
|
|
789
790
|
|
|
790
791
|
it("baseUrl 去尾斜杠后拼出两个企微侧地址(回调 URL 要原样填进后台)", () => {
|
|
791
792
|
installVars();
|
|
792
|
-
const cfg =
|
|
793
|
+
const cfg = readSuiteConfig();
|
|
793
794
|
expect(cfg.baseUrl).toBe("https://site.example.com/api/auth/wecom");
|
|
794
795
|
// 不带 callback_format=json:官方文档只给「对外收款」写了这个参数,代开发的通用回调配置只有 XML
|
|
795
796
|
expect(eventUrlOf(cfg)).toBe("https://site.example.com/api/auth/wecom/event");
|
|
@@ -799,7 +800,7 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
|
|
|
799
800
|
|
|
800
801
|
it("子路径部署:站点根保留子路径(302 回跳必须落 /oj/login,不是 /login)", () => {
|
|
801
802
|
installVars({ wecom_base_url: "https://site.example.com/oj/api/auth/wecom" });
|
|
802
|
-
const cfg =
|
|
803
|
+
const cfg = readSuiteConfig();
|
|
803
804
|
expect(siteRootOf(cfg)).toBe("https://site.example.com/oj");
|
|
804
805
|
expect(loginLandingUrl({ origin: siteRootOf(cfg), ticket: "tk", tenant: "ww_corp_a" }))
|
|
805
806
|
.toBe("https://site.example.com/oj/login?wecom_ticket=tk&tenant=ww_corp_a");
|
|
@@ -807,12 +808,12 @@ describe("P1-J:部署常量读取(缺项一次报全 / 密钥格式 fail-fas
|
|
|
807
808
|
|
|
808
809
|
it("baseUrl 后缀不对 → 400(宁可拒,也不猜站点根拼出错误回跳)", () => {
|
|
809
810
|
installVars({ wecom_base_url: "https://site.example.com/api/wecom" });
|
|
810
|
-
expect(() => siteRootOf(
|
|
811
|
+
expect(() => siteRootOf(readSuiteConfig())).toThrow(/必须以 \/api\/auth\/wecom 结尾/);
|
|
811
812
|
});
|
|
812
813
|
|
|
813
814
|
it("baseUrl 非绝对地址 → 400(相对路径会让 redirect_uri 与回调 URL 双双失效)", () => {
|
|
814
815
|
installVars({ wecom_base_url: "/api/auth/wecom" });
|
|
815
|
-
expect(() =>
|
|
816
|
+
expect(() => readSuiteConfig()).toThrow(/绝对地址/);
|
|
816
817
|
});
|
|
817
818
|
|
|
818
819
|
it("wecom_enc_key 只收 16/32 字节的 hex 或 base64(AES-192 会在加密时才炸,故在读配置处拦住)", () => {
|