Files
aca-classroom-service/smoke/smoke.mjs
T
wanghu 0e75efa023
build / build-push (push) Failing after 2m7s
commit
2026-10-20 09:13:14 +08:00

281 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 教室管理后端冒烟脚本(Node 18+,无第三方依赖)。
*
* 用法:
* node smoke/smoke.mjs # 无认证模式(本地 YH_SSO_URLPATTERN=/none/* + ACACLASSROOM_DEVCONTEXT_ENABLED=true)
* TEACHER_COOKIE="CASTGC=..." node smoke/smoke.mjs # 真实 SSO 会话(浏览器复制 Cookie)
*
* 覆盖:健康检查 → 基础/校区/类型/楼栋/教室 全链路(自建数据 → 断言 → 逆序清理)。
*/
const BASE = process.env.SMOKE_BASE || "http://localhost:8106";
const COOKIE = process.env.TEACHER_COOKIE || "";
const headers = { ...(COOKIE ? { Cookie: COOKIE } : {}) };
let passed = 0, failed = 0;
const ok = (name, cond, detail = "") => {
if (cond) { passed++; console.log(` ✓ ${name}`); }
else { failed++; console.error(` ✗ ${name} ${detail}`); }
};
async function api(method, path, { form, json, params } = {}) {
const url = new URL(BASE + path);
if (params) Object.entries(params).forEach(([k, v]) => v != null && url.searchParams.set(k, v));
const opt = { method, headers: { ...headers } };
if (json != null) {
opt.headers["Content-Type"] = "application/json;charset=UTF-8";
opt.body = JSON.stringify(json);
} else if (form != null) {
opt.headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
opt.body = new URLSearchParams(form).toString();
}
const res = await fetch(url, opt);
const text = await res.text();
try { return JSON.parse(text); } catch { return { code: -1, msg: text.substring(0, 120) }; }
}
console.log(`[smoke] target=${BASE} auth=${COOKIE ? "cookie" : "none(dev-context)"}`);
// 1. 健康
const health = await fetch(`${BASE}/actuator/health`).then(r => r.json()).catch(() => null);
ok("actuator/health UP", health && health.status === "UP");
// 2. 基础
const info = await api("GET", "/api/base/GetUserInfo");
ok("GetUserInfo code=0", info.code === 0, JSON.stringify(info).substring(0, 80));
const mods = await api("GET", "/api/base/GetUserMod");
ok("GetUserMod code=0", mods.code === 0);
// 3. 校区
const stamp = Date.now().toString(36);
const campusName = `冒烟校区${stamp}`;
const cAdd = await api("POST", "/api/campus/Add", { form: { campusName, sortOrder: 99 } });
ok("campus Add", cAdd.code === 0 && cAdd.data > 0, cAdd.msg);
const campusId = cAdd.data;
const cDup = await api("POST", "/api/campus/Add", { form: { campusName } });
ok("campus Add 重名拒绝", cDup.code !== 0);
const cList = await api("GET", "/api/campus/List");
ok("campus List 含新校区", cList.code === 0 && cList.data.some(c => c.serID === campusId));
// 4. 教室类型
const typeName = `冒烟类型${stamp}`;
const tAdd = await api("POST", "/api/classtype/Add", { form: { typeName } });
ok("classtype Add", tAdd.code === 0, tAdd.msg);
const typeId = tAdd.data;
// 5. 楼栋
const bAdd = await api("POST", "/api/building/Add", { form: { campusId, buildingName: `冒烟楼栋${stamp}`, buildingCode: `S${stamp.slice(-3)}`, floorCount: 2 } });
ok("building Add", bAdd.code === 0, bAdd.msg);
const buildingId = bAdd.data;
// 6. 教室批量添加(带设备)+ 列表
const eqSeed = await api("POST", "/api/equipment/Add", { form: { equipmentName: `冒烟设备${stamp}` } });
ok("equipment Add(独立台账)", eqSeed.code === 0, eqSeed.msg);
const eqSeedId = eqSeed.data;
const eqSeedDup = await api("POST", "/api/equipment/Add", { form: { equipmentName: `冒烟设备${stamp}` } });
ok("equipment Add 校内重名拒绝", eqSeedDup.code !== 0);
const batch = await api("POST", "/api/classroom/BatchAdd", { json: { buildingId, floorFrom: 1, floorTo: 2, countPerFloor: 3, typeId, equipments: [{ equipmentId: eqSeedId, quantity: 2 }] } });
ok("classroom BatchAdd 6 间(带设备,每间数量 2)", batch.code === 0 && batch.data.created === 6, batch.msg);
const batchBad = await api("POST", "/api/classroom/BatchAdd", { json: { buildingId, floorFrom: 1, floorTo: 9, countPerFloor: 1, typeId } });
ok("BatchAdd 楼层超限拒绝", batchBad.code !== 0);
const rooms = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
ok("classroom List 6 间", rooms.code === 0 && rooms.data.total === 6, `total=${rooms.data && rooms.data.total}`);
const room1 = rooms.data.list[0].serID;
ok("教室行含设备关联(数量 2,名称×2)", rooms.data.list.every(r => Array.isArray(r.equipmentIds) && r.equipmentIds.includes(eqSeedId) && r.equipmentQtys && r.equipmentQtys[eqSeedId] === 2 && /冒烟设备.*×2$/.test(r.equipmentNames)), JSON.stringify(rooms.data.list[0] && rooms.data.list[0].equipmentNames));
const floors = await api("GET", "/api/classroom/Floors", { params: { buildingId } });
ok("classroom Floors [1,2]", floors.code === 0 && JSON.stringify(floors.data) === "[1,2]");
const bStats = await api("GET", "/api/building/Stats", { params: { campusId } });
ok("building Stats total=6", bStats.code === 0 && bStats.data.some(b => b.serID === buildingId && b.total === 6));
// 7. 教室编辑(含设备全量替换)+ 编号守卫
const edit = await api("POST", "/api/classroom/Edit", { json: { serID: room1, roomName: `冒烟教室${stamp}`, status: "closed", capacity: 50, seatCount: 45, equipmentIds: [] } });
ok("classroom Edit(清空设备)", edit.code === 0, edit.msg);
const roomsAfterEdit = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
const room1After = roomsAfterEdit.data.list.find(r => r.serID === room1);
ok("Edit 后教室设备已清空", room1After && room1After.equipmentIds.length === 0);
const editQty = await api("POST", "/api/classroom/Edit", { json: { serID: room1, equipments: [{ equipmentId: eqSeedId, quantity: 3 }] } });
ok("classroom Edit equipments 按数量替换", editQty.code === 0, editQty.msg);
const roomsAfterQty = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
const room1Qty = roomsAfterQty.data.list.find(r => r.serID === room1);
ok("Edit 后设备数量为 3(名称×3)", room1Qty && room1Qty.equipmentQtys && room1Qty.equipmentQtys[eqSeedId] === 3 && /冒烟设备.*×3$/.test(room1Qty.equipmentNames), JSON.stringify(room1Qty && room1Qty.equipmentQtys));
const editClear = await api("POST", "/api/classroom/Edit", { json: { serID: room1, equipmentIds: [] } });
ok("classroom Edit 旧字段 equipmentIds 清空设备(兼容)", editClear.code === 0, editClear.msg);
const room2code = roomsAfterEdit.data.list[1].roomCode;
const editDup = await api("POST", "/api/classroom/Edit", { json: { serID: roomsAfterEdit.data.list[1].serID, roomCode: roomsAfterEdit.data.list[2].roomCode } });
ok("Edit 楼栋内编号重复拒绝", editDup.code !== 0);
// 8. 设备编辑 + 引用删除守卫(设备被教室引用时不可删)
const eqList = await api("GET", "/api/equipment/List");
ok("equipment List 含引用数与统计数量(5 间 × 每间 2 = 10)", eqList.code === 0 && eqList.data.list.some(e => e.serID === eqSeedId && e.classroomCount === 5 && e.quantity === 10), JSON.stringify(eqList.data && eqList.data.list.find(e => e.serID === eqSeedId)));
const eqEdit = await api("POST", "/api/equipment/Edit", { form: { serID: eqSeedId, equipmentName: `冒烟设备${stamp}` } });
ok("equipment Edit", eqEdit.code === 0, eqEdit.msg);
const eqDelGuarded = await api("POST", "/api/equipment/Delete", { form: { serid: eqSeedId } });
ok("equipment Delete 被引用守卫拒绝", eqDelGuarded.code !== 0);
// 9. 新增接口:列表排序 / 楼栋楼层分布 / 教室批量编辑
const sortedDesc = await api("GET", "/api/classroom/List", {
params: { buildingId, page: 1, count: 20, sortField: "capacity", order: "desc" },
});
ok(
"List 排序 capacity desc",
sortedDesc.code === 0 &&
sortedDesc.data.list[0].capacity >= sortedDesc.data.list[sortedDesc.data.list.length - 1].capacity,
JSON.stringify(sortedDesc.data && sortedDesc.data.list.map(r => r.capacity))
);
const sortedFloorDesc = await api("GET", "/api/classroom/List", {
params: { buildingId, page: 1, count: 20, sortField: "floor", order: "desc" },
});
ok("List 排序 floor desc 首行为 2 层", sortedFloorDesc.code === 0 && sortedFloorDesc.data.list[0].floor === 2);
const defaultOrder = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
ok(
"List 不传排序参数保持默认序(楼层升序 + 编号升序)",
defaultOrder.code === 0 &&
defaultOrder.data.list[0].floor === 1 &&
defaultOrder.data.list[0].roomCode <= defaultOrder.data.list[1].roomCode
);
const bStatsFloors = await api("GET", "/api/building/Stats", { params: { campusId } });
const mineBuilding = bStatsFloors.data && bStatsFloors.data.find(b => b.serID === buildingId);
ok(
"building Stats 新增 floors 楼层分布(2 层,每层 3 间)",
mineBuilding &&
Array.isArray(mineBuilding.floors) &&
mineBuilding.floors.length === 2 &&
mineBuilding.floors[0].floor === 1 &&
mineBuilding.floors[0].total === 3 &&
mineBuilding.floors[1].floor === 2,
JSON.stringify(mineBuilding && mineBuilding.floors)
);
// 9.5 空间作用域查询(教室管理三栏布局):按校区 List + Tree 扁平数据
const byCampus = await api("GET", "/api/classroom/List", { params: { campusId, page: 1, count: 20 } });
ok(
"List 按校区作用域命中 6 间",
byCampus.code === 0 && byCampus.data.total === 6,
`total=${byCampus.data && byCampus.data.total}`
);
ok(
"List 行含校区/楼栋名称",
byCampus.data.list.every(r => r.campusName === campusName && r.buildingName === `冒烟楼栋${stamp}`),
JSON.stringify(byCampus.data.list[0])
);
const byCampusFloor = await api("GET", "/api/classroom/List", { params: { campusId, floor: 1, page: 1, count: 20 } });
ok("List 校区+楼层作用域命中 3 间", byCampusFloor.code === 0 && byCampusFloor.data.total === 3);
ok("List 校区/楼栋均缺拒绝", (await api("GET", "/api/classroom/List", { params: { page: 1, count: 20 } })).code !== 0);
const treeRows = await api("GET", "/api/classroom/Tree");
const myTreeRows = treeRows.data ? treeRows.data.filter(r => r.buildingId === buildingId) : [];
ok(
"Tree 扁平数据含本楼栋 6 间(轻量字段)",
treeRows.code === 0 && myTreeRows.length === 6 && myTreeRows.every(r => r.campusId === campusId && r.roomCode && r.status),
JSON.stringify(myTreeRows[0])
);
// 9.7 绑定班级:class 档案 → BindClass/UnbindClass/BatchUnbind → 年级 Tab 过滤
const clsNameA = `冒烟高一班${stamp}`;
const clsNameB = `冒烟高二班${stamp}`;
const clsAddA = await api("POST", "/api/class/Add", { form: { className: clsNameA, grade: 1, studentCount: 45, headTeacher: "王老师" } });
ok("class Add(高一)", clsAddA.code === 0 && clsAddA.data > 0, clsAddA.msg);
const clsAddB = await api("POST", "/api/class/Add", { form: { className: clsNameB, grade: 2, studentCount: 44, headTeacher: "陈老师" } });
ok("class Add(高二)", clsAddB.code === 0 && clsAddB.data > 0, clsAddB.msg);
ok("class Add 校内重名拒绝", (await api("POST", "/api/class/Add", { form: { className: clsNameA, grade: 1 } })).code !== 0);
ok("class Add 非法年级拒绝", (await api("POST", "/api/class/Add", { form: { className: `冒烟班X${stamp}`, grade: 9 } })).code !== 0);
const clsListG1 = await api("GET", "/api/class/List", { params: { grade: 1 } });
const clsA = clsListG1.data ? clsListG1.data.find(c => c.serID === clsAddA.data) : null;
ok("class List grade=1 含新班级(未绑定 classroomId=null)", clsListG1.code === 0 && clsA && clsA.classroomId == null && clsA.headTeacher === "王老师", JSON.stringify(clsA));
const bind = await api("POST", "/api/classroom/BindClass", { json: { serID: room1, classId: clsAddA.data } });
ok("classroom BindClass 返回班级名", bind.code === 0 && bind.data && bind.data.className === clsNameA, bind.msg);
const bindDup = await api("POST", "/api/classroom/BindClass", { json: { serID: rooms.data.list[1].serID, classId: clsAddA.data } });
ok("BindClass 班级已被其他教室占用拒绝", bindDup.code !== 0);
ok("BindClass 缺参拒绝", (await api("POST", "/api/classroom/BindClass", { json: { serID: room1 } })).code !== 0);
const roomsG1 = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 1, page: 1, count: 20 } });
const boundRow = roomsG1.data ? roomsG1.data.list.find(r => r.serID === room1) : null;
ok(
"List grade=1 含已绑教室且行含班级信息(名/人数/班主任)",
roomsG1.code === 0 && roomsG1.data.total === 6 && boundRow && boundRow.classId === clsAddA.data &&
boundRow.className === clsNameA && boundRow.studentCount === 45 && boundRow.headTeacher === "王老师",
JSON.stringify(boundRow)
);
const roomsG2 = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 2, page: 1, count: 20 } });
ok(
"List grade=2 排除高一已绑定教室(6-1=5,其余未绑定)",
roomsG2.code === 0 && roomsG2.data.total === 5 && roomsG2.data.list.every(r => r.serID !== room1 && r.classId == null),
`total=${roomsG2.data && roomsG2.data.total}`
);
const roomsBoundOnly = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 1, bindStatus: "bound", page: 1, count: 20 } });
ok("List bindStatus=bound 只含已绑 1 间", roomsBoundOnly.code === 0 && roomsBoundOnly.data.total === 1);
ok("class Delete 绑定中拒绝", (await api("POST", "/api/class/Delete", { form: { serid: clsAddA.data } })).code !== 0);
ok("classroom UnbindClass", (await api("POST", "/api/classroom/UnbindClass", { form: { serid: room1 } })).code === 0);
ok("UnbindClass 未绑定教室静默成功", (await api("POST", "/api/classroom/UnbindClass", { form: { serid: room1 } })).code === 0);
// 批量绑定两间 → 批量解绑 3 间(含 1 间未绑定 → skipped)
await api("POST", "/api/classroom/BindClass", { json: { serID: room1, classId: clsAddA.data } });
await api("POST", "/api/classroom/BindClass", { json: { serID: rooms.data.list[1].serID, classId: clsAddB.data } });
const bUnbind = await api("POST", "/api/classroom/BatchUnbind", { json: [room1, rooms.data.list[1].serID, rooms.data.list[2].serID] });
ok(
"BatchUnbind 3 间(updated=2, skipped=1)",
bUnbind.code === 0 && bUnbind.data.updated === 2 && bUnbind.data.skipped === 1,
JSON.stringify(bUnbind.data)
);
ok("BatchUnbind 空集合拒绝", (await api("POST", "/api/classroom/BatchUnbind", { json: [] })).code !== 0);
ok("class Delete 解绑后可删 A", (await api("POST", "/api/class/Delete", { form: { serid: clsAddA.data } })).code === 0);
ok("class Delete 解绑后可删 B", (await api("POST", "/api/class/Delete", { form: { serid: clsAddB.data } })).code === 0);
// 菜单兜底(仅本地免 SSO 模式):绑定班级在第二位
if (!COOKIE) {
const modsTree = await api("GET", "/api/base/GetUserMod");
const names = (modsTree.data || []).map(m => m.name);
ok(
"GetUserMod 菜单序 教室管理→绑定班级→基础设置",
modsTree.code === 0 && JSON.stringify(names) === JSON.stringify(["教室管理", "绑定班级", "基础设置"]),
JSON.stringify(names)
);
}
const allIds = roomsAfterEdit.data.list.map(r => r.serID);
const bEditStatus = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: allIds, status: "closed" } });
ok("classroom BatchEdit 批量关闭 6 间", bEditStatus.code === 0 && bEditStatus.data.updated === 6, bEditStatus.msg);
const afterBatch = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20, status: "closed" } });
ok("BatchEdit 后按 closed 过滤命中 6 间", afterBatch.code === 0 && afterBatch.data.total === 6);
const bEditType = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: allIds.slice(0, 2), typeId } });
ok("classroom BatchEdit 批量改类型 2 间", bEditType.code === 0 && bEditType.data.updated === 2, bEditType.msg);
ok("BatchEdit 空集合拒绝", (await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [] } })).code !== 0);
ok("BatchEdit 未指定修改项拒绝", (await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1] } })).code !== 0);
ok(
"BatchEdit 非法状态拒绝",
(await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1], status: "xxx" } })).code !== 0
);
const bEditSkip = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1, 99999999], status: "open" } });
ok(
"BatchEdit 不存在 id 计入 skipped(不整批失败)",
bEditSkip.code === 0 && bEditSkip.data.updated === 1 && bEditSkip.data.skipped === 1,
JSON.stringify(bEditSkip.data)
);
// 10. 其余删除守卫(楼栋有教室 → 类型被引用 → 校区有楼栋)
const delBuilding = await api("POST", "/api/building/Delete", { form: { serid: buildingId } });
ok("building Delete 守卫拒绝", delBuilding.code !== 0);
const delType = await api("POST", "/api/classtype/Delete", { form: { serid: typeId } });
ok("classtype Delete 守卫拒绝", delType.code !== 0);
const delCampus = await api("POST", "/api/campus/Delete", { form: { serid: campusId } });
ok("campus Delete 守卫拒绝", delCampus.code !== 0);
// 11. 逆序清理:教室(关联随删)→ 楼栋 → 类型 → 设备 → 校区
const delRooms = await api("POST", "/api/classroom/Delete", { json: allIds });
ok("classroom Delete 全部(关联自动清理)", delRooms.code === 0, delRooms.msg);
ok("equipment Delete(解除引用后可删)", (await api("POST", "/api/equipment/Delete", { form: { serid: eqSeedId } })).code === 0);
ok("building Delete", (await api("POST", "/api/building/Delete", { form: { serid: buildingId } })).code === 0);
ok("classtype Delete", (await api("POST", "/api/classtype/Delete", { form: { serid: typeId } })).code === 0);
ok("campus Delete", (await api("POST", "/api/campus/Delete", { form: { serid: campusId } })).code === 0);
console.log(`\n[smoke] 通过 ${passed} / ${passed + failed}`);
process.exit(failed ? 1 : 0);