feat(core-edu): admin/dashboard/leave-requests 模块 + gRPC + 状态机测试 + nextstep 文档
This commit is contained in:
190
services/core-edu/test/grpc-smoke.mjs
Normal file
190
services/core-edu/test/grpc-smoke.mjs
Normal file
@@ -0,0 +1,190 @@
|
||||
// gRPC smoke test for core-edu service (P3.13)
|
||||
// 运行: node services/core-edu/test/grpc-smoke.mjs
|
||||
// 前提: edu-core-edu-test 容器已启动,gRPC 端口 50053 可达
|
||||
import grpc from "@grpc/grpc-js";
|
||||
import protoLoader from "@grpc/proto-loader";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const PROTO_PATH = path.resolve(__dirname, "../../../packages/shared-proto/proto/core_edu.proto");
|
||||
const packageDef = protoLoader.loadSync(PROTO_PATH, {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
});
|
||||
const proto = grpc.loadPackageDefinition(packageDef);
|
||||
|
||||
const TARGET = "localhost:50053";
|
||||
const clientMap = {};
|
||||
|
||||
const services = [
|
||||
"ExamService",
|
||||
"HomeworkService",
|
||||
"GradeService",
|
||||
"ClassService",
|
||||
"AttendanceService",
|
||||
"ScheduleService",
|
||||
"LeaveRequestService",
|
||||
"DashboardService",
|
||||
"AdminService",
|
||||
];
|
||||
|
||||
for (const s of services) {
|
||||
const Ctor = proto.next_edu_cloud.core_edu.v1[s];
|
||||
if (!Ctor) {
|
||||
console.error(`MISSING service: ${s}`);
|
||||
continue;
|
||||
}
|
||||
clientMap[s] = new Ctor(TARGET, grpc.credentials.createInsecure());
|
||||
}
|
||||
|
||||
const results = { pass: [], fail: [] };
|
||||
function record(name, ok, detail) {
|
||||
const entry = { rpc: name, ok, detail: detail ? String(detail).slice(0, 300) : undefined };
|
||||
if (ok) results.pass.push(entry);
|
||||
else results.fail.push(entry);
|
||||
const tag = ok ? "PASS" : "FAIL";
|
||||
console.log(`[${tag}] ${name}${detail ? " :: " + entry.detail : ""}`);
|
||||
}
|
||||
|
||||
function call(client, method, req) {
|
||||
return new Promise((resolve) => {
|
||||
if (!client) {
|
||||
resolve({ error: new Error("client missing") });
|
||||
return;
|
||||
}
|
||||
if (typeof client[method] !== "function") {
|
||||
resolve({ error: new Error(`method ${method} not found`) });
|
||||
return;
|
||||
}
|
||||
client[method](req, (error, response) => {
|
||||
resolve({ error, response });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`\n=== core-edu gRPC smoke test @ ${TARGET} ===\n`);
|
||||
|
||||
// ---- ExamService ----
|
||||
let r;
|
||||
|
||||
r = await call(clientMap.ExamService, "ListExamsByClass", { class_id: "cls-nonexistent" });
|
||||
record("ExamService.ListExamsByClass", !r.error, r.error?.message || `exams=${r.response?.exams?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.ExamService, "GetExam", { id: "exam-nonexistent" });
|
||||
// NotFound 是正常的业务错误(说明 RPC 可达且执行了)
|
||||
record("ExamService.GetExam", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
|
||||
|
||||
r = await call(clientMap.ExamService, "SaveExamDraft", { exam_id: "1b7f27aa-f45d-495d-b74f-cecbd0230e2e", student_id: "stu-1", answers: [] });
|
||||
record("ExamService.SaveExamDraft", !r.error, r.error?.message || `draftId=${r.response?.draftId}`);
|
||||
|
||||
r = await call(clientMap.ExamService, "RecordExamViolation", { exam_id: "1b7f27aa-f45d-495d-b74f-cecbd0230e2e", student_id: "stu-1", violation_type: "tab_switch", detail: "test", severity: 1 });
|
||||
record("ExamService.RecordExamViolation", !r.error, r.error?.message || `violationId=${r.response?.violationId}`);
|
||||
|
||||
// ---- HomeworkService ----
|
||||
r = await call(clientMap.HomeworkService, "ListHomeworkByClass", { class_id: "cls-nonexistent" });
|
||||
record("HomeworkService.ListHomeworkByClass", !r.error, r.error?.message || `homework=${r.response?.homework?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.HomeworkService, "GetHomework", { id: "hw-nonexistent" });
|
||||
record("HomeworkService.GetHomework", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
|
||||
|
||||
// ---- GradeService ----
|
||||
r = await call(clientMap.GradeService, "ListGradesByStudent", { student_id: "stu-nonexistent" });
|
||||
record("GradeService.ListGradesByStudent", !r.error, r.error?.message || `grades=${r.response?.grades?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.GradeService, "GetReportCard", { student_id: "stu-nonexistent" });
|
||||
record("GradeService.GetReportCard", !r.error, r.error?.message || `overallGrade=${r.response?.overallGrade}`);
|
||||
|
||||
// ---- ClassService ----
|
||||
r = await call(clientMap.ClassService, "GetClass", { id: "cls-nonexistent" });
|
||||
record("ClassService.GetClass", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
|
||||
|
||||
r = await call(clientMap.ClassService, "GetClassesByTeacher", { teacher_id: "t-nonexistent" });
|
||||
record("ClassService.GetClassesByTeacher", !r.error, r.error?.message || `classes=${r.response?.classes?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.ClassService, "BatchGetClasses", { ids: [] });
|
||||
record("ClassService.BatchGetClasses", !r.error, r.error?.message || `classes=${r.response?.classes?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.ClassService, "ListStudentsByClass", { class_id: "cls-nonexistent" });
|
||||
record("ClassService.ListStudentsByClass", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || `students=${r.response?.students?.length ?? 0}`);
|
||||
|
||||
// ---- AttendanceService ----
|
||||
r = await call(clientMap.AttendanceService, "ListAttendanceByStudent", { student_id: "stu-nonexistent" });
|
||||
record("AttendanceService.ListAttendanceByStudent", !r.error, r.error?.message || `attendance=${r.response?.attendance?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.AttendanceService, "ListAttendanceByClass", { class_id: "cls-nonexistent" });
|
||||
record("AttendanceService.ListAttendanceByClass", !r.error, r.error?.message || `attendance=${r.response?.attendance?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.AttendanceService, "GetAttendance", { id: "att-nonexistent" });
|
||||
record("AttendanceService.GetAttendance", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || "ok");
|
||||
|
||||
// ---- ScheduleService ----
|
||||
r = await call(clientMap.ScheduleService, "GetScheduleByStudent", { student_id: "stu-nonexistent" });
|
||||
record("ScheduleService.GetScheduleByStudent", !r.error, r.error?.message || `slots=${r.response?.slots?.length ?? 0}`);
|
||||
|
||||
// ---- LeaveRequestService ----
|
||||
r = await call(clientMap.LeaveRequestService, "ListLeaveRequestsByStudent", { student_id: "stu-nonexistent" });
|
||||
record("LeaveRequestService.ListLeaveRequestsByStudent", !r.error, r.error?.message || `requests=${r.response?.leaveRequests?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.LeaveRequestService, "CreateLeaveRequest", {
|
||||
student_id: "stu-smoke",
|
||||
class_id: "cls-smoke",
|
||||
leave_type: "personal",
|
||||
start_date: "2026-07-14",
|
||||
end_date: "2026-07-14",
|
||||
reason: "smoke test",
|
||||
submitted_by: "dev-teacher",
|
||||
school_id: "sch-smoke",
|
||||
});
|
||||
record("LeaveRequestService.CreateLeaveRequest", !r.error, r.error?.message || `id=${r.response?.id}`);
|
||||
|
||||
// ---- DashboardService ----
|
||||
r = await call(clientMap.DashboardService, "GetDashboard", { teacher_id: "t-nonexistent" });
|
||||
record("DashboardService.GetDashboard", !r.error, r.error?.message || `totalClasses=${r.response?.totalClasses}`);
|
||||
|
||||
r = await call(clientMap.DashboardService, "GetClassPerformance", { class_id: "cls-nonexistent" });
|
||||
record("DashboardService.GetClassPerformance", !r.error || r.error.code === grpc.status.NOT_FOUND, r.error?.message || `avg=${r.response?.averageScore}`);
|
||||
|
||||
// ---- AdminService ----
|
||||
r = await call(clientMap.AdminService, "ListSchools", {});
|
||||
record("AdminService.ListSchools", !r.error, r.error?.message || `schools=${r.response?.schools?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.AdminService, "ListGradeLevels", { school_id: "sch-nonexistent" });
|
||||
record("AdminService.ListGradeLevels", !r.error, r.error?.message || `gradeLevels=${r.response?.gradeLevels?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.AdminService, "ListDepartments", { school_id: "sch-nonexistent" });
|
||||
record("AdminService.ListDepartments", !r.error, r.error?.message || `departments=${r.response?.departments?.length ?? 0}`);
|
||||
|
||||
r = await call(clientMap.AdminService, "ListAcademicYears", {});
|
||||
record("AdminService.ListAcademicYears", !r.error, r.error?.message || `academicYears=${r.response?.academicYears?.length ?? 0}`);
|
||||
|
||||
// ---- 汇总 ----
|
||||
console.log(`\n=== 汇总 ===`);
|
||||
console.log(`PASS: ${results.pass.length} FAIL: ${results.fail.length} TOTAL: ${results.pass.length + results.fail.length}`);
|
||||
if (results.fail.length > 0) {
|
||||
console.log("\n失败项:");
|
||||
for (const f of results.fail) {
|
||||
console.log(` - ${f.rpc}: ${f.detail}`);
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("全部 RPC 可达 ✓");
|
||||
}
|
||||
|
||||
for (const s of services) {
|
||||
clientMap[s]?.close?.();
|
||||
}
|
||||
grpc.getClientChannelMap?.();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("smoke test crashed:", e);
|
||||
process.exit(2);
|
||||
});
|
||||
Reference in New Issue
Block a user