@oj-module/cli 0.2.6 → 0.2.14
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/README.md +1 -1
- package/devkit/README.md +70 -24
- package/devkit/framework-dev.md +14 -5
- package/devkit/manual.md +28 -23
- package/devkit/module-dev-and-cdn-playground.md +197 -0
- package/devkit/sql-builder-missing-capabilities-pr.md +268 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +130 -45
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +64 -0
- package/src/__tests__/versions.test.ts +26 -0
- package/src/build.ts +9 -0
- package/src/bundle.ts +8 -5
- package/src/contract/__tests__/handler-scan.test.ts +29 -0
- package/src/contract/__tests__/market-client.integration.test.ts +170 -0
- package/src/contract/handler-scan.ts +35 -4
- package/src/dev.ts +10 -2
- package/src/index.ts +9 -3
- package/src/init.ts +24 -0
- package/src/lifecycle/__tests__/bundle.test.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +204 -0
- package/src/market/__tests__/build-index.test.ts +154 -0
- package/src/market/__tests__/cdn-layout.test.ts +96 -0
- package/src/market/__tests__/failure-playbook.test.ts +33 -0
- package/src/market/__tests__/fixture.test.ts +48 -0
- package/src/market/__tests__/publish.test.ts +109 -0
- package/src/market/__tests__/report.test.ts +51 -0
- package/src/market/__tests__/setup.test.ts +114 -0
- package/src/market/__tests__/verify.test.ts +111 -0
- package/src/market/bootstrap.ts +255 -0
- package/src/market/build-index.ts +159 -0
- package/src/market/cdn-layout.ts +122 -0
- package/src/market/command.ts +139 -0
- package/src/market/failure-playbook.ts +57 -0
- package/src/market/fixture.ts +138 -0
- package/src/market/index-builder.ts +76 -0
- package/src/market/publish.ts +156 -0
- package/src/market/report.ts +67 -0
- package/src/market/scan.ts +99 -0
- package/src/market/setup.ts +144 -0
- package/src/market/trial.ts +121 -0
- package/src/market/verify.ts +298 -0
- package/src/market/walkthrough.ts +506 -0
- package/src/oj.ts +3 -1
- package/src/preview-config.ts +177 -0
- package/src/preview.ts +106 -56
- package/src/sign.ts +21 -6
- package/src/usage.ts +22 -2
- package/src/vendor.ts +4 -2
- package/src/versions.ts +28 -0
- package/templates/api/.ojm-api-exempt.json +6 -2
- package/templates/api/config.yaml +24 -2
- package/templates/api/src/_platform/README.md +31 -0
- package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
- package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
- package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
- package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +29 -56
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
- package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +20 -13
- package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
- package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
- package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +25 -22
- package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
- package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
- package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
- package/templates/api/src/_platform/_shared/service/infra/oplog.ts +40 -0
- package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +18 -0
- package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +16 -13
- package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
- package/templates/api/src/_platform/contacts/api.ts +2 -2
- package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
- package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
- package/templates/api/src/_platform/manifest.yaml +4 -1
- package/templates/api/src/_platform/market/api.ts +54 -16
- package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
- package/templates/api/src/_platform/schema.yaml +26 -0
- package/templates/api/src/_platform/solutions/api.ts +13 -12
- package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
- package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
- package/templates/api/src/auth/_shared/session.ts +26 -22
- package/templates/api/src/auth/api.ts +1 -1
- package/templates/api/src/demo/todos/api.ts +8 -8
- package/templates/api/src/home/line/api.ts +4 -4
- package/templates/api/src/home/pie/api.ts +4 -4
- package/templates/api/src/notification/_shared/notify.ts +97 -57
- package/templates/api/src/notification/api.ts +1 -1
- package/templates/api/src/notification/manifest.yaml +1 -0
- package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
- package/templates/api/src/notification/schema.yaml +7 -0
- package/templates/api/src/notification/seed.sql +5 -4
- package/templates/api/src/personal-center/profile/api.ts +63 -0
- package/templates/api/src/personal-center/upload/api.ts +1 -1
- package/templates/api/src/system/login-methods/api.ts +1 -1
- package/templates/api/src/system/oplog/api.ts +3 -3
- package/templates/api/src/system/permissions/api.ts +2 -2
- package/templates/api/src/system/roles/api.ts +2 -2
- package/templates/api/src/system/roles/bindings/api.ts +2 -2
- package/templates/api/src/system/settings/api.ts +2 -2
- package/templates/api/src/system/users/api.ts +2 -2
- package/templates/api/src/system/users/forbidden/api.ts +2 -2
- package/templates/api/src/system/users/password/api.ts +2 -2
- package/templates/api/src/system/users/roles/api.ts +2 -2
- package/templates/api/src/web/user-info/api.ts +4 -5
- package/templates/api/tests/auth-session.spec.ts +1 -1
- package/templates/api/tests/auth-throttle.spec.ts +1 -1
- package/templates/api/tests/fixture.ts +8 -16
- package/templates/api/tests/guard.spec.ts +1 -1
- package/templates/api/tests/l1-builder.test.ts +91 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
- package/templates/api/tests/lifecycle/market-api.spec.ts +2 -4
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
- package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
- package/templates/api/tests/lifecycle/uninstall.spec.ts +29 -89
- package/templates/api/tests/query-utils.spec.ts +85 -1
- package/templates/api/tests/rbac-audit.spec.ts +1 -1
- package/templates/api/tests/rbac-contacts.spec.ts +2 -2
- package/templates/api/tests/rbac-guard.spec.ts +1 -1
- package/templates/api/tests/rbac-roles.spec.ts +2 -2
- package/templates/api/tests/rbac-settings.spec.ts +66 -21
- package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/api/tests/rbac-users.spec.ts +1 -1
- package/templates/api/tsconfig.json +8 -0
- package/templates/cdn/README.md +48 -0
- package/templates/env.d.ts +2 -0
- package/templates/global.d.ts +850 -0
- package/templates/init.include.yaml +14 -2
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/openapi.json +580 -23
- package/templates/package.json +2 -1
- package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
- package/templates/web/src/_platform/__client/api.ts +152 -39
- package/templates/web/src/notification/entry.ts +46 -9
- package/templates/web/src/notification/locales/en-US.json +19 -0
- package/templates/web/src/notification/locales/zh-CN.json +19 -0
- package/templates/web/src/notification/pages/center/index.tsx +131 -0
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
- package/templates/web/src/oj-market/api/client.ts +5 -105
- package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
- package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
- package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
- package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
- package/templates/web/src/oj-market/entry.ts +8 -2
- package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
- package/templates/web/src/oj-market/locales/en-US.json +2 -1
- package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
- package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
- package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
- package/templates/web/src/personal-center/__client/api.ts +25 -1
- package/templates/web/src/personal-center/entry.ts +5 -0
- package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
- package/templates/web/src/system/entry.ts +2 -1
- package/templates/web.config.ts +13 -10
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/_platform/_shared/service/oplog.ts +0 -47
- package/templates/api/src/notifications/api.ts +0 -32
- package/templates/api/src/notifications/manifest.yaml +0 -5
- package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
- package/templates/web/src/notifications/__client/api.ts +0 -80
- /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// L1 回归(`oj test` 收集,*.test.ts;与 vitest 的 *.spec.ts 互不干扰)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么有这类文件:SQL 构造器是 oj 二进制内的纯 Rust 实现,vitest 夹具够不着真
|
|
4
|
+
// builder(架构定论:无 JS 模块可导、不写运行时垫片)——模板 tests/*.spec.ts 只能做
|
|
5
|
+
// L2 记录型断言。**真执行面**(SQL 渲染 / 真过滤 / 真转义)在这里经 oj test 闭环:
|
|
6
|
+
// 真 v8 + 真路由/鉴权 + 真迁移/seed + 真 db + 真构造器(方法论见 devkit oj-test-guide.md)。
|
|
7
|
+
//
|
|
8
|
+
// 覆盖 0.1.81/0.1.82 构造器迁移的关键行为:
|
|
9
|
+
// ① settings 真构造器(IN + LIMIT)——vitest 侧同源码 5 个用例红海的对照面;
|
|
10
|
+
// ② demo/todos:whereIf 真跳过(无 keyword 不过滤)+ contains 真渲染(%kw% + 元字符转义);
|
|
11
|
+
// ③ notification:join ON 值条件 + UNION ALL + 字面量列 {lit,as} 的真渲染(场景 41 同款)。
|
|
12
|
+
//
|
|
13
|
+
// 登录账号:fixtures 的 l1tester(src/_platform/fixtures/l1-test-user.sql,仅 oj test 灌入)。
|
|
14
|
+
// 不用 seed 的 admin:其 must_change=1(判权端点恒 403)且初始口令 123456 会随用户
|
|
15
|
+
// 首登改密漂移——L1 绑 admin 曾实测 401。l1tester 为 additive 专用账号(must_change=0)。
|
|
16
|
+
//
|
|
17
|
+
// 运行:cd packages/cli/templates/api && ~/.local/share/oj/oj test -c config.yaml -d src -t tests
|
|
18
|
+
|
|
19
|
+
const L1_USER = { username: "l1tester", password: "l1test1234" };
|
|
20
|
+
const asL1 = async () => ({ Authorization: `Bearer ${await client.login(L1_USER.username, L1_USER.password)}` });
|
|
21
|
+
|
|
22
|
+
describe("l1:0.1.81/0.1.82 构造器迁移真执行面", () => {
|
|
23
|
+
it("匿名 login-methods:真构造器(IN + LIMIT 渲染)返回 200 + 方法数组", async () => {
|
|
24
|
+
const r = await client.get("/system/login-methods");
|
|
25
|
+
expect(r.status).toBe(200);
|
|
26
|
+
const body = JSON.parse(r.body);
|
|
27
|
+
expect(body.code).toBe(0);
|
|
28
|
+
expect(Array.isArray(body.data.methods)).toBeTruthy();
|
|
29
|
+
expect(body.data.methods).toContain("local");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("demo/todos 无 keyword:whereIf 真跳过(不发射条件),全量 4 条", async () => {
|
|
33
|
+
const h = await asL1();
|
|
34
|
+
const r = await client.get("/demo/todos", { headers: h });
|
|
35
|
+
expect(r.status).toBe(200);
|
|
36
|
+
const body = JSON.parse(r.body);
|
|
37
|
+
expect(body.data.total).toBe(4);
|
|
38
|
+
expect(body.data.list.length).toBe(4); // L1 expect 无 toHaveLength(契约见 api-manual §9)
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("demo/todos keyword=契约:contains 真渲染(%kw%)精确命中 1 条", async () => {
|
|
42
|
+
const h = await asL1();
|
|
43
|
+
const r = await client.get(`/demo/todos?keyword=${encodeURIComponent("契约")}`, { headers: h });
|
|
44
|
+
expect(r.status).toBe(200);
|
|
45
|
+
const body = JSON.parse(r.body);
|
|
46
|
+
expect(body.data.total).toBe(1);
|
|
47
|
+
expect(String(body.data.list[0].title)).toContain("契约");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("demo/todos keyword=%:LIKE 元字符经引擎转义不泛化(0 命中且不炸)", async () => {
|
|
51
|
+
const h = await asL1();
|
|
52
|
+
const r = await client.get(`/demo/todos?keyword=${encodeURIComponent("%")}`, { headers: h });
|
|
53
|
+
expect(r.status).toBe(200);
|
|
54
|
+
expect(JSON.parse(r.body).data.total).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("notification 列表:join ON 值条件 + UNION + 字面量 scope 列真渲染,按人/广播分流", async () => {
|
|
58
|
+
const h = await asL1();
|
|
59
|
+
// 自给自足(oj-test-guide §6 纪律:按自己推的行精确断言,不依赖 seed 的 user_id=1):
|
|
60
|
+
// 先清空本人视图,再经真 push 端点各发一条按人 + 一条广播,回来过 list 断言。
|
|
61
|
+
const clear = await client.post("/notification/notifications/clear", { headers: h });
|
|
62
|
+
expect(clear.status).toBe(200);
|
|
63
|
+
|
|
64
|
+
const pushUser = await client.post("/notification/push", {
|
|
65
|
+
headers: h,
|
|
66
|
+
body: JSON.stringify({ title: "L1 按人通知", message: "join ON 值条件 + UNION 真渲染" }),
|
|
67
|
+
});
|
|
68
|
+
expect(pushUser.status).toBe(200);
|
|
69
|
+
const userId = String(JSON.parse(pushUser.body).data.id);
|
|
70
|
+
expect(userId.startsWith("u:")).toBeTruthy(); // 命名空间 id:按人
|
|
71
|
+
|
|
72
|
+
const pushBcast = await client.post("/notification/push-broadcast", {
|
|
73
|
+
headers: h,
|
|
74
|
+
body: JSON.stringify({ title: "L1 广播通知", message: "LEFT JOIN 回执 + 字面量 scope 列" }),
|
|
75
|
+
});
|
|
76
|
+
expect(pushBcast.status).toBe(200);
|
|
77
|
+
const bcastId = String(JSON.parse(pushBcast.body).data.id);
|
|
78
|
+
expect(bcastId.startsWith("b:")).toBeTruthy(); // 命名空间 id:广播
|
|
79
|
+
|
|
80
|
+
const r = await client.get("/notification/notifications", { headers: h });
|
|
81
|
+
expect(r.status).toBe(200);
|
|
82
|
+
const list = JSON.parse(r.body).data as Array<Record<string, unknown>>;
|
|
83
|
+
expect(Array.isArray(list)).toBeTruthy();
|
|
84
|
+
const byId = new Map(list.map(n => [String(n.id), n]));
|
|
85
|
+
// {lit:"user",as:"scope"} / {lit:"broadcast",...} 经 UNION 落到首列名——两条各带正确作用域
|
|
86
|
+
expect(byId.get(userId)?.scope).toBe("user");
|
|
87
|
+
expect(byId.get(bcastId)?.scope).toBe("broadcast");
|
|
88
|
+
expect(String(byId.get(userId)?.title)).toBe("L1 按人通知");
|
|
89
|
+
expect(String(byId.get(bcastId)?.title)).toBe("L1 广播通知");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -28,7 +28,7 @@ import { acquireTaskLock, commitLockEntry, parseLock, readLock, serializeLock, w
|
|
|
28
28
|
import { createStateStore } from "../../src/_platform/_lifecycle/state";
|
|
29
29
|
import { getTask, readTasks, redact, sweepTimedOut, writeTask } from "../../src/_platform/_lifecycle/tasks";
|
|
30
30
|
import { createTrustStore } from "../../src/_platform/_lifecycle/trust";
|
|
31
|
-
import { installDb
|
|
31
|
+
import { installDb } from "../fixture";
|
|
32
32
|
|
|
33
33
|
const g = globalThis as any;
|
|
34
34
|
|
|
@@ -979,7 +979,6 @@ describe("market_history 留痕(P1-15a 缺口 A):install/enable/disable/un
|
|
|
979
979
|
}
|
|
980
980
|
|
|
981
981
|
it("runInstall 终态(done)落 install 历史(守卫式:随主流程写,失败不阻断)", async () => {
|
|
982
|
-
dbo.exec(MARKET_HISTORY_DDL);
|
|
983
982
|
const t = await runInstall(makeReq());
|
|
984
983
|
expect(t.status).toBe("done");
|
|
985
984
|
const hist = rows("SELECT solution, version, action, status, operator, message FROM market_history");
|
|
@@ -989,7 +988,6 @@ describe("market_history 留痕(P1-15a 缺口 A):install/enable/disable/un
|
|
|
989
988
|
});
|
|
990
989
|
|
|
991
990
|
it("setEnabled 落 disable/enable 历史(status=success,message 与返回人话一致)", async () => {
|
|
992
|
-
dbo.exec(MARKET_HISTORY_DDL);
|
|
993
991
|
await seedHistory();
|
|
994
992
|
const off = await setEnabled({ name: "demo-solution", enabled: false, sharedRoot, lockPath, state, modulesJsonPath });
|
|
995
993
|
const on = await setEnabled({ name: "demo-solution", enabled: true, sharedRoot, lockPath, state, modulesJsonPath });
|
|
@@ -1185,7 +1183,6 @@ describe("P1-16 接线:reconcile 不一致自动修复(重跑 setEnabled)+
|
|
|
1185
1183
|
});
|
|
1186
1184
|
|
|
1187
1185
|
it("operator 通道:runInstall / setEnabled 的 operator 落 market_history(缺省 null 语义不变)", async () => {
|
|
1188
|
-
dbo.exec(MARKET_HISTORY_DDL);
|
|
1189
1186
|
await writeLock(lockPath, { "demo-solution": "0.1.0", "auth": "0.1.0" });
|
|
1190
1187
|
await state.write({ solutions: { "demo-solution": { version: "0.1.0", enabled: true, installedAt: 1000 } } });
|
|
1191
1188
|
await fsp.mkdir(join(sharedRoot, "demo-solution-0.1.0"), { recursive: true });
|
|
@@ -26,9 +26,9 @@ import { readLock, writeLock } from "../../src/_platform/_lifecycle/lock";
|
|
|
26
26
|
import { createStateStore } from "../../src/_platform/_lifecycle/state";
|
|
27
27
|
import { getTask, writeTask } from "../../src/_platform/_lifecycle/tasks";
|
|
28
28
|
import { createTrustStore } from "../../src/_platform/_lifecycle/trust";
|
|
29
|
-
import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
|
|
29
|
+
import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
|
|
30
30
|
import { createMarketApi, marketEnv, parseVersionsJson, versionsJsonPathOf } from "../../src/_platform/market/api";
|
|
31
|
-
import { installDb
|
|
31
|
+
import { installDb } from "../fixture";
|
|
32
32
|
|
|
33
33
|
function hexToBytes(hex: string): Uint8Array {
|
|
34
34
|
const out = new Uint8Array(hex.length / 2);
|
|
@@ -680,7 +680,6 @@ describe("GET index(CDN bundles.yaml no-cache 拉取 + 本地 state 对比)"
|
|
|
680
680
|
});
|
|
681
681
|
|
|
682
682
|
it("operator 回落 history 行(P2-11:最近一次 install 动作的 operator;表缺失/无行 → null 不炸)", async () => {
|
|
683
|
-
dbo.exec(MARKET_HISTORY_DDL);
|
|
684
683
|
dbo.prepare("insert into market_history (solution, version, action, status, operator, message, created_at) values (?, ?, ?, ?, ?, ?, ?)")
|
|
685
684
|
.run("demo-solution", "0.1.0", "install", "done", "alice", "", 1700000000);
|
|
686
685
|
const api = createMarketApi(makeEnv());
|
|
@@ -917,7 +916,6 @@ describe("P1-16 接线:marketEnv 生产口径 / exec opts 转发 / operator
|
|
|
917
916
|
});
|
|
918
917
|
|
|
919
918
|
it("operator 通道:install/enable 端点按 http.user.id 反查 username 落 market_history(uninstall 同款反查)", async () => {
|
|
920
|
-
dbo.exec(MARKET_HISTORY_DDL);
|
|
921
919
|
// 预置「已装」最小形态(enable 用例同款:单条目禁用会触发空锁拒启,S1-1.2)
|
|
922
920
|
await writeLock(join(sharedRoot, "manifests.yaml"), { "demo-solution": "0.1.0", "other": "1.0.0" });
|
|
923
921
|
const state = await createStateStore(join(sharedRoot, ".solutions", "state.yaml"));
|
|
@@ -514,7 +514,7 @@ describe("checkModuleManifest(C2-31 安装期复核纯函数:C2 manifest ↔
|
|
|
514
514
|
const c2 = { name: "demo-solution", version: "0.1.0", summary: "演示方案 Demo — émoji 🎉" };
|
|
515
515
|
const native = { name: "demo-solution", desc: "演示方案 Demo — émoji 🎉", version: "0.1.0" };
|
|
516
516
|
|
|
517
|
-
it("合法:
|
|
517
|
+
it("合法:name/version 三方一致、desc 非空、七键之内 → ok", () => {
|
|
518
518
|
expect(checkModuleManifest(c2, native, "demo-solution", "0.1.0")).toEqual({ ok: true });
|
|
519
519
|
});
|
|
520
520
|
|
|
@@ -535,10 +535,9 @@ describe("checkModuleManifest(C2-31 安装期复核纯函数:C2 manifest ↔
|
|
|
535
535
|
expect(r.ok).toBe(false);
|
|
536
536
|
});
|
|
537
537
|
|
|
538
|
-
it("desc !== summary →
|
|
538
|
+
it("desc !== summary → 放行(等值要求已于 2026-10-21 移除:desc 只是模块描述,非安全边界)", () => {
|
|
539
539
|
const r = checkModuleManifest(c2, { ...native, desc: "另一个描述" }, "demo-solution", "0.1.0");
|
|
540
|
-
expect(r.ok).toBe(
|
|
541
|
-
expect(!r.ok && r.message).toContain("desc");
|
|
540
|
+
expect(r.ok).toBe(true);
|
|
542
541
|
});
|
|
543
542
|
|
|
544
543
|
it("name 或 version 与包身份不一致 → 拒", () => {
|
|
@@ -613,11 +612,9 @@ describe("extract C2-31 安装期复核(B-11:api 段声明与包内原生清
|
|
|
613
612
|
expect(r.message).toContain("C2-31");
|
|
614
613
|
});
|
|
615
614
|
|
|
616
|
-
it("包内原生清单 desc !== summary →
|
|
615
|
+
it("包内原生清单 desc !== summary → 放行(等值校验已移除)", async () => {
|
|
617
616
|
await seedBundle({ nativeManifest: { name: "demo-solution", desc: "另一个描述", version: "0.1.0" } });
|
|
618
617
|
const r = await extract(exCtx({ manifest: c2Manifest(), exec: realTarExec() }));
|
|
619
|
-
expect(r.ok).toBe(
|
|
620
|
-
expect(r.retryable).toBe(false);
|
|
621
|
-
expect(r.message).toContain("C2-31");
|
|
618
|
+
expect(r.ok).toBe(true);
|
|
622
619
|
});
|
|
623
620
|
});
|
|
@@ -11,7 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
13
|
import { createTrustStore, isValidKeyId } from "../../src/_platform/_lifecycle/trust";
|
|
14
|
-
import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
|
|
14
|
+
import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
|
|
15
15
|
import { createMarketApi, marketEnv } from "../../src/_platform/market/api";
|
|
16
16
|
import { installDb } from "../fixture";
|
|
17
17
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// 端点(createMarketApi 的 uninstall-impact / uninstall)只做门禁 + 取参 + 调 runUninstall;
|
|
3
3
|
// 编排器负责四处同删(锁/sidecar/sys_solution_route/modules.json)+ 权限级联 uninstallSolution
|
|
4
4
|
// + 版本目录移除 + 审计 + 全用户失效 + market_history 留痕。本规格为验收锚点。
|
|
5
|
-
// 夹具:本仓范式(node:sqlite 真实迁移建表 + 真实 fs 面 g.fs);market_history
|
|
6
|
-
//
|
|
5
|
+
// 夹具:本仓范式(node:sqlite 真实迁移建表 + 真实 fs 面 g.fs);market_history 表由平台迁移
|
|
6
|
+
// 0012(IF NOT EXISTS)随 installDb 重放建立,守卫语义(表不存在静默跳过)单列一例。
|
|
7
7
|
/* eslint-disable test/prefer-lowercase-title -- 端点动词/路径是验收锚点,保留大写(market-api.spec 同约定) */
|
|
8
8
|
import type { FsFace } from "../../src/_platform/_lifecycle/atomic";
|
|
9
9
|
import type { MarketEnv } from "../../src/_platform/market/api";
|
|
@@ -13,10 +13,10 @@ import { dirname, join } from "node:path";
|
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
14
14
|
import { readLock, writeLock } from "../../src/_platform/_lifecycle/lock";
|
|
15
15
|
import { createStateStore } from "../../src/_platform/_lifecycle/state";
|
|
16
|
-
import {
|
|
17
|
-
import { invalidateAllUsers } from "../../src/_platform/_shared/service/access";
|
|
16
|
+
import { collectRetainedTables, moduleTablePrefix } from "../../src/_platform/_lifecycle/uninstall";
|
|
17
|
+
import { invalidateAllUsers } from "../../src/_platform/_shared/service/infra/access";
|
|
18
18
|
import { createMarketApi } from "../../src/_platform/market/api";
|
|
19
|
-
import { installDb
|
|
19
|
+
import { installDb } from "../fixture";
|
|
20
20
|
|
|
21
21
|
const g = globalThis as any;
|
|
22
22
|
|
|
@@ -67,7 +67,6 @@ async function seedInstalled(opts: { withOther?: boolean } = {}): Promise<void>
|
|
|
67
67
|
{ name: "demo-solution", enabled: true, src: "modules/demo-solution/" },
|
|
68
68
|
...(withOther ? [{ name: "other", enabled: true, src: "modules/other/" }] : []),
|
|
69
69
|
]));
|
|
70
|
-
dbo.exec(MARKET_HISTORY_DDL);
|
|
71
70
|
dbo.prepare("INSERT INTO sys_solution_route (path, module, category) VALUES (?, ?, ?)").run("/demo-solution", "demo-solution", "main");
|
|
72
71
|
dbo.prepare("INSERT INTO sys_role (name, module, title, builtin) VALUES (?, ?, ?, 0)").run("demo-solution.admin", "demo-solution", "管理员");
|
|
73
72
|
dbo.prepare("INSERT INTO sys_role (name, module, title, builtin) VALUES (?, ?, ?, 0)").run("demo-solution.viewer", "demo-solution", "访客");
|
|
@@ -295,92 +294,33 @@ describe("P3-4 业务表保留提示(Q4:不删业务数据,retained 回传
|
|
|
295
294
|
});
|
|
296
295
|
});
|
|
297
296
|
|
|
298
|
-
describe("P4-1
|
|
299
|
-
it("
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const pg = catalogSpec("pg");
|
|
303
|
-
expect(sqlite.existing).toBe("SELECT name FROM sqlite_master WHERE type='table'");
|
|
304
|
-
expect(mysql.existing).toContain("information_schema.tables");
|
|
305
|
-
expect(mysql.existing).toContain("DATABASE()");
|
|
306
|
-
expect(pg.existing).toContain("information_schema.tables");
|
|
307
|
-
expect(pg.existing).toContain("table_schema = 'public'");
|
|
297
|
+
describe("P4-1 卸载业务表保留清单(P3-4#2):moduleTablePrefix + db.listTables 方言无关", () => {
|
|
298
|
+
it("moduleTablePrefix 连字符→下划线作表前缀(listTables 内部按字面量前缀匹配,无需手拼 %)", () => {
|
|
299
|
+
expect(moduleTablePrefix("demo-solution")).toBe("demo_solution_");
|
|
300
|
+
expect(moduleTablePrefix("a!b%c_d")).toBe("a!b%c_d_");
|
|
308
301
|
});
|
|
309
302
|
|
|
310
|
-
it("
|
|
311
|
-
const pat = "demo_solution_%";
|
|
312
|
-
const sqlite = catalogSpec("sqlite").like(pat);
|
|
313
|
-
const mysql = catalogSpec("mysql").like(pat);
|
|
314
|
-
const pg = catalogSpec("pg").like(pat);
|
|
315
|
-
// sqlite
|
|
316
|
-
expect(sqlite[0]).toBe("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ? ESCAPE '!'");
|
|
317
|
-
expect(sqlite[1]).toEqual([pat]);
|
|
318
|
-
// mysql:information_schema + DATABASE() + ESCAPE + 参数化(值不拼字符串,SSRF/注入无关但口径统一)
|
|
319
|
-
expect(mysql[0]).toContain("information_schema.tables");
|
|
320
|
-
expect(mysql[0]).toContain("DATABASE()");
|
|
321
|
-
expect(mysql[0]).toContain("ESCAPE '!'");
|
|
322
|
-
expect(mysql[1]).toEqual([pat]);
|
|
323
|
-
// pg:information_schema + 'public' + ESCAPE + 参数化
|
|
324
|
-
expect(pg[0]).toContain("information_schema.tables");
|
|
325
|
-
expect(pg[0]).toContain("table_schema = 'public'");
|
|
326
|
-
expect(pg[0]).toContain("ESCAPE '!'");
|
|
327
|
-
expect(pg[1]).toEqual([pat]);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("tablePrefixPattern 转义 !%_ 且连字符→下划线作表前缀", () => {
|
|
331
|
-
// 连字符→下划线后,该下划线亦被转义(!_)= 字面分隔符而非 LIKE 通配,避免前缀扫描越权命中
|
|
332
|
-
expect(tablePrefixPattern("demo-solution")).toBe("demo!_solution_%");
|
|
333
|
-
// 危险字符必须转义,避免 LIKE 通配语义被注入(防前缀扫描越权命中)
|
|
334
|
-
expect(tablePrefixPattern("a!b%c_d")).toBe("a!!b!%c!_d_%");
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
describe("P4-1 多方言卸载矩阵(续):collectRetainedTables 经 db.dialect 走 mysql/pg 信息_schema 路径", () => {
|
|
339
|
-
/** 注入指定方言的 db.query 桩:existing 返回全表清单、like 返回前缀命中,模拟 information_schema 行为 */
|
|
340
|
-
function stubDialect(dialect: "mysql" | "pg", existingNames: string[], likeNames: string[]): void {
|
|
341
|
-
const spec = catalogSpec(dialect);
|
|
342
|
-
(g.db as { dialect: () => string, query: (sql: string, p?: unknown[]) => unknown }).dialect = () => dialect;
|
|
343
|
-
(g.db as { query: (sql: string, p?: unknown[]) => unknown }).query = (sql: string) => {
|
|
344
|
-
if (sql === spec.existing)
|
|
345
|
-
return existingNames.map(name => ({ name }));
|
|
346
|
-
if (sql.includes("AND table_name LIKE"))
|
|
347
|
-
return likeNames.map(name => ({ name }));
|
|
348
|
-
return [];
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
it("mysql 方言:manifest 声明 + 前缀兜底扫描均经 information_schema,返回实际存在的业务表", async () => {
|
|
303
|
+
it("manifest 声明 + 前缀兜底经 db.listTables 合并,仅回传实际存在的业务表", async () => {
|
|
353
304
|
await seedInstalledWithTables();
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
expect(retained).toContain("demo_solution_orphan");
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("db.dialect 缺省回落 sqlite(不抛、不误导),与既有 sqlite 全量路径一致", async () => {
|
|
371
|
-
await seedInstalledWithTables();
|
|
372
|
-
// 还原为缺省(不提供 dialect 函数)→ currentDialect 回落 "sqlite"
|
|
373
|
-
const spec = catalogSpec("sqlite");
|
|
374
|
-
(g.db as { dialect?: () => string }).dialect = undefined;
|
|
375
|
-
(g.db as { query: (sql: string, p?: unknown[]) => unknown }).query = (sql: string) => {
|
|
376
|
-
if (sql === spec.existing)
|
|
377
|
-
return [{ name: "demo_solution_notes" }, { name: "demo_solution_tags" }];
|
|
378
|
-
if (sql.includes("AND name LIKE"))
|
|
379
|
-
return [{ name: "demo_solution_notes" }];
|
|
380
|
-
return [];
|
|
305
|
+
// manifest 追加「声明了但没建表」的幽灵条目,验证存在性过滤
|
|
306
|
+
const manifestPath = join(sharedRoot, "demo-solution-0.1.0", "manifest.yaml");
|
|
307
|
+
await fsp.writeFile(manifestPath, `${await fsp.readFile(manifestPath, "utf-8")} - demo_solution_ghost\n`);
|
|
308
|
+
// 真实 sqlite 目录:manifest 声明 notes/tags(seed 已建)+ 前缀扫描命中 orphan(未声明)
|
|
309
|
+
dbo.exec("CREATE TABLE demo_solution_orphan (id INTEGER)");
|
|
310
|
+
// L2:listTables 是 oj 运行时能力(纯 Rust 二进制内,vitest 无载体,见 devkit
|
|
311
|
+
// oj-test-guide.md)——桩回放**真实 sqlite 目录**(sqlite_master 实查)并记录
|
|
312
|
+
// 调用形态;被测的是合并/过滤编排,不是引擎的目录自省本身。
|
|
313
|
+
const seen: Array<string | undefined> = [];
|
|
314
|
+
g.db.listTables = async (prefix?: string) => {
|
|
315
|
+
seen.push(prefix);
|
|
316
|
+
const all = (dbo.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{ name: string }>).map(r => r.name);
|
|
317
|
+
return prefix === undefined ? all : all.filter(t => t.startsWith(prefix));
|
|
381
318
|
};
|
|
382
|
-
const retained = await collectRetainedTables("demo-solution",
|
|
383
|
-
expect(
|
|
384
|
-
expect(retained).toContain("
|
|
319
|
+
const retained = await collectRetainedTables("demo-solution", manifestPath);
|
|
320
|
+
expect(seen).toEqual(["demo_solution_", undefined]); // 前缀扫描 + 全量存在性过滤各一次
|
|
321
|
+
expect(retained).toContain("demo_solution_notes"); // manifest 声明
|
|
322
|
+
expect(retained).toContain("demo_solution_tags"); // manifest 声明
|
|
323
|
+
expect(retained).toContain("demo_solution_orphan"); // 前缀兜底命中(未声明)
|
|
324
|
+
expect(retained).not.toContain("demo_solution_ghost"); // 声明但物理不存在 → 不回传
|
|
385
325
|
});
|
|
386
326
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
4
4
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
5
5
|
|
|
6
|
-
import { likePattern, paged } from "../src/_platform/_shared/service/query";
|
|
6
|
+
import { likePattern, paged, pagedFactory } from "../src/_platform/_shared/service/infra/query";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -66,3 +66,87 @@ describe("AC1-22:LIKE 模式与 keyword 长度红线", () => {
|
|
|
66
66
|
expect(likePattern("张三")).toBe("%张三%");
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
describe("AC1-24(构造器工厂):pagedFactory 复用同一组条件", () => {
|
|
71
|
+
// L2 记录型假构造器:pagedFactory 是纯编排(count 聚合 + 条件分页 + 截断判定),
|
|
72
|
+
// SQL 渲染/执行在 oj 二进制内,vitest 无真 builder 载体(架构定论:builder 纯 Rust
|
|
73
|
+
// 实现,无 JS 模块可导;SQL 断言走 oj test / oj exec,见 devkit oj-test-guide.md)。
|
|
74
|
+
// 这里只断言编排契约:mk 各跑一次、count 走聚合 select、total=0 不跑 list。
|
|
75
|
+
function fakeBuilder(total: number, rows: any[]) {
|
|
76
|
+
const calls: string[] = [];
|
|
77
|
+
let countPath = false;
|
|
78
|
+
const b: any = {
|
|
79
|
+
select: (cols: any[]) => {
|
|
80
|
+
countPath = cols.some((c: any) => c && typeof c === "object" && c.fn === "count");
|
|
81
|
+
calls.push(`select:${JSON.stringify(cols)}`);
|
|
82
|
+
return b;
|
|
83
|
+
},
|
|
84
|
+
where: (cond: any) => {
|
|
85
|
+
calls.push(`where:${JSON.stringify(cond)}`);
|
|
86
|
+
return b;
|
|
87
|
+
},
|
|
88
|
+
orderBy: (items: any[]) => {
|
|
89
|
+
calls.push(`orderBy:${JSON.stringify(items)}`);
|
|
90
|
+
return b;
|
|
91
|
+
},
|
|
92
|
+
limit: (n: number) => {
|
|
93
|
+
calls.push(`limit:${n}`);
|
|
94
|
+
return b;
|
|
95
|
+
},
|
|
96
|
+
offset: (n: number) => {
|
|
97
|
+
calls.push(`offset:${n}`);
|
|
98
|
+
return b;
|
|
99
|
+
},
|
|
100
|
+
all: async () => (countPath ? [{ c: total }] : rows),
|
|
101
|
+
};
|
|
102
|
+
return { b, calls };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
it("count + list 各跑一次:count 走聚合 select(不受 LIMIT 钳制),list 带 orderBy/limit/offset", async () => {
|
|
106
|
+
const built: Array<{ calls: string[] }> = [];
|
|
107
|
+
const mk = () => {
|
|
108
|
+
const f = fakeBuilder(301, Array.from({ length: 200 }, (_, i) => ({ id: 300 - i })));
|
|
109
|
+
built.push(f);
|
|
110
|
+
return f.b;
|
|
111
|
+
};
|
|
112
|
+
const r = await pagedFactory<any>(mk, { page: 1, size: 500 });
|
|
113
|
+
expect(built).toHaveLength(2);
|
|
114
|
+
expect(built[0].calls.some(c => c.startsWith("select:") && c.includes("\"count\""))).toBe(true);
|
|
115
|
+
expect(built[1].calls.includes("limit:200")).toBe(true); // 500 被钳 200
|
|
116
|
+
expect(built[1].calls.includes("offset:0")).toBe(true);
|
|
117
|
+
expect(r.total).toBe(301);
|
|
118
|
+
expect(r.size).toBe(200);
|
|
119
|
+
expect(r.list.length).toBe(200);
|
|
120
|
+
expect(r.truncated).toBe(true); // 301 > 1*200
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("条件经 mk 工厂收敛(contains 替代 LIKE ? ESCAPE '!'):两次构造都带上 where", async () => {
|
|
124
|
+
const built: Array<{ calls: string[] }> = [];
|
|
125
|
+
const mk = () => {
|
|
126
|
+
const f = fakeBuilder(1, [{ id: 7, username: "contains_me" }]);
|
|
127
|
+
f.b.where({ field: "username", op: "contains", value: "contains" });
|
|
128
|
+
built.push(f);
|
|
129
|
+
return f.b;
|
|
130
|
+
};
|
|
131
|
+
const r = await pagedFactory<any>(mk, { size: 10 });
|
|
132
|
+
expect(built).toHaveLength(2);
|
|
133
|
+
for (const f of built)
|
|
134
|
+
expect(f.calls.some(c => c.startsWith("where:") && c.includes("contains"))).toBe(true);
|
|
135
|
+
expect(r.total).toBe(1);
|
|
136
|
+
expect(r.truncated).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("total=0 → 不跑 list 查询(省一次往返),缺省 page=1/size=20", async () => {
|
|
140
|
+
const built: Array<{ calls: string[] }> = [];
|
|
141
|
+
const mk = () => {
|
|
142
|
+
const f = fakeBuilder(0, []);
|
|
143
|
+
built.push(f);
|
|
144
|
+
return f.b;
|
|
145
|
+
};
|
|
146
|
+
const r = await pagedFactory<any>(mk, {});
|
|
147
|
+
expect(built).toHaveLength(1); // 只跑了 count
|
|
148
|
+
expect(r.list).toEqual([]);
|
|
149
|
+
expect(r.truncated).toBe(false);
|
|
150
|
+
expect(r.size).toBe(20);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
4
4
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
|
-
import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/_platform/_shared/service/audit";
|
|
6
|
+
import { auditDeny, auditOp, recordOp, resetDenyAudit } from "../src/_platform/_shared/service/infra/audit";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { contactDetail, searchContacts } from "../src/_platform/_shared/service/contacts";
|
|
7
|
+
import { contactDetail, searchContacts } from "../src/_platform/_shared/service/domain/contacts";
|
|
8
8
|
import { installDb } from "./fixture";
|
|
9
9
|
|
|
10
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -74,7 +74,7 @@ describe("详情与限速", () => {
|
|
|
74
74
|
it("接口级限速:同用户突发超限 → 429", async () => {
|
|
75
75
|
const uid = mkUser("c_rate");
|
|
76
76
|
g.http = { user: { id: uid } };
|
|
77
|
-
const { rateLimit } = await import("../src/_platform/_shared/service/rate");
|
|
77
|
+
const { rateLimit } = await import("../src/_platform/_shared/service/infra/rate");
|
|
78
78
|
// 窗口 60s / 限额 3(测试参数;生产端点各自定额)
|
|
79
79
|
for (let i = 0; i < 3; i++)
|
|
80
80
|
await rateLimit(`contacts:${uid}`, 3, 60_000);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { assertPermission, assertSuperAdmin, assertSystemAdmin, invalidateUser } from "../src/_platform/_shared/service/access";
|
|
7
|
+
import { assertPermission, assertSuperAdmin, assertSystemAdmin, invalidateUser } from "../src/_platform/_shared/service/infra/access";
|
|
8
8
|
import { installDb } from "./fixture";
|
|
9
9
|
|
|
10
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { createRole, deleteRole, listPermissions, listRoles, updateBindings } from "../src/_platform/_shared/service/domain/roles";
|
|
8
|
+
import { assertPermission } from "../src/_platform/_shared/service/infra/access";
|
|
9
9
|
import { installDb } from "./fixture";
|
|
10
10
|
|
|
11
11
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
3
3
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { listLoginMethods, listSettings, updateSetting } from "../src/_platform/_shared/service/domain/settings";
|
|
6
|
+
import { searchOplog } from "../src/_platform/_shared/service/infra/oplog";
|
|
7
7
|
import { installDb } from "./fixture";
|
|
8
8
|
|
|
9
9
|
let dbo: ReturnType<typeof installDb>;
|
|
10
10
|
const g = globalThis as any;
|
|
11
11
|
|
|
12
|
-
function mkOplog(username: string, action: string, module: string, level = 6) {
|
|
13
|
-
dbo.prepare("INSERT INTO sys_oplog (create_time, user_id, username, action, module, level) VALUES (?, 1, ?, ?, ?, ?)")
|
|
14
|
-
.run(1000, username, action, module, level);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
12
|
beforeAll(() => {
|
|
18
13
|
dbo = installDb();
|
|
19
14
|
});
|
|
@@ -25,22 +20,72 @@ afterEach(() => {
|
|
|
25
20
|
});
|
|
26
21
|
|
|
27
22
|
describe("AC1-29/D13:操作日志查询(admin 可查)", () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
// L2:searchOplog 的本职是「过滤维度 → whereIf 条件」的映射 + 分页复用(pagedFactory);
|
|
24
|
+
// 构造器执行(SQL 渲染 / 真过滤)在 oj 二进制内——vitest 无真 builder 载体(架构定论:
|
|
25
|
+
// builder 纯 Rust 实现、无 JS 模块可导;SQL 断言走 oj test / oj exec,见 devkit
|
|
26
|
+
// oj-test-guide.md)。这里用记录型假构造器断言条件发射;whereIf 的「假值/空数组跳过」
|
|
27
|
+
// 按 api-manual §1153 契约建模(引擎语义的忠实复刻,非新逻辑)。
|
|
28
|
+
function fakeTable() {
|
|
29
|
+
const conds: Array<{ test: unknown, cond: any }> = [];
|
|
30
|
+
const b: any = {
|
|
31
|
+
whereIf: (test: unknown, cond: any) => {
|
|
32
|
+
if (test !== undefined && test !== null && test !== false && !(Array.isArray(test) && test.length === 0))
|
|
33
|
+
conds.push({ test, cond });
|
|
34
|
+
return b;
|
|
35
|
+
},
|
|
36
|
+
select: () => b,
|
|
37
|
+
orderBy: () => b,
|
|
38
|
+
limit: () => b,
|
|
39
|
+
offset: () => b,
|
|
40
|
+
// 固定非零 total:保证 count/list 两条路径都构造(total=0 跳过 list 的行为
|
|
41
|
+
// 由 query-utils.spec 的 pagedFactory 用例覆盖,这里只关心条件发射)
|
|
42
|
+
all: async () => [{ c: 42 }],
|
|
43
|
+
};
|
|
44
|
+
return { b, conds };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 以假构造器跑 searchOplog,回传 count 构造发射的条件(并校验 count/list 同条件) */
|
|
48
|
+
async function captureConds(filter: Parameters<typeof searchOplog>[0]) {
|
|
49
|
+
const built: Array<ReturnType<typeof fakeTable>> = [];
|
|
50
|
+
const prevDb = g.db;
|
|
51
|
+
g.db = {
|
|
52
|
+
table: () => {
|
|
53
|
+
const t = fakeTable();
|
|
54
|
+
built.push(t);
|
|
55
|
+
return t.b;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
await searchOplog(filter);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
g.db = prevDb;
|
|
63
|
+
}
|
|
64
|
+
expect(built).toHaveLength(2); // count + list 各构造一次
|
|
65
|
+
expect(built[1].conds).toEqual(built[0].conds); // mk 工厂:两组条件一致
|
|
66
|
+
return built[0].conds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it("无过滤/空串过滤 → 不发射任何条件(归一化空串 → whereIf 跳过,等价旧 if (filter.x))", async () => {
|
|
70
|
+
expect(await captureConds({})).toEqual([]);
|
|
71
|
+
expect(await captureConds({ action: "", username: "", module: "" })).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("action/module 精确 + username 包含:三维度各发射一条,字段与 op 对应", async () => {
|
|
75
|
+
const conds = await captureConds({ action: "user.create", module: "framework", username: "bo" });
|
|
76
|
+
expect(conds).toEqual([
|
|
77
|
+
{ test: "user.create", cond: { field: "action", op: "eq", value: "user.create" } },
|
|
78
|
+
{ test: "framework", cond: { field: "module", op: "eq", value: "framework" } },
|
|
79
|
+
{ test: "bo", cond: { field: "username", op: "contains", value: "bo" } },
|
|
80
|
+
]);
|
|
38
81
|
});
|
|
39
82
|
|
|
40
|
-
it("403 拒绝类(auth.deny
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
83
|
+
it("403 拒绝类(auth.deny)可在日志中检索:按 action 精确命中,且从无 level 条件(读不过滤等级)", async () => {
|
|
84
|
+
const conds = await captureConds({ action: "auth.deny" });
|
|
85
|
+
expect(conds).toEqual([
|
|
86
|
+
{ test: "auth.deny", cond: { field: "action", op: "eq", value: "auth.deny" } },
|
|
87
|
+
]);
|
|
88
|
+
expect(conds.some(c => (c.cond as { field?: string }).field === "level")).toBe(false);
|
|
44
89
|
});
|
|
45
90
|
});
|
|
46
91
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/* eslint-disable test/prefer-lowercase-title -- AC 编号是验收锚点,保留大写 */
|
|
5
5
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { uninstallSolution, upsertSolutionPerms } from "../src/_platform/_shared/service/solution-perms";
|
|
7
|
+
import { uninstallSolution, upsertSolutionPerms } from "../src/_platform/_shared/service/domain/solution-perms";
|
|
8
8
|
import { installDb } from "./fixture";
|
|
9
9
|
|
|
10
10
|
let dbo: ReturnType<typeof installDb>;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { SessionDeps } from "../src/auth/_shared/session";
|
|
7
7
|
|
|
8
8
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
9
|
-
import { changePasswordByAdmin, createUser, listUsers, setForbidden, updateUserRoles } from "../src/_platform/_shared/service/users";
|
|
9
|
+
import { changePasswordByAdmin, createUser, listUsers, setForbidden, updateUserRoles } from "../src/_platform/_shared/service/domain/users";
|
|
10
10
|
import { createSession } from "../src/auth/_shared/session";
|
|
11
11
|
import { installDb } from "./fixture";
|
|
12
12
|
|