Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions core/client_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,4 +434,130 @@ describe("core/client", (test) => {

assertEquals(triggered, 1);
});

test("survive unknown commands from server", async () => {
const { client, server } = mock();
await client.connect("host");

server.send([
"FOOBAR",
"BAZQUX param1 param2",
"123 me :unknown numeric",
"PING :alive",
]);

const msg = await client.once("raw:ping");
assertEquals(msg.params, ["alive"]);
server.shutdown();
await client.once("disconnected");
});

test("survive empty lines from server", async () => {
const { client, server } = mock();
await client.connect("host");

server.send([
"",
" ",
"PING :ok",
]);

const msg = await client.once("raw:ping");
assertEquals(msg.params, ["ok"]);
server.shutdown();
await client.once("disconnected");
});

test("survive malformed prefix from server", async () => {
const { client, server } = mock();
await client.connect("host");

server.send([
":!@ PRIVMSG #ch :hello",
":@! PRIVMSG #ch :hello",
": PRIVMSG #ch :hello",
"PING :ok",
]);

const msg = await client.once("raw:ping");
assertEquals(msg.params, ["ok"]);
server.shutdown();
await client.once("disconnected");
});

test("survive malformed tags from server", async () => {
const { client, server } = mock();
await client.connect("host");

server.send([
"@ PING :1",
"@=== PING :2",
"@;; PING :3",
"@key PING :4",
"PING :ok",
]);

const msg = await client.once("raw:ping");
assertEquals(msg.command, "ping");
server.shutdown();
await client.once("disconnected");
});

test("survive server shutdown mid-conversation", async () => {
const { client, server } = mock();
await client.connect("host");

server.send("PING :before");
await client.once("raw:ping");

server.shutdown();
await client.once("disconnected");
});

test("survive null bytes in messages", async () => {
const { client, server } = mock();
await client.connect("host");

server.send(":nick!u@h PRIVMSG #ch :\x00null\x00bytes");
const msg = await client.once("raw:privmsg");
assertEquals(msg.params[1], "\x00null\x00bytes");
server.shutdown();
await client.once("disconnected");
});

test("survive very long message from server", async () => {
const { client, server } = mock({ bufferSize: 65536 });
await client.connect("host");

const longText = "A".repeat(10_000);
server.send(`:nick!u@h PRIVMSG #ch :${longText}`);
const msg = await client.once("raw:privmsg");
assertEquals(msg.params[1].length, 10_000);
server.shutdown();
await client.once("disconnected");
});

test("survive flood of messages", async () => {
const { client, server } = mock();
await client.connect("host");

const N = 80;
let count = 0;
const done = new Promise<void>((resolve) => {
const off = client.on("raw:privmsg", () => {
if (++count === N) {
off();
resolve();
}
});
});

server.send(
Array.from({ length: N }, (_, i) => `:nick!u@h PRIVMSG #ch :msg${i}`),
);
await done;
assertEquals(count, N);
server.shutdown();
await client.once("disconnected");
});
});
105 changes: 104 additions & 1 deletion core/parsers_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { assertEquals } from "@std/assert";
import { describe } from "../testing/helpers.ts";
import { escapeTagValue, parseChunk, unescapeTagValue } from "./parsers.ts";
import {
escapeTagValue,
parseChunk,
parseSource,
unescapeTagValue,
} from "./parsers.ts";

