mirror of
https://github.com/minescope/mineping.git
synced 2025-06-19 05:26:21 +03:00
test: implement tests using vitest framework
This commit is contained in:
parent
ef2bebe755
commit
435e59739c
1499
package-lock.json
generated
Normal file
1499
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -1,22 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "@minescope/mineping",
|
"name": "@minescope/mineping",
|
||||||
"version": "1.6.1",
|
"version": "1.7.0-beta.0",
|
||||||
"description": "Ping both Minecraft Bedrock and Java servers.",
|
"description": "Ping both Minecraft Bedrock and Java servers.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
"types": "types/index.d.ts",
|
"types": "types/index.d.ts",
|
||||||
"keywords": [],
|
"scripts": {
|
||||||
"author": {
|
"test": "vitest run",
|
||||||
"name": "Timofey (xzeldon)",
|
"test:watch": "vitest"
|
||||||
"email": "contact@zeldon.ru",
|
|
||||||
"url": "https://zeldon.ru"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/minescope/mineping.git"
|
"url": "git://github.com/minescope/mineping.git"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"license": "MIT",
|
||||||
|
"keywords": [],
|
||||||
|
"author": {
|
||||||
|
"name": "Timofey Gelazoniya",
|
||||||
|
"email": "timofey@z4n.me",
|
||||||
|
"url": "https://zeldon.ru"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"devDependencies": {
|
||||||
|
"vitest": "^3.2.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
117
test/bedrock.test.js
Normal file
117
test/bedrock.test.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import dgram from "node:dgram";
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { pingBedrock } from "../lib/bedrock.js";
|
||||||
|
|
||||||
|
vi.mock("node:dgram");
|
||||||
|
|
||||||
|
describe("bedrock.js", () => {
|
||||||
|
let mockSocket;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// A store for event handlers, closed over by the mockSocket.
|
||||||
|
const handlers = {};
|
||||||
|
|
||||||
|
// Create a stateful mock socket to simulate EventEmitter.
|
||||||
|
mockSocket = {
|
||||||
|
send: vi.fn(),
|
||||||
|
close: vi.fn(),
|
||||||
|
on: vi.fn((event, handler) => {
|
||||||
|
handlers[event] = handler;
|
||||||
|
}),
|
||||||
|
emit: vi.fn((event, ...args) => {
|
||||||
|
if (handlers[event]) {
|
||||||
|
handlers[event](...args);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
dgram.createSocket = vi.fn().mockReturnValue(mockSocket);
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ping a server and parse MOTD", async () => {
|
||||||
|
const host = "play.example.com";
|
||||||
|
const options = { port: 25565, timeout: 10000 };
|
||||||
|
const pingPromise = pingBedrock(host, options);
|
||||||
|
|
||||||
|
const motd =
|
||||||
|
"MCPE;§l§bOasys§fPE;0;1337;1096;1999;-37530542056358113;oasys-pe.ru;Adventure;1";
|
||||||
|
const mockPongPacket = createMockPongPacket(motd);
|
||||||
|
|
||||||
|
mockSocket.emit("message", mockPongPacket);
|
||||||
|
|
||||||
|
const result = await pingPromise;
|
||||||
|
|
||||||
|
expect(dgram.createSocket).toHaveBeenCalledWith("udp4");
|
||||||
|
expect(mockSocket.send).toHaveBeenCalledWith(
|
||||||
|
expect.any(Buffer),
|
||||||
|
0,
|
||||||
|
33,
|
||||||
|
options.port,
|
||||||
|
host
|
||||||
|
);
|
||||||
|
expect(mockSocket.close).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({
|
||||||
|
edition: "MCPE",
|
||||||
|
name: "§l§bOasys§fPE",
|
||||||
|
version: { protocolVersion: 0, minecraftVersion: "1337" },
|
||||||
|
players: { online: 1096, max: 1999 },
|
||||||
|
serverId: "-37530542056358113",
|
||||||
|
mapName: "oasys-pe.ru",
|
||||||
|
gameMode: "Adventure",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("errors", () => {
|
||||||
|
it("should throw an error if host is not provided", () => {
|
||||||
|
expect(() => pingBedrock(null)).toThrow("Host argument is not provided");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on socket timeout", async () => {
|
||||||
|
const pingPromise = pingBedrock("play.example.com", { timeout: 1000 });
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("Socket timeout");
|
||||||
|
expect(mockSocket.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on a generic socket error", async () => {
|
||||||
|
const pingPromise = pingBedrock("play.example.com");
|
||||||
|
|
||||||
|
// Simulate a DNS or network error by emitting it.
|
||||||
|
mockSocket.emit("error", new Error("EHOSTUNREACH"));
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("EHOSTUNREACH");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only reject once, even if multiple errors occur", async () => {
|
||||||
|
const pingPromise = pingBedrock("play.example.com");
|
||||||
|
|
||||||
|
// Fire a socket error first.
|
||||||
|
mockSocket.emit("error", new Error("First error"));
|
||||||
|
|
||||||
|
// Then, try to trigger another error by sending a bad message.
|
||||||
|
mockSocket.emit("message", Buffer.alloc(0));
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("First error");
|
||||||
|
expect(mockSocket.close).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createMockPongPacket(motd) {
|
||||||
|
const motdBuffer = Buffer.from(motd, "utf-8");
|
||||||
|
const packet = Buffer.alloc(35 + motdBuffer.length);
|
||||||
|
packet.writeUInt8(0x1c, 0);
|
||||||
|
packet.writeBigInt64LE(BigInt(Date.now()), 1);
|
||||||
|
Buffer.from("00ffff00fefefefefdfdfdfd12345678", "hex").copy(packet, 17);
|
||||||
|
packet.writeUInt16BE(motdBuffer.length, 33);
|
||||||
|
motdBuffer.copy(packet, 35);
|
||||||
|
return packet;
|
||||||
|
}
|
112
test/java.test.js
Normal file
112
test/java.test.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import net from "node:net";
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { pingJava } from "../lib/java.js";
|
||||||
|
import varint from "../lib/varint.js";
|
||||||
|
|
||||||
|
vi.mock("node:net");
|
||||||
|
|
||||||
|
describe("pingJava", () => {
|
||||||
|
let mockSocket;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockHandlers = {};
|
||||||
|
mockSocket = {
|
||||||
|
write: vi.fn(),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
setNoDelay: vi.fn(),
|
||||||
|
on: vi.fn((event, handler) => (mockHandlers[event] = handler)),
|
||||||
|
emit: vi.fn((event, ...args) => mockHandlers[event]?.(...args)),
|
||||||
|
};
|
||||||
|
net.createConnection = vi.fn().mockReturnValue(mockSocket);
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ping a server and handle a chunked response", async () => {
|
||||||
|
const host = "mc.hypixel.net";
|
||||||
|
const options = {
|
||||||
|
port: 25565,
|
||||||
|
timeout: 5000,
|
||||||
|
protocolVersion: 765,
|
||||||
|
virtualHost: "mc.hypixel.net",
|
||||||
|
};
|
||||||
|
|
||||||
|
const pingPromise = pingJava(host, options);
|
||||||
|
|
||||||
|
mockSocket.emit("connect");
|
||||||
|
|
||||||
|
expect(net.createConnection).toHaveBeenCalledWith({
|
||||||
|
host,
|
||||||
|
port: options.port,
|
||||||
|
});
|
||||||
|
expect(mockSocket.setNoDelay).toHaveBeenCalledWith(true);
|
||||||
|
expect(mockSocket.write).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
version: { name: "1.21", protocol: 765 },
|
||||||
|
players: { max: 20, online: 5, sample: [] },
|
||||||
|
description: "A Minecraft Server",
|
||||||
|
favicon: "data:image/png;base64,iVBORw0KGgo...",
|
||||||
|
};
|
||||||
|
const fullPacket = createMockJavaResponse(mockResponse);
|
||||||
|
const chunk1 = fullPacket.subarray(0, 10);
|
||||||
|
const chunk2 = fullPacket.subarray(10);
|
||||||
|
|
||||||
|
mockSocket.emit("data", chunk1);
|
||||||
|
mockSocket.emit("data", chunk2);
|
||||||
|
|
||||||
|
const result = await pingPromise;
|
||||||
|
expect(result).toEqual(mockResponse);
|
||||||
|
expect(mockSocket.destroy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("errors", () => {
|
||||||
|
it("should throw an error if host is not provided", () => {
|
||||||
|
expect(() => pingJava(null)).toThrow("Host argument is not provided");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on socket timeout before data is received", async () => {
|
||||||
|
const pingPromise = pingJava("localhost", { timeout: 1000 });
|
||||||
|
mockSocket.emit("connect");
|
||||||
|
|
||||||
|
// Advance time to trigger the timeout
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("Socket timeout");
|
||||||
|
expect(mockSocket.destroy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject on connection error", async () => {
|
||||||
|
const pingPromise = pingJava("localhost");
|
||||||
|
|
||||||
|
// Simulate a connection refusal
|
||||||
|
mockSocket.emit("error", new Error("ECONNREFUSED"));
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("ECONNREFUSED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only reject once, even if multiple errors occur", async () => {
|
||||||
|
const pingPromise = pingJava("localhost");
|
||||||
|
|
||||||
|
// Fire two errors back-to-back
|
||||||
|
mockSocket.emit("error", new Error("First error"));
|
||||||
|
mockSocket.emit("error", new Error("Second error"));
|
||||||
|
|
||||||
|
await expect(pingPromise).rejects.toThrow("First error");
|
||||||
|
expect(mockSocket.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createMockJavaResponse(response) {
|
||||||
|
const jsonString = JSON.stringify(response);
|
||||||
|
const jsonBuffer = Buffer.from(jsonString, "utf8");
|
||||||
|
const responseLength = varint.encodeInt(jsonBuffer.length);
|
||||||
|
const packetId = varint.encodeInt(0);
|
||||||
|
const packetData = Buffer.concat([packetId, responseLength, jsonBuffer]);
|
||||||
|
const packetLength = varint.encodeInt(packetData.length);
|
||||||
|
return Buffer.concat([packetLength, packetData]);
|
||||||
|
}
|
76
test/varint.test.js
Normal file
76
test/varint.test.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import varint from "../lib/varint.js";
|
||||||
|
|
||||||
|
describe("varint.js", () => {
|
||||||
|
it("should encode and decode integers symmetrically (round-trip)", () => {
|
||||||
|
const testValues = [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
127, // Max 1-byte
|
||||||
|
128, // Min 2-byte
|
||||||
|
255,
|
||||||
|
16383, // Max 2-byte
|
||||||
|
16384, // Min 3-byte
|
||||||
|
2147483647, // Max signed 32-bit int
|
||||||
|
-1, // Critical edge case (encodes as max unsigned int)
|
||||||
|
];
|
||||||
|
|
||||||
|
testValues.forEach((value) => {
|
||||||
|
const encoded = varint.encodeInt(value);
|
||||||
|
const decoded = varint.decodeInt(encoded, 0);
|
||||||
|
expect(decoded, `Value ${value} failed round-trip`).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should decode an integer from a non-zero offset", () => {
|
||||||
|
// [255 (invalid varint), 128 (valid varint), 127 (valid varint)]
|
||||||
|
const buffer = Buffer.from([0xff, 0x80, 0x01, 0x7f]);
|
||||||
|
expect(varint.decodeInt(buffer, 1)).toBe(128);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error for a malformed varint that is too long", () => {
|
||||||
|
const invalidBuffer = Buffer.from([0x80, 0x80, 0x80, 0x80, 0x80, 0x80]);
|
||||||
|
expect(() => varint.decodeInt(invalidBuffer, 0)).toThrow(
|
||||||
|
"VarInt is too big"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly predict the encoded length of a varint", () => {
|
||||||
|
const boundaries = [0, 127, 128, 16383, 16384, 2097151, 2097152];
|
||||||
|
boundaries.forEach((value) => {
|
||||||
|
const predictedLength = varint.decodeLength(value);
|
||||||
|
const actualLength = varint.encodeInt(value).length;
|
||||||
|
expect(predictedLength).toBe(actualLength);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should encode 16-bit unsigned shorts in big-endian format", () => {
|
||||||
|
expect(varint.encodeUShort(0)).toEqual(Buffer.from([0x00, 0x00]));
|
||||||
|
expect(varint.encodeUShort(256)).toEqual(Buffer.from([0x01, 0x00]));
|
||||||
|
expect(varint.encodeUShort(65535)).toEqual(Buffer.from([0xff, 0xff]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly assemble and parse a Minecraft handshake packet", () => {
|
||||||
|
const protocolVersion = -1;
|
||||||
|
const virtualHost = "mc.example.com";
|
||||||
|
const port = 25565;
|
||||||
|
|
||||||
|
const payload = Buffer.concat([
|
||||||
|
varint.encodeInt(0),
|
||||||
|
varint.encodeInt(protocolVersion),
|
||||||
|
varint.encodeInt(virtualHost.length),
|
||||||
|
varint.encodeString(virtualHost),
|
||||||
|
varint.encodeUShort(port),
|
||||||
|
varint.encodeInt(1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const finalPacket = varint.concat([payload]);
|
||||||
|
|
||||||
|
const decodedPacketLength = varint.decodeInt(finalPacket, 0);
|
||||||
|
expect(decodedPacketLength).toBe(payload.length);
|
||||||
|
|
||||||
|
const lengthOfPacketLength = varint.decodeLength(decodedPacketLength);
|
||||||
|
const decodedPayload = finalPacket.subarray(lengthOfPacketLength);
|
||||||
|
expect(decodedPayload).toEqual(payload);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user