uptime-kuma/test/backend-test/monitors/test-grpc.js
Frank Elsinga 0f61d7ee1b
chore: enable formatting over the entire codebase in CI (#6655)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-09 02:10:36 +01:00

294 lines
9.7 KiB
JavaScript

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