describe("core/parsers", (test) => {
test("parse message without prefix", () => {
Expand Down Expand Up @@ -170,4 +175,102 @@ describe("core/parsers", (test) => {
},
]);
});

test("survive empty string", () => {
const [msgs, remainder] = parseChunk("");
assertEquals(msgs, []);
assertEquals(remainder, "");
});

test("survive bare \\r\\n", () => {
parseChunk("\r\n");
parseChunk("\r\n\r\n\r\n");
});

test("survive only spaces", () => {
parseChunk(" \r\n");
});

test("survive unknown command", () => {
const [msgs] = parseChunk("FOOBAR\r\n");
assertEquals(msgs.length, 1);
assertEquals(msgs[0].params, []);
});

test("survive command with no params", () => {
const [msgs] = parseChunk("PING\r\n");
assertEquals(msgs.length, 1);
assertEquals(msgs[0].command, "ping");
assertEquals(msgs[0].params, []);
});

test("survive prefix with no command", () => {
parseChunk(":server\r\n");
});

test("survive lone @ (tag marker with no data)", () => {
parseChunk("@\r\n");
});

test("survive malformed tags", () => {
parseChunk("@ PING\r\n");
parseChunk("@= PING\r\n");
parseChunk("@; PING\r\n");
parseChunk("@;; PING\r\n");
parseChunk("@=== PING\r\n");
parseChunk("@key PING\r\n");
parseChunk("@ke=y=z PING\r\n");
});

test("survive prefix with only ! or @", () => {
parseChunk(":!@ PING\r\n");
parseChunk(":@ PING\r\n");
parseChunk(":! PING\r\n");
parseChunk(":@! PING\r\n");
});

test("survive null bytes", () => {
parseChunk(":n\0ick PRIVMSG #ch :\0\r\n");
});

test("survive unicode and emoji", () => {
parseChunk(":nïck!üser@hôst PRIVMSG #chännel :🔥🎉\r\n");
});

test("survive very long message", () => {
const longText = "A".repeat(100_000);
parseChunk(`:nick!u@h PRIVMSG #ch :${longText}\r\n`);
});

test("survive trailing without colon", () => {
const [msgs] = parseChunk(":server 001 nick welcome\r\n");
assertEquals(msgs.length, 1);
});

test("survive only \\r or only \\n", () => {
const [msgs1, r1] = parseChunk("PING\r");
assertEquals(msgs1, []);
assertEquals(r1, "PING\r");

const [msgs2, r2] = parseChunk("PING\n");
assertEquals(msgs2, []);
assertEquals(r2, "PING\n");
});

test("survive mixed valid and garbage lines", () => {
parseChunk(
"PING :ok\r\n\r\n:::garbage\r\n@@@\r\n:nick PRIVMSG #ch :hi\r\n",
);
});

test("parseSource with empty string", () => {
const source = parseSource("");
assertEquals(source.name, "");
});

test("parseSource with only special chars", () => {
parseSource("!@");
parseSource("@!");
parseSource("!!!@@@");
});
});
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"lint": "deno lint",
"fmt": "deno fmt",
"test": "deno test --failfast --allow-net core/*_test.ts plugins/*_test.ts runtime/deno_test.ts --parallel",
"coverage": "rm -rf coverage && deno test --allow-net --parallel --coverage=coverage/cov_profile core/*_test.ts plugins/*_test.ts runtime/deno_test.ts && deno coverage --exclude=\"_test.ts\" --exclude=\"/testing/\" coverage/cov_profile",
"coverage": "rm -rf coverage && deno test --allow-net --parallel --coverage=coverage/cov_profile core/*_test.ts plugins/*_test.ts runtime/deno_test.ts && deno coverage --exclude=\"_test.ts\" --exclude=\"testing/\" --exclude=\"runtime/mod.ts\" coverage/cov_profile",
"test:integration": "deno task test:integration:ergo && deno task test:integration:inspircd && deno task test:integration:unrealircd",
"test:integration:ergo": "docker compose -f integration/ergo/docker-compose.yml down -v 2>/dev/null; docker compose -f integration/ergo/docker-compose.yml up -d --force-recreate && sleep 2 && IRCD=ergo deno test --allow-net --allow-read --allow-env integration/integration_test.ts --failfast ; docker compose -f integration/ergo/docker-compose.yml down -v",
"test:integration:inspircd": "docker compose -f integration/inspircd/docker-compose.yml down -v 2>/dev/null; docker compose -f integration/inspircd/docker-compose.yml up -d --force-recreate && sleep 3 && IRCD=inspircd deno test --allow-net --allow-read --allow-env integration/integration_test.ts --failfast ; docker compose -f integration/inspircd/docker-compose.yml down -v",
Expand Down
23 changes: 23 additions & 0 deletions plugins/batch_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ describe("plugins/batch", (test) => {
assertEquals(batches[1].count, 0);
});

test("ignore BATCH with no ref param", async () => {
const { client, server } = await mock();

let started = false;
client.on("batch_start", () => {
started = true;
});

server.send(":server BATCH");
await delay();

assertEquals(started, false);
});

test("pass through message with unknown batch tag", async () => {
const { client, server } = await mock();

server.send("@batch=unknown :nick!user@host PRIVMSG #ch :hello");
const msg = await client.once("raw:privmsg");

assertEquals(msg.params, ["#ch", "hello"]);
});

test("ignore unknown BATCH -ref", async () => {
const { client, server } = await mock();

Expand Down
33 changes: 33 additions & 0 deletions plugins/cap_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,39 @@ describe("plugins/cap", (test) => {
assertEquals(capReq!.includes("sasl"), true);
});

test("negotiate sends CAP END when no caps are supported", async () => {
const { client, server } = await mockWithCaps("unknown-only");

client.utils.negotiateCapabilities({
completeImmediately: true,
});
server.receive(); // CAP LS 302

server.send(":server CAP me LS :still-unknown");
await client.once("raw:cap");
await tick();

assertEquals(server.receive(), ["CAP END"]);
});

test("negotiate sends CAP END on NAK with completeImmediately", async () => {
const { client, server } = await mockWithCaps();

client.utils.negotiateCapabilities({ completeImmediately: true });
server.receive(); // CAP LS 302

server.send(":server CAP me LS :cap-notify multi-prefix");
await client.once("raw:cap");
await tick();
server.receive(); // CAP REQ

server.send(":server CAP me NAK :cap-notify multi-prefix");
await client.once("cap:nak");
await tick();

assertEquals(server.receive(), ["CAP END"]);
});

test("track CAP ACK", async () => {
const { client, server } = await mock();

Expand Down
32 changes: 32 additions & 0 deletions plugins/monitor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ describe("plugins/monitor", (test) => {
assertEquals(client.state.monitorList.has("nick1"), false);
});

test("send MONITOR - with array (remove)", async () => {
const { client, server } = await mock();

client.monitor.add(["nick1", "nick2"]);
server.receive();

client.monitor.remove(["nick1", "nick2"]);
assertEquals(server.receive(), ["MONITOR - nick1,nick2"]);
assertEquals(client.state.monitorList.size, 0);
});

test("send MONITOR L (list)", async () => {
const { client, server } = await mock();

Expand Down Expand Up @@ -127,4 +138,25 @@ describe("plugins/monitor", (test) => {

assertEquals(client.state.monitorLimit, 100);
});

test("ignore ISUPPORT MONITOR without value", async () => {
const { client, server } = await mock();

server.send(":serverhost 005 me MONITOR :are supported by this server");
await client.once("isupport:monitor");

assertEquals(client.state.monitorLimit, 0);
});

test("handle RPL_MONLIST with empty targets", async () => {
const { client, server } = await mock();

server.send([
":serverhost 732 me",
":serverhost 733 me :End of MONITOR list",
]);

const msg = await client.once("monitor_list");
assertEquals(msg.params, { nicks: [] });
});
});
Loading
Loading