263 lines
8.7 KiB
JavaScript
263 lines
8.7 KiB
JavaScript
const { describe, test } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { NTPMonitorType } = require("../../server/monitor-types/ntp");
|
|
const { UP } = require("../../src/util");
|
|
|
|
describe("NTPMonitorType", () => {
|
|
const ntp = new NTPMonitorType();
|
|
|
|
test("createNTPPacket() returns a 48-byte buffer with correct header", () => {
|
|
const packet = ntp.createNTPPacket();
|
|
assert.strictEqual(packet.length, 48);
|
|
// LI=0, VN=3, Mode=3 => 0x1B
|
|
assert.strictEqual(packet[0], 0x1b);
|
|
// Rest should be zeros
|
|
for (let i = 1; i < 48; i++) {
|
|
assert.strictEqual(packet[i], 0, `byte ${i} should be zero`);
|
|
}
|
|
});
|
|
|
|
test("readNTPTimestamp() correctly converts NTP timestamp to milliseconds", () => {
|
|
const buf = Buffer.alloc(8);
|
|
// 1 second since NTP epoch
|
|
buf.writeUInt32BE(1, 0);
|
|
buf.writeUInt32BE(0, 4);
|
|
assert.strictEqual(ntp.readNTPTimestamp(buf, 0), 1000);
|
|
|
|
// 0.5 seconds fractional
|
|
buf.writeUInt32BE(0, 0);
|
|
buf.writeUInt32BE(0x80000000, 4);
|
|
assert.strictEqual(ntp.readNTPTimestamp(buf, 0), 500);
|
|
});
|
|
|
|
test("parseNTPResponse() extracts correct fields from a valid packet", () => {
|
|
// Construct a minimal valid NTP response packet
|
|
const msg = Buffer.alloc(48);
|
|
|
|
// Byte 0: LI=0, VN=3, Mode=4 (server) => 00 011 100 = 0x1C
|
|
msg[0] = 0x1c;
|
|
// Stratum 2
|
|
msg[1] = 2;
|
|
|
|
// Root dispersion at offset 8: 0.5 seconds = 0x00008000
|
|
msg.writeUInt32BE(0x00008000, 8);
|
|
|
|
// Reference ID at offset 12: "GPS\0" for stratum 1 test not applicable here
|
|
// For stratum 2, it's an IPv4 address
|
|
msg[12] = 192;
|
|
msg[13] = 168;
|
|
msg[14] = 1;
|
|
msg[15] = 1;
|
|
|
|
// Server receive timestamp (T2) at offset 32: 3912710400 seconds (approx 2024-01-01)
|
|
msg.writeUInt32BE(3912710400, 32);
|
|
msg.writeUInt32BE(0, 36);
|
|
|
|
// Server transmit timestamp (T3) at offset 40: same + 1ms
|
|
msg.writeUInt32BE(3912710400, 40);
|
|
msg.writeUInt32BE(4294967, 44); // ~1ms in fractional seconds
|
|
|
|
const t1 = 3912710400 * 1000; // Client originate in ms since NTP epoch
|
|
const t4 = 3912710400 * 1000 + 50; // Client receive 50ms later
|
|
|
|
const result = ntp.parseNTPResponse(msg, t1, t4);
|
|
|
|
assert.strictEqual(result.stratum, 2);
|
|
assert.strictEqual(result.leapIndicator, 0);
|
|
assert.strictEqual(result.refid, "192.168.1.1");
|
|
// Root dispersion: 0x8000 / 65536 * 1000 = 500ms
|
|
assert.ok(
|
|
Math.abs(result.rootDispersion - 500) < 0.1,
|
|
`rootDispersion should be ~500ms, got ${result.rootDispersion}`
|
|
);
|
|
assert.strictEqual(typeof result.offset, "number");
|
|
assert.strictEqual(typeof result.roundTripDelay, "number");
|
|
});
|
|
|
|
test("parseNTPResponse() parses ASCII refid for stratum 1", () => {
|
|
const msg = Buffer.alloc(48);
|
|
msg[0] = 0x1c;
|
|
msg[1] = 1; // Stratum 1
|
|
msg.write("GPS\0", 12, "ascii");
|
|
// Set timestamps to avoid NaN
|
|
msg.writeUInt32BE(3912710400, 32);
|
|
msg.writeUInt32BE(0, 36);
|
|
msg.writeUInt32BE(3912710400, 40);
|
|
msg.writeUInt32BE(0, 44);
|
|
|
|
const t1 = 3912710400 * 1000;
|
|
const t4 = t1 + 10;
|
|
|
|
const result = ntp.parseNTPResponse(msg, t1, t4);
|
|
assert.strictEqual(result.stratum, 1);
|
|
assert.strictEqual(result.refid, "GPS");
|
|
});
|
|
|
|
test("parseNTPResponse() rejects packets shorter than 48 bytes", () => {
|
|
const short = Buffer.alloc(20);
|
|
assert.throws(() => ntp.parseNTPResponse(short, 0, 0), /expected 48\+ bytes/);
|
|
});
|
|
|
|
test("check() throws for stratum 16 (unsynchronized)", async () => {
|
|
const monitor = {
|
|
hostname: "localhost",
|
|
port: 123,
|
|
timeout: 5,
|
|
ntp_stratum_threshold: 5,
|
|
ntp_time_offset_threshold: 1000,
|
|
ntp_root_dispersion_threshold: 500,
|
|
};
|
|
const heartbeat = {};
|
|
|
|
// Stub queryNTP to return stratum 16
|
|
const originalQuery = ntp.queryNTP;
|
|
ntp.queryNTP = async () => ({
|
|
stratum: 16,
|
|
offset: 0,
|
|
rootDispersion: 10,
|
|
refid: "INIT",
|
|
roundTripDelay: 5,
|
|
leapIndicator: 3,
|
|
});
|
|
|
|
try {
|
|
await assert.rejects(() => ntp.check(monitor, heartbeat, null), /unsynchronized.*stratum 16/);
|
|
} finally {
|
|
ntp.queryNTP = originalQuery;
|
|
}
|
|
});
|
|
|
|
test("check() throws when stratum exceeds threshold", async () => {
|
|
const monitor = {
|
|
hostname: "localhost",
|
|
port: 123,
|
|
timeout: 5,
|
|
ntp_stratum_threshold: 2,
|
|
ntp_time_offset_threshold: 1000,
|
|
ntp_root_dispersion_threshold: 500,
|
|
};
|
|
const heartbeat = {};
|
|
|
|
const originalQuery = ntp.queryNTP;
|
|
ntp.queryNTP = async () => ({
|
|
stratum: 3,
|
|
offset: 0.5,
|
|
rootDispersion: 10,
|
|
refid: "GPS",
|
|
roundTripDelay: 5,
|
|
leapIndicator: 0,
|
|
});
|
|
|
|
try {
|
|
await assert.rejects(() => ntp.check(monitor, heartbeat, null), /Stratum 3 meets or exceeds threshold 2/);
|
|
} finally {
|
|
ntp.queryNTP = originalQuery;
|
|
}
|
|
});
|
|
|
|
test("check() throws when offset exceeds threshold", async () => {
|
|
const monitor = {
|
|
hostname: "localhost",
|
|
port: 123,
|
|
timeout: 5,
|
|
ntp_stratum_threshold: 5,
|
|
ntp_time_offset_threshold: 100,
|
|
ntp_root_dispersion_threshold: 500,
|
|
};
|
|
const heartbeat = {};
|
|
|
|
const originalQuery = ntp.queryNTP;
|
|
ntp.queryNTP = async () => ({
|
|
stratum: 2,
|
|
offset: -150.5,
|
|
rootDispersion: 10,
|
|
refid: "GPS",
|
|
roundTripDelay: 5,
|
|
leapIndicator: 0,
|
|
});
|
|
|
|
try {
|
|
await assert.rejects(() => ntp.check(monitor, heartbeat, null), /Time offset.*exceeds threshold 100ms/);
|
|
} finally {
|
|
ntp.queryNTP = originalQuery;
|
|
}
|
|
});
|
|
|
|
test("check() throws when dispersion exceeds threshold", async () => {
|
|
const monitor = {
|
|
hostname: "localhost",
|
|
port: 123,
|
|
timeout: 5,
|
|
ntp_stratum_threshold: 5,
|
|
ntp_time_offset_threshold: 1000,
|
|
ntp_root_dispersion_threshold: 50,
|
|
};
|
|
const heartbeat = {};
|
|
|
|
const originalQuery = ntp.queryNTP;
|
|
ntp.queryNTP = async () => ({
|
|
stratum: 2,
|
|
offset: 1.5,
|
|
rootDispersion: 100,
|
|
refid: "GPS",
|
|
roundTripDelay: 5,
|
|
leapIndicator: 0,
|
|
});
|
|
|
|
try {
|
|
await assert.rejects(() => ntp.check(monitor, heartbeat, null), /Root dispersion.*exceeds threshold 50ms/);
|
|
} finally {
|
|
ntp.queryNTP = originalQuery;
|
|
}
|
|
});
|
|
|
|
test("check() sets heartbeat UP when all thresholds pass", async () => {
|
|
const monitor = {
|
|
hostname: "localhost",
|
|
port: 123,
|
|
timeout: 5,
|
|
ntp_stratum_threshold: 5,
|
|
ntp_time_offset_threshold: 1000,
|
|
ntp_root_dispersion_threshold: 500,
|
|
};
|
|
const heartbeat = {};
|
|
|
|
const originalQuery = ntp.queryNTP;
|
|
ntp.queryNTP = async () => ({
|
|
stratum: 2,
|
|
offset: 1.5,
|
|
rootDispersion: 10.2,
|
|
refid: "GPS",
|
|
roundTripDelay: 5.3,
|
|
leapIndicator: 0,
|
|
});
|
|
|
|
try {
|
|
await ntp.check(monitor, heartbeat, null);
|
|
assert.strictEqual(heartbeat.status, UP);
|
|
assert.match(heartbeat.msg, /Stratum: 2/);
|
|
assert.match(heartbeat.msg, /RefID: GPS/);
|
|
assert.match(heartbeat.msg, /Offset: 1\.500ms/);
|
|
assert.match(heartbeat.msg, /Dispersion: 10\.200ms/);
|
|
assert.strictEqual(typeof heartbeat.ping, "number");
|
|
} finally {
|
|
ntp.queryNTP = originalQuery;
|
|
}
|
|
});
|
|
|
|
test(
|
|
"queryNTP() can reach a public NTP server",
|
|
{
|
|
skip: !!process.env.CI,
|
|
},
|
|
async () => {
|
|
const result = await ntp.queryNTP("time.google.com", 123, 10000);
|
|
assert.strictEqual(typeof result.stratum, "number");
|
|
assert.ok(result.stratum >= 1 && result.stratum <= 15, `stratum should be 1-15, got ${result.stratum}`);
|
|
assert.strictEqual(typeof result.offset, "number");
|
|
assert.strictEqual(typeof result.roundTripDelay, "number");
|
|
assert.strictEqual(typeof result.rootDispersion, "number");
|
|
assert.strictEqual(typeof result.refid, "string");
|
|
}
|
|
);
|
|
});
|