const { describe, test } = require("node:test"); const assert = require("node:assert"); const grpc = require("@grpc/grpc-js"); const protoLoader = require("@grpc/proto-loader"); const { GrpcKeywordMonitorType } = require("../../../server/monitor-types/grpc"); const { UP, PENDING } = require("../../../src/util"); const fs = require("fs"); const path = require("path"); const os = require("os"); const testProto = ` syntax = "proto3"; package test; service TestService { rpc Echo (EchoRequest) returns (EchoResponse); } message EchoRequest { string message = 1; } message EchoResponse { string message = 1; } `; /** * Create a gRPC server for testing * @param {number} port Port to listen on * @param {object} methodHandlers Object with method handlers * @returns {Promise} gRPC server instance */ async function createTestGrpcServer(port, methodHandlers) { // Write proto to temp file const tmpDir = os.tmpdir(); const protoPath = path.join(tmpDir, `test-${port}.proto`); fs.writeFileSync(protoPath, testProto); // Load proto file const packageDefinition = protoLoader.loadSync(protoPath, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, }); const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); const testPackage = protoDescriptor.test; const server = new grpc.Server(); // Add service implementation server.addService(testPackage.TestService.service, { Echo: (call, callback) => { if (methodHandlers.Echo) { methodHandlers.Echo(call, callback); } else { callback(null, { message: call.request.message }); } }, }); return new Promise((resolve, reject) => { server.bindAsync(`0.0.0.0:${port}`, grpc.ServerCredentials.createInsecure(), (err) => { if (err) { reject(err); } else { server.start(); // Clean up temp file fs.unlinkSync(protoPath); resolve(server); } }); }); } describe( "GrpcKeywordMonitorType", { skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"), }, () => { test("check() sets status to UP when keyword is found in response", async () => { const port = 50051; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { callback(null, { message: "Hello World with SUCCESS keyword" }); }, }); const grpcMonitor = new GrpcKeywordMonitorType(); const monitor = { grpcUrl: `localhost:${port}`, grpcProtobuf: testProto, grpcServiceName: "test.TestService", grpcMethod: "echo", grpcBody: JSON.stringify({ message: "test" }), keyword: "SUCCESS", invertKeyword: false, grpcEnableTls: false, isInvertKeyword: () => false, }; const heartbeat = { msg: "", status: PENDING, }; try { await grpcMonitor.check(monitor, heartbeat, {}); assert.strictEqual(heartbeat.status, UP); assert.ok(heartbeat.msg.includes("SUCCESS")); assert.ok(heartbeat.msg.includes("is")); } finally { server.forceShutdown(); } }); test("check() rejects when keyword is not found in response", async () => { const port = 50052; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { callback(null, { message: "Hello World without the expected keyword" }); }, }); const grpcMonitor = new GrpcKeywordMonitorType(); const monitor = { grpcUrl: `localhost:${port}`, grpcProtobuf: testProto, grpcServiceName: "test.TestService", grpcMethod: "echo", grpcBody: JSON.stringify({ message: "test" }), keyword: "MISSING", invertKeyword: false, grpcEnableTls: false, isInvertKeyword: () => false, }; const heartbeat = { msg: "", status: PENDING, }; try { await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => { assert.ok(err.message.includes("MISSING")); assert.ok(err.message.includes("not")); return true; }); } finally { server.forceShutdown(); } }); test("check() rejects when inverted keyword is present in response", async () => { const port = 50053; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { callback(null, { message: "Response with ERROR keyword" }); }, }); const grpcMonitor = new GrpcKeywordMonitorType(); const monitor = { grpcUrl: `localhost:${port}`, grpcProtobuf: testProto, grpcServiceName: "test.TestService", grpcMethod: "echo", grpcBody: JSON.stringify({ message: "test" }), keyword: "ERROR", invertKeyword: true, grpcEnableTls: false, isInvertKeyword: () => true, }; const heartbeat = { msg: "", status: PENDING, }; try { await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => { assert.ok(err.message.includes("ERROR")); assert.ok(err.message.includes("present")); return true; }); } finally { server.forceShutdown(); } }); test("check() sets status to UP when inverted keyword is not present in response", async () => { const port = 50054; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { callback(null, { message: "Response without error keyword" }); }, }); const grpcMonitor = new GrpcKeywordMonitorType(); const monitor = { grpcUrl: `localhost:${port}`, grpcProtobuf: testProto, grpcServiceName: "test.TestService", grpcMethod: "echo", grpcBody: JSON.stringify({ message: "test" }), keyword: "ERROR", invertKeyword: true, grpcEnableTls: false, isInvertKeyword: () => true, }; const heartbeat = { msg: "", status: PENDING, }; try { await grpcMonitor.check(monitor, heartbeat, {}); assert.strictEqual(heartbeat.status, UP); assert.ok(heartbeat.msg.includes("ERROR")); assert.ok(heartbeat.msg.includes("not")); } finally { server.forceShutdown(); } }); test("check() rejects when gRPC server is unreachable", async () => { const grpcMonitor = new GrpcKeywordMonitorType(); const monitor = { grpcUrl: "localhost:50099", grpcProtobuf: testProto, grpcServiceName: "test.TestService", grpcMethod: "echo", grpcBody: JSON.stringify({ message: "test" }), keyword: "SUCCESS", invertKeyword: false, grpcEnableTls: false, isInvertKeyword: () => false, }; const heartbeat = { msg: "", status: PENDING, }; await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => { // Should fail with connection error return true; }); }); test("check() truncates long response messages in error output", async () => { const port = 50055; const longMessage = "A".repeat(100) + " with SUCCESS keyword"; const server = await createTestGrpcServer(port, { Echo: (call, callback) => { callback(null, { message: longMessage }); }, }); const grpcMonitor = new GrpcKeywordMonitorType(); const monitor = { grpcUrl: `localhost:${port}`, grpcProtobuf: testProto, grpcServiceName: "test.TestService", grpcMethod: "echo", grpcBody: JSON.stringify({ message: "test" }), keyword: "MISSING", invertKeyword: false, grpcEnableTls: false, isInvertKeyword: () => false, }; const heartbeat = { msg: "", status: PENDING, }; try { await assert.rejects(grpcMonitor.check(monitor, heartbeat, {}), (err) => { // Should truncate message to 50 characters with "..." assert.ok(err.message.includes("...")); return true; }); } finally { server.forceShutdown(); } }); } );