- 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)
162 lines
5.3 KiB
TypeScript
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")
|
|
})
|
|
})
|