feat(push-gateway): config 扩展 + kafka consumer + ws handler + nextstep 文档

This commit is contained in:
SpecialX
2026-07-14 16:03:17 +08:00
parent 5a88c8b45d
commit 9fd7c018c2
8 changed files with 630 additions and 36 deletions

View File

@@ -0,0 +1,98 @@
// Test script: Exam events through Kafka → push-gateway → WebSocket (v2)
// Verifies that the 3 exam events (ExamExtended, ExamForceSubmitted,
// ExamQuestionReordered) traverse the new edu.notify.notification.sent topic
// and reach the WebSocket client with correct event_type passthrough.
//
// Usage: node services/push-gateway/scripts/test-exam-events.mjs
import WebSocket from "ws";
import { Kafka } from "kafkajs";
const WS_URL = "ws://localhost:8081/ws?token=dev-token";
const KAFKA_BROKER = "localhost:9092";
const TOPIC = "edu.notify.notification.sent";
const USER_ID = "dev-user";
const examEvents = [
{
event_type: "ExamExtended",
data: { examId: "exam-001", extendedMinutes: 15, reason: "special needs" },
},
{
event_type: "ExamForceSubmitted",
data: { examId: "exam-001", reason: "time_expired", submissionId: "sub-001" },
},
{
event_type: "ExamQuestionReordered",
data: { examId: "exam-001", questionIds: ["q-3", "q-1", "q-2"] },
},
];
console.log("[exam-test] connecting WebSocket to", WS_URL);
const ws = new WebSocket(WS_URL);
const received = [];
ws.on("open", async () => {
console.log("[exam-test] WebSocket connected, publishing 3 exam events...");
const kafka = new Kafka({ brokers: [KAFKA_BROKER] });
const producer = kafka.producer();
await producer.connect();
for (const ev of examEvents) {
const payload = {
event_id: `evt-exam-${ev.event_type}-${Date.now()}`,
user_id: USER_ID,
event_type: ev.event_type,
channel: "ws",
title: `Exam event: ${ev.event_type}`,
content: `Verification of ${ev.event_type} passthrough`,
data: ev.data,
broadcast: false,
occurred_at: Date.now(),
};
await producer.send({
topic: TOPIC,
messages: [{ value: JSON.stringify(payload) }],
});
console.log(`[exam-test] published ${ev.event_type}`);
}
await producer.disconnect();
// Wait up to 15 seconds for all 3 messages.
const deadline = Date.now() + 15000;
while (Date.now() < deadline && received.length < 3) {
await new Promise((r) => setTimeout(r, 200));
}
if (received.length < 3) {
console.error(`[exam-test] FAIL: only ${received.length}/3 messages received`);
process.exit(1);
}
const got = received.map((m) => m.event).sort();
const want = examEvents.map((e) => e.event_type).sort();
if (JSON.stringify(got) === JSON.stringify(want)) {
console.log("[exam-test] PASS: all 3 exam events received with correct event_type");
console.log("[exam-test] events:", got.join(", "));
process.exit(0);
} else {
console.error("[exam-test] FAIL: event mismatch");
console.error(" got:", got);
console.error(" want:", want);
process.exit(1);
}
});
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
console.log("[exam-test] received:", JSON.stringify(msg));
received.push(msg);
});
ws.on("error", (err) => {
console.error("[exam-test] WebSocket error:", err.message);
process.exit(1);
});
setTimeout(() => {
console.error("[exam-test] FAIL: overall timeout 20s");
process.exit(1);
}, 20000);

View File

@@ -0,0 +1,78 @@
// Test script: Kafka → push-gateway → WebSocket full chain (v2)
// Usage: node scripts/test-kafka-ws.mjs
// Prerequisites: push-gateway running on localhost:8081, Kafka on localhost:9092,
// topic edu.notify.notification.sent exists.
import WebSocket from "ws";
import { Kafka } from "kafkajs";
const WS_URL = "ws://localhost:8081/ws?token=dev-token";
const KAFKA_BROKER = "localhost:9092";
const TOPIC = "edu.notify.notification.sent";
const USER_ID = "dev-user"; // DevMode maps dev-token to this user
const EVENT_TYPE = "TestEventV2KafkaChain";
console.log("[test] connecting WebSocket to", WS_URL);
const ws = new WebSocket(WS_URL);
const messages = [];
ws.on("open", () => {
console.log("[test] WebSocket connected, publishing Kafka message...");
publishKafka();
});
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
console.log("[test] received WS message:", JSON.stringify(msg));
messages.push(msg);
});
ws.on("error", (err) => {
console.error("[test] WebSocket error:", err.message);
process.exit(1);
});
async function publishKafka() {
const kafka = new Kafka({ brokers: [KAFKA_BROKER] });
const producer = kafka.producer();
await producer.connect();
const payload = {
event_id: `evt-test-${Date.now()}`,
user_id: USER_ID,
event_type: EVENT_TYPE,
channel: "ws",
title: "v2 Kafka chain test",
content: "Verifying edu.notify.notification.sent consumption",
data: { message: "hello from v2 kafka chain test" },
broadcast: false,
occurred_at: Date.now(),
};
await producer.send({
topic: TOPIC,
messages: [{ value: JSON.stringify(payload) }],
});
console.log("[test] Kafka message published to", TOPIC);
await producer.disconnect();
// Wait up to 10 seconds for the WS message.
const deadline = Date.now() + 10000;
while (Date.now() < deadline && messages.length === 0) {
await new Promise((r) => setTimeout(r, 200));
}
if (messages.length === 0) {
console.error("[test] FAIL: no WS message received within 10s");
process.exit(1);
}
const got = messages[0];
if (got.event === EVENT_TYPE && got.type === "message") {
console.log("[test] PASS: WS message matches expected event_type");
process.exit(0);
} else {
console.error("[test] FAIL: WS message mismatch:", JSON.stringify(got));
process.exit(1);
}
}
// Hard timeout.
setTimeout(() => {
console.error("[test] FAIL: overall timeout 15s");
process.exit(1);
}, 15000);