// 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); });