Files
shrimp-server/test/buildShrimpRouter.test.ts
wc24 ca283c6733 v0.5.0: 重新组织目录结构,按职责拆分
- Shrimp.ts (875 行) 按类拆分到 Shrimp/(7 文件: Shrimp, ShrimpModule, ShrimpTableModule, ShrimpPower, ShrimpCrud, UserRoleModule, OperationLogModule)
- forMongo/: MongoShrimpCrud + MongoShrimp(原 ShrimpServer 的 mongo 装配部分,无 express)
- forExpress/: buildShrimpRouter + jwtAuth + types
- main/: index(0 依赖核心) + expmon(express+mongo 整合,导出 ShrimpServer) + types(纯协议)
- 入口子路径 server → expmon(express+mongo 整合方案命名更准确)
- 删除冗余的 index.ts / core.ts(合并为 main/index.ts)
2026-05-23 19:44:02 +08:00

162 lines
5.3 KiB
TypeScript

/**
* buildShrimpRouter 独立装配冒烟测试
*
* 验证 buildShrimpRouter 不依赖 ShrimpServer 类,消费方完全可以:
* - 用 MemoryShrimpCrud(而非 MongoShrimpCrud)
* - 自己造 ITokenAuth(而非默认 JWT)
* - 自己实现 IUserAccessor
* 然后拼出可工作的 Express Router。
*
* 这是"内部解耦"的核心契约 — 如果哪天 buildShrimpRouter 偷偷耦合回
* ShrimpServer / mongoose / jsonwebtoken,本测试会编译失败或运行失败。
*/
import { describe, it, expect } from "vitest"
import express from "express"
import {
Shrimp,
MemoryShrimpCrud,
UserModule,
RoleModule,
OperationLogModule,
plainHasher,
} from "../src/main"
import { buildShrimpRouter } from "../src/forExpress/buildShrimpRouter"
import type { ITokenAuth, IUserAccessor } from "../src/forExpress/types"
import type { WithId, IUser, IRole } from "../src/main"
function makeFakeTokenAuth(): ITokenAuth {
const store = new Map<string, Record<string, any>>()
let seq = 0
return {
sign(payload) {
const tok = `tok-${++seq}`
store.set(tok, payload)
return tok
},
verify(token) {
const payload = store.get(token)
if (!payload) throw new Error("invalid token")
return payload
},
}
}
const userAccessor: IUserAccessor = {
getJwtPayload: (user) => ({ _id: user._id }),
isUserActive: (user) => user.active === true,
getUserRoleIds: (user) =>
Array.isArray(user.roleId) ? user.roleId.map(String) : user.roleId ? [String(user.roleId)] : [],
isRoleSuperAdmin: (role) => !!role?.isSuperAdmin,
getRolePowers: (role) => role?.powers || [],
getUserDisplayName: (user) => user.nickname || "",
}
async function makeApp() {
Shrimp.passwordHasher = plainHasher
const shrimp = new Shrimp()
const userCrud = new MemoryShrimpCrud<WithId<IUser>>()
const roleCrud = new MemoryShrimpCrud<WithId<IRole>>()
const logCrud = new MemoryShrimpCrud<any>()
const userModule = new UserModule(shrimp, userCrud)
const roleModule = new RoleModule(shrimp, roleCrud)
const operationLogModule = new OperationLogModule(userModule, logCrud)
const adminRole = await roleCrud.add({
name: "admin",
description: "",
isSuperAdmin: true,
powers: [],
} as any)
await userModule.add({
username: "alice",
password: "secret",
nickname: "Alice",
roleId: [adminRole._id],
active: true,
} as any)
const tokenAuth = makeFakeTokenAuth()
const router = buildShrimpRouter({
shrimp,
userModule,
roleModule,
operationLogModule,
tokenAuth,
config: { title: "smoke" },
globalOptions: {},
forcedOptionAccess: new Map(),
userAccessor,
})
const app = express()
app.use(express.json())
app.use(router)
return app
}
async function jsonFetch(app: express.Express, method: string, path: string, body?: any, headers: Record<string, string> = {}) {
return new Promise<{ status: number; body: any }>((resolve, reject) => {
const server = app.listen(0, () => {
const port = (server.address() as any).port
const data = body == null ? undefined : JSON.stringify(body)
const req = require("http").request(
{
method,
host: "127.0.0.1",
port,
path,
headers: {
"content-type": "application/json",
...(data ? { "content-length": Buffer.byteLength(data) } : {}),
...headers,
},
},
(res: any) => {
let buf = ""
res.on("data", (c: any) => (buf += c))
res.on("end", () => {
server.close()
try {
resolve({ status: res.statusCode, body: JSON.parse(buf) })
} catch (e) {
reject(new Error("non-JSON body: " + buf))
}
})
}
)
req.on("error", reject)
if (data) req.write(data)
req.end()
})
})
}
describe("buildShrimpRouter 独立装配", () => {
it("可以用纯内存 CRUD + 假 token 鉴权拼出可工作的 Router", async () => {
const app = await makeApp()
// /config 不需要鉴权
const cfg = await jsonFetch(app, "GET", "/config")
expect(cfg.body.data).toEqual({ title: "smoke" })
// /login 用注入的 IUserAccessor + fake tokenAuth
const login = await jsonFetch(app, "POST", "/login", { username: "alice", password: "secret" })
expect(login.body.code).toBe(0)
const token = login.body.data.token
expect(typeof token).toBe("string")
// 无 token 拒绝
const noTok = await jsonFetch(app, "GET", "/me")
expect(noTok.body.code).not.toBe(0)
// 有 token 通过
const me = await jsonFetch(app, "GET", "/me", undefined, { authorization: `Bearer ${token}` })
expect(me.body.code).toBe(0)
expect(me.body.data.nickname).toBe("Alice")
})
})