@oj-module/cli 0.2.46 → 0.2.48
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/SKILL.md +1 -1
- package/devkit/framework-dev.md +2 -2
- package/devkit/manual.md +24 -14
- package/devkit/module-dev-and-cdn-playground.md +2 -2
- package/devkit/wecom-daikaifa.md +282 -27
- 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/__tests__/templates-tenant-scope.test.ts +7 -2
- package/src/init.ts +1 -1
- package/src/market/__tests__/bootstrap.test.ts +1 -1
- package/src/market/__tests__/setup.test.ts +7 -7
- package/src/market/bootstrap.ts +1 -1
- package/templates/README.md +1 -1
- package/templates/config.yaml +26 -7
- package/templates/init.include.yaml +12 -0
- package/templates/openapi.json +2863 -549
- package/templates/src/_platform/api/_shared/service/domain/contacts.ts +7 -0
- package/templates/src/_platform/api/_shared/service/domain/org-read.ts +451 -0
- package/templates/src/_platform/api/_shared/service/domain/org.ts +566 -0
- package/templates/src/_platform/api/_shared/service/domain/users.ts +140 -2
- package/templates/src/_platform/api/_shared/service/infra/query.ts +2 -1
- package/templates/src/_platform/api/fixtures/l1-test-user.sql +1 -1
- package/templates/src/_platform/api/manifest.yaml +3 -0
- package/templates/src/_platform/api/migrations/0016__org.mysql.sql +41 -0
- package/templates/src/_platform/api/migrations/0016__org.pg.sql +40 -0
- package/templates/src/_platform/api/migrations/0016__org.sql +62 -0
- package/templates/src/_platform/api/org/departments/api.ts +51 -0
- package/templates/src/_platform/api/org/is-member/api.ts +33 -0
- package/templates/src/_platform/api/org/members/api.ts +55 -0
- package/templates/src/_platform/api/org/picker/api.ts +59 -0
- package/templates/src/_platform/api/org/resolve/api.ts +38 -0
- package/templates/src/_platform/api/org/tree/api.ts +37 -0
- package/templates/src/_platform/api/org/user/api.ts +46 -0
- package/templates/src/_platform/api/schema.yaml +48 -0
- package/templates/src/_platform/api/seed.mysql.sql +10 -5
- package/templates/src/_platform/api/seed.pg.sql +10 -5
- package/templates/src/_platform/api/seed.sql +13 -5
- package/templates/src/_platform/web/__client/api.schemas.ts +126 -0
- package/templates/src/_platform/web/__client/api.ts +170 -1
- package/templates/src/auth/api/_shared/ldap-login.ts +2 -0
- package/templates/src/auth/api/_shared/session.ts +5 -1
- package/templates/src/auth/api/_shared/wecom/admin.ts +17 -10
- package/templates/src/auth/api/_shared/wecom/client.ts +311 -40
- package/templates/src/auth/api/_shared/wecom/config.ts +40 -17
- package/templates/src/auth/api/_shared/wecom/contacts.ts +263 -0
- package/templates/src/auth/api/_shared/wecom/crypt.ts +17 -7
- package/templates/src/auth/api/_shared/wecom/events.ts +111 -26
- package/templates/src/auth/api/_shared/wecom/login.ts +152 -1
- package/templates/src/auth/api/_shared/wecom/payload.ts +116 -0
- package/templates/src/auth/api/_shared/wecom/store.ts +65 -0
- package/templates/src/auth/api/_shared/wecom/sync.ts +487 -0
- package/templates/src/auth/api/manifest.yaml +3 -0
- package/templates/src/auth/api/migrations/0002__org_sync.mysql.sql +28 -0
- package/templates/src/auth/api/migrations/0002__org_sync.pg.sql +27 -0
- package/templates/src/auth/api/migrations/0002__org_sync.sql +41 -0
- package/templates/src/auth/api/schema.yaml +31 -0
- package/templates/src/auth/api/wecom/admin/api.ts +7 -5
- package/templates/src/auth/api/wecom/api.ts +100 -18
- package/templates/src/auth/api/wecom/sync/api.ts +199 -0
- package/templates/src/auth/web/__client/api.schemas.ts +97 -0
- package/templates/src/auth/web/__client/api.ts +73 -1
- package/templates/src/login/web/auth-provider.ts +9 -1
- package/templates/src/notification/api/seed.sql +1 -1
- package/templates/src/system/api/departments/api.ts +142 -0
- package/templates/src/system/api/departments/detach/api.ts +34 -0
- package/templates/src/system/api/departments/members/api.ts +142 -0
- package/templates/src/system/api/departments/move/api.ts +39 -0
- package/templates/src/system/api/users/api.ts +45 -5
- package/templates/src/system/api/users/profile/api.ts +44 -0
- package/templates/src/system/web/__client/api.schemas.ts +143 -0
- package/templates/src/system/web/__client/api.ts +293 -1
- package/templates/src/system/web/entry.ts +6 -0
- package/templates/src/system/web/locales/en-US.json +113 -7
- package/templates/src/system/web/locales/zh-CN.json +113 -7
- package/templates/src/system/web/pages/departments/index.tsx +346 -0
- package/templates/src/system/web/pages/departments/sync-bar.tsx +257 -0
- package/templates/src/system/web/pages/departments/tree.ts +66 -0
- package/templates/src/system/web/pages/errors.ts +1 -1
- package/templates/src/system/web/pages/users/index.tsx +49 -7
- package/templates/src/system/web/pages/wecom/index.tsx +16 -1
- package/templates/tests/l1-builder.test.ts +1 -1
- package/templates/tests/org-departments.spec.ts +311 -0
- package/templates/tests/org-read.spec.ts +469 -0
- package/templates/tests/rbac-contacts.spec.ts +13 -3
- package/templates/tests/rbac-seed.spec.ts +11 -0
- package/templates/tests/seed-role-accounts.spec.ts +6 -2
- package/templates/tests/wecom-admin.spec.ts +64 -42
- package/templates/tests/wecom-client-timeout.spec.ts +99 -0
- package/templates/tests/wecom-connector.spec.ts +115 -11
- package/templates/tests/wecom-crypt.spec.ts +12 -3
- package/templates/tests/wecom-endpoints.spec.ts +182 -14
- package/templates/tests/wecom-org-sync.spec.ts +598 -0
- package/templates/tests/wecom-sync.spec.ts +286 -0
- package/vendor/host-versions.json +3 -3
- package/templates/src/auth/api/_shared/wecom/xml.ts +0 -36
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type { OrgDeps } from "../src/_platform/api/_shared/service/domain/org";
|
|
2
|
+
|
|
3
|
+
// BDD:组织架构领域(0016,方案 A1+A2)——CRUD 基线 + 归属权(D13)+ 保底出口(D14)。
|
|
4
|
+
//
|
|
5
|
+
// 两条最容易回归的红线,用例必须钉死:
|
|
6
|
+
// 1. **判环方向**:把部门挂到自己的子孙下必须 400。实现曾从「目标往下找自己」,
|
|
7
|
+
// 漏判「父挂到孙下」(环是 `id → target → … → id`,即 id 是 target 的**祖先**,
|
|
8
|
+
// 必须沿 target 的父链**往上**找)。漏判后部门树直接成环,前端建树死循环。
|
|
9
|
+
// 2. **归属权(D13)**:origin ≠ local 的行,改名/删除/移动一律 400,且错误文案要能
|
|
10
|
+
// 直接给人看(带来源人话 + 指向「转为本地维护」)。同步与回收只作用于自己 origin。
|
|
11
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
addMember,
|
|
14
|
+
createDepartment,
|
|
15
|
+
deleteDepartment,
|
|
16
|
+
detachToLocal,
|
|
17
|
+
listDepartments,
|
|
18
|
+
listMembers,
|
|
19
|
+
memberCounts,
|
|
20
|
+
moveDepartment,
|
|
21
|
+
|
|
22
|
+
removeMember,
|
|
23
|
+
updateDepartment,
|
|
24
|
+
updateMember,
|
|
25
|
+
upsertDepartment,
|
|
26
|
+
} from "../src/_platform/api/_shared/service/domain/org";
|
|
27
|
+
import { deleteUser, listUsers, updateUserProfile } from "../src/_platform/api/_shared/service/domain/users";
|
|
28
|
+
import { ADMIN, installDb } from "./fixture";
|
|
29
|
+
|
|
30
|
+
let dbo: ReturnType<typeof installDb>;
|
|
31
|
+
const g = globalThis as any;
|
|
32
|
+
const deps: OrgDeps = { now: () => 1_700_000_000 };
|
|
33
|
+
|
|
34
|
+
/** 建一条外部来源(企微)的部门——走适配器写路径,不手写 SQL,顺带钉 upsert 幂等 */
|
|
35
|
+
async function mkDept(name: string, parentId: number | null = null): Promise<number> {
|
|
36
|
+
return createDepartment({ name, parentId }, deps);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deptRow(id: number): { origin: string, name: string | null, parent_id: number | null, deleted: number } {
|
|
40
|
+
const row = dbo.prepare("SELECT origin, name, parent_id, deleted FROM sys_department WHERE id = ?").get(id) as any;
|
|
41
|
+
return { origin: String(row.origin), name: row.name == null ? null : String(row.name), parent_id: row.parent_id == null ? null : Number(row.parent_id), deleted: Number(row.deleted) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mkUser(username: string): number {
|
|
45
|
+
dbo.prepare("INSERT INTO users (username, password_hash) VALUES (?, 'x')").run(username);
|
|
46
|
+
return (dbo.prepare("SELECT id FROM users WHERE username = ?").get(username) as { id: number }).id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeAll(() => {
|
|
50
|
+
dbo = installDb();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
g.http = { user: { id: ADMIN } };
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
dbo.exec("DELETE FROM sys_dept_member");
|
|
59
|
+
dbo.exec("DELETE FROM sys_department");
|
|
60
|
+
dbo.exec("DELETE FROM sys_oplog");
|
|
61
|
+
dbo.exec("DELETE FROM sys_user_role");
|
|
62
|
+
dbo.exec("DELETE FROM sys_refresh_token");
|
|
63
|
+
dbo.exec("DELETE FROM users WHERE username LIKE 'u%'");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("部门 CRUD 基线", () => {
|
|
67
|
+
it("建部门:origin 恒 local + origin_id 自生成 + 落审计", async () => {
|
|
68
|
+
const id = await mkDept("总公司");
|
|
69
|
+
const row = deptRow(id);
|
|
70
|
+
expect(row.origin).toBe("local");
|
|
71
|
+
expect(row.name).toBe("总公司");
|
|
72
|
+
expect(row.parent_id).toBeNull();
|
|
73
|
+
const audit = dbo.prepare("SELECT target FROM sys_oplog WHERE action = 'org.department.create'").all() as Array<{ target: string }>;
|
|
74
|
+
expect(audit.map(a => a.target)).toEqual(["总公司"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("父部门不存在 → 404(不建出孤儿)", async () => {
|
|
78
|
+
await expect(mkDept("孤儿", 999)).rejects.toMatchObject({ status: 404 });
|
|
79
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_department").get()).toMatchObject({ c: 0 });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("改名 / 改英文名;空名 → 400", async () => {
|
|
83
|
+
const id = await mkDept("旧名");
|
|
84
|
+
await updateDepartment(id, { name: "新名", nameEn: "New" }, deps);
|
|
85
|
+
expect(deptRow(id).name).toBe("新名");
|
|
86
|
+
await expect(updateDepartment(id, { name: " " }, deps)).rejects.toMatchObject({ status: 400 });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("删部门:有子部门 → 400(不级联删)", async () => {
|
|
90
|
+
const parent = await mkDept("父");
|
|
91
|
+
await mkDept("子", parent);
|
|
92
|
+
await expect(deleteDepartment(parent)).rejects.toMatchObject({ status: 400 });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("删部门:无子部门 → 真删 + 成员关联一并清(逻辑外键无 FK)", async () => {
|
|
96
|
+
const id = await mkDept("待删");
|
|
97
|
+
const uid = mkUser("u_a");
|
|
98
|
+
await addMember(id, uid, {}, deps);
|
|
99
|
+
await deleteDepartment(id);
|
|
100
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_department WHERE id = ?").get(id)).toMatchObject({ c: 0 });
|
|
101
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_dept_member WHERE dept_id = ?").get(id)).toMatchObject({ c: 0 });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("移动:判环(回归)", () => {
|
|
106
|
+
/** 建 1 → 2 → 3 三级链,返回各自 id */
|
|
107
|
+
async function chain(): Promise<[number, number, number]> {
|
|
108
|
+
const a = await mkDept("A");
|
|
109
|
+
const b = await mkDept("B", a);
|
|
110
|
+
const c = await mkDept("C", b);
|
|
111
|
+
return [a, b, c];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it("合法移动:提到根 / 换父", async () => {
|
|
115
|
+
const [a, b, c] = await chain();
|
|
116
|
+
await moveDepartment(c, a, deps); // C 挂到 A 下(A 不是 C 的子孙)
|
|
117
|
+
expect(deptRow(c).parent_id).toBe(a);
|
|
118
|
+
await moveDepartment(a, null, deps); // 提到根
|
|
119
|
+
expect(deptRow(a).parent_id).toBeNull();
|
|
120
|
+
expect(deptRow(b).parent_id).toBe(a);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("自挂 → 400", async () => {
|
|
124
|
+
const [a] = await chain();
|
|
125
|
+
await expect(moveDepartment(a, a, deps)).rejects.toMatchObject({ status: 400 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("挂到自己的子部门 → 400", async () => {
|
|
129
|
+
const [a, b] = await chain();
|
|
130
|
+
await expect(moveDepartment(a, b, deps)).rejects.toMatchObject({ status: 400 });
|
|
131
|
+
expect(deptRow(a).parent_id).toBeNull(); // 未落库
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("挂到自己的孙部门 → 400(判环方向回归:曾漏判,树直接成环)", async () => {
|
|
135
|
+
const [a, , c] = await chain();
|
|
136
|
+
await expect(moveDepartment(a, c, deps)).rejects.toMatchObject({ status: 400 });
|
|
137
|
+
expect(deptRow(a).parent_id).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("判环不受 sync_visible/deleted 过滤影响(隐藏与软删的行也在父链里)", async () => {
|
|
141
|
+
const a = await mkDept("A");
|
|
142
|
+
const b = await mkDept("B", a);
|
|
143
|
+
dbo.prepare("UPDATE sys_department SET sync_visible = 0, deleted = 1 WHERE id = ?").run(b);
|
|
144
|
+
await expect(moveDepartment(a, b, deps)).rejects.toMatchObject({ status: 400 });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("归属权(D13)与保底出口(D14)", () => {
|
|
149
|
+
it("非 local 部门:改名 / 删除 / 移动 一律 400,文案带来源与出路", async () => {
|
|
150
|
+
const id = await upsertDepartment({ origin: "wecom", originId: "wx-1", parentId: null, name: "企微部门" }, deps);
|
|
151
|
+
await expect(updateDepartment(id, { name: "改名" }, deps)).rejects.toMatchObject({ status: 400 });
|
|
152
|
+
await expect(deleteDepartment(id)).rejects.toMatchObject({ status: 400 });
|
|
153
|
+
await expect(moveDepartment(id, null, deps)).rejects.toMatchObject({ status: 400 });
|
|
154
|
+
// 人话提示:点名来源 + 指路「转为本地维护」
|
|
155
|
+
await expect(updateDepartment(id, { name: "改名" }, deps)).rejects.toThrow(/企业微信/);
|
|
156
|
+
await expect(updateDepartment(id, { name: "改名" }, deps)).rejects.toThrow(/转为本地维护/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("转为本地维护后 CRUD 全开;重复 detach 幂等", async () => {
|
|
160
|
+
const id = await upsertDepartment({ origin: "wecom", originId: "wx-2", parentId: null, name: "企微部门" }, deps);
|
|
161
|
+
await detachToLocal(id, deps);
|
|
162
|
+
expect(deptRow(id).origin).toBe("local");
|
|
163
|
+
await updateDepartment(id, { name: "已转本地" }, deps);
|
|
164
|
+
expect(deptRow(id).name).toBe("已转本地");
|
|
165
|
+
await detachToLocal(id, deps); // 已是 local,直接返回
|
|
166
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_oplog WHERE action = 'org.department.detach'").get()).toMatchObject({ c: 1 });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("加人不受部门 origin 限制(「谁在部门」本站始终可调)", async () => {
|
|
170
|
+
const id = await upsertDepartment({ origin: "wecom", originId: "wx-3", parentId: null, name: "企微部门" }, deps);
|
|
171
|
+
const uid = mkUser("u_w");
|
|
172
|
+
await addMember(id, uid, {}, deps);
|
|
173
|
+
expect((await listMembers(id)).map(m => m.userId)).toEqual([uid]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("适配器写路径(读改写,构造器无 upsert)", () => {
|
|
178
|
+
it("同 (origin, origin_id) 重复 upsert 复用同一行,只改结构字段", async () => {
|
|
179
|
+
const a = await upsertDepartment({ origin: "wecom", originId: "wx-9", parentId: null, name: "旧" }, deps);
|
|
180
|
+
const b = await upsertDepartment({ origin: "wecom", originId: "wx-9", parentId: null, name: "新" }, deps);
|
|
181
|
+
expect(b).toBe(a);
|
|
182
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_department").get()).toMatchObject({ c: 1 });
|
|
183
|
+
expect(deptRow(a).name).toBe("新");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("upsert 不覆写 sync_visible / deleted / origin", async () => {
|
|
187
|
+
const id = await upsertDepartment({ origin: "wecom", originId: "wx-10", parentId: null, name: "x" }, deps);
|
|
188
|
+
dbo.prepare("UPDATE sys_department SET sync_visible = 0, deleted = 1 WHERE id = ?").run(id);
|
|
189
|
+
await upsertDepartment({ origin: "wecom", originId: "wx-10", parentId: null, name: "y" }, deps);
|
|
190
|
+
const row = dbo.prepare("SELECT sync_visible, deleted, origin FROM sys_department WHERE id = ?").get(id) as any;
|
|
191
|
+
expect(Number(row.sync_visible)).toBe(0);
|
|
192
|
+
expect(Number(row.deleted)).toBe(1);
|
|
193
|
+
expect(String(row.origin)).toBe("wecom");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("部门成员", () => {
|
|
198
|
+
it("加 / 列 / 改 / 移;重复加 → 400;改不存在的关联 → 404", async () => {
|
|
199
|
+
const id = await mkDept("技术部");
|
|
200
|
+
const uid = mkUser("u_m");
|
|
201
|
+
await addMember(id, uid, { orderNo: 5 }, deps);
|
|
202
|
+
await expect(addMember(id, uid, {}, deps)).rejects.toMatchObject({ status: 400 });
|
|
203
|
+
const [m] = await listMembers(id);
|
|
204
|
+
expect(m).toMatchObject({ userId: uid, deptId: id, orderNo: 5, isLeader: false, isMain: false, username: "u_m" });
|
|
205
|
+
await updateMember(id, uid, { isLeader: true }, deps);
|
|
206
|
+
expect((await listMembers(id))[0]!.isLeader).toBe(true);
|
|
207
|
+
await removeMember(id, uid, deps);
|
|
208
|
+
expect(await listMembers(id)).toEqual([]);
|
|
209
|
+
await expect(updateMember(id, uid, { isLeader: true }, deps)).rejects.toMatchObject({ status: 404 });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("主部门全租户唯一:设新的主部门会把旧的降下来", async () => {
|
|
213
|
+
const d1 = await mkDept("一");
|
|
214
|
+
const d2 = await mkDept("二");
|
|
215
|
+
const uid = mkUser("u_main");
|
|
216
|
+
await addMember(d1, uid, { isMain: true }, deps);
|
|
217
|
+
await addMember(d2, uid, {}, deps);
|
|
218
|
+
await updateMember(d2, uid, { isMain: true }, deps);
|
|
219
|
+
const mains = dbo.prepare("SELECT dept_id FROM sys_dept_member WHERE user_id = ? AND is_main = 1").all(uid) as Array<{ dept_id: number }>;
|
|
220
|
+
expect(mains.map(r => Number(r.dept_id))).toEqual([d2]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("memberCounts 按部门聚合成员数", async () => {
|
|
224
|
+
const d1 = await mkDept("一");
|
|
225
|
+
const d2 = await mkDept("二");
|
|
226
|
+
for (const u of [mkUser("u_c1"), mkUser("u_c2")])
|
|
227
|
+
await addMember(d1, u, {}, deps);
|
|
228
|
+
await addMember(d2, mkUser("u_c3"), {}, deps);
|
|
229
|
+
const counts = await memberCounts();
|
|
230
|
+
expect(counts.get(d1)).toBe(2);
|
|
231
|
+
expect(counts.get(d2)).toBe(1);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("用户侧(改资料 / 删号)", () => {
|
|
236
|
+
it("改资料:只动展示字段,用户名不在可改之列", async () => {
|
|
237
|
+
const uid = mkUser("u_p");
|
|
238
|
+
await updateUserProfile(ADMIN, uid, { position: "cto", displayName: "张三", email: "z@x.com" });
|
|
239
|
+
const row = dbo.prepare("SELECT position, display_name, email, username FROM users WHERE id = ?").get(uid) as any;
|
|
240
|
+
expect(row.position).toBe("cto");
|
|
241
|
+
expect(row.display_name).toBe("张三");
|
|
242
|
+
expect(row.email).toBe("z@x.com");
|
|
243
|
+
expect(row.username).toBe("u_p");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("改资料:用户不存在 → 404;空 patch 不写库", async () => {
|
|
247
|
+
await expect(updateUserProfile(ADMIN, 9999, { position: "x" })).rejects.toMatchObject({ status: 404 });
|
|
248
|
+
const uid = mkUser("u_noop");
|
|
249
|
+
await updateUserProfile(ADMIN, uid, {});
|
|
250
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_oplog WHERE action = 'user.profile.update'").get()).toMatchObject({ c: 0 });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("删号:级联清部门关联 / 角色 / 会话", async () => {
|
|
254
|
+
const uid = mkUser("u_del");
|
|
255
|
+
const dept = await mkDept("待清");
|
|
256
|
+
await addMember(dept, uid, {}, deps);
|
|
257
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, 'editor')").run(uid);
|
|
258
|
+
dbo.prepare("INSERT INTO sys_refresh_token (user_id, token_hash, expires_at, revoked, created_at) VALUES (?, 'h', 0, 0, ?)").run(uid, 1_700_000_000);
|
|
259
|
+
await deleteUser(ADMIN, uid);
|
|
260
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM users WHERE id = ?").get(uid)).toMatchObject({ c: 0 });
|
|
261
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_dept_member WHERE user_id = ?").get(uid)).toMatchObject({ c: 0 });
|
|
262
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_user_role WHERE user_id = ?").get(uid)).toMatchObject({ c: 0 });
|
|
263
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM sys_refresh_token WHERE user_id = ?").get(uid)).toMatchObject({ c: 0 });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("删号:禁止删自己;最后一个 superadmin 拒删", async () => {
|
|
267
|
+
await expect(deleteUser(ADMIN, ADMIN)).rejects.toMatchObject({ status: 400 });
|
|
268
|
+
const solo = mkUser("u_sup");
|
|
269
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, 'superadmin')").run(solo);
|
|
270
|
+
await expect(deleteUser(ADMIN, solo)).rejects.toMatchObject({ status: 400 });
|
|
271
|
+
// 有两个 superadmin 时允许删(兜底只保最后一个)
|
|
272
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) VALUES (?, 'superadmin')").run(ADMIN);
|
|
273
|
+
await deleteUser(ADMIN, solo);
|
|
274
|
+
expect(dbo.prepare("SELECT COUNT(*) c FROM users WHERE id = ?").get(solo)).toMatchObject({ c: 0 });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("listUsers 带 departments(主部门在前 + isMain)与 position", async () => {
|
|
278
|
+
const d1 = await mkDept("主");
|
|
279
|
+
const d2 = await mkDept("副");
|
|
280
|
+
const uid = mkUser("u_list");
|
|
281
|
+
await addMember(d2, uid, {}, deps);
|
|
282
|
+
await addMember(d1, uid, { isMain: true }, deps);
|
|
283
|
+
await updateUserProfile(ADMIN, uid, { position: "tech lead" });
|
|
284
|
+
const row = (await listUsers()).find(u => u.username === "u_list");
|
|
285
|
+
expect(row?.position).toBe("tech lead");
|
|
286
|
+
// 带 id:前端按部门(含子部门)筛选靠它,部门重名也串不了
|
|
287
|
+
expect(row?.departments).toEqual([
|
|
288
|
+
{ id: d1, name: "主", isMain: true },
|
|
289
|
+
{ id: d2, name: "副", isMain: false },
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("部门名缺失就下发 null,不在接口里造 `#id` 字符串(展示口径归前端)", async () => {
|
|
294
|
+
const d = await mkDept("待改名");
|
|
295
|
+
const uid = mkUser("u_noname");
|
|
296
|
+
await addMember(d, uid, {}, deps);
|
|
297
|
+
dbo.prepare("UPDATE sys_department SET name = NULL WHERE id = ?").run(d);
|
|
298
|
+
const row = (await listUsers()).find(u => u.username === "u_noname");
|
|
299
|
+
expect(row?.departments).toEqual([{ id: d, name: null, isMain: false }]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("口径:listDepartments 默认过滤隐藏与软删,开关显式打开才全给", async () => {
|
|
303
|
+
const visible = await mkDept("可见");
|
|
304
|
+
const hidden = await mkDept("隐藏");
|
|
305
|
+
dbo.prepare("UPDATE sys_department SET sync_visible = 0 WHERE id = ?").run(hidden);
|
|
306
|
+
const gone = await mkDept("源侧已删");
|
|
307
|
+
dbo.prepare("UPDATE sys_department SET deleted = 1 WHERE id = ?").run(gone);
|
|
308
|
+
expect((await listDepartments()).map(d => d.id)).toEqual([visible]);
|
|
309
|
+
expect((await listDepartments({ includeHidden: true, includeDeleted: true })).map(d => d.id).sort()).toEqual([visible, hidden, gone].sort());
|
|
310
|
+
});
|
|
311
|
+
});
|