serverless-ircd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +47 -0
- package/.github/workflows/deploy-cf.yml +97 -0
- package/LICENSE +29 -0
- package/README.md +323 -0
- package/apps/cf-worker/package.json +37 -0
- package/apps/cf-worker/scripts/smoke.mjs +175 -0
- package/apps/cf-worker/src/worker.ts +59 -0
- package/apps/cf-worker/tests/smoke.test.ts +150 -0
- package/apps/cf-worker/tsconfig.build.json +17 -0
- package/apps/cf-worker/tsconfig.test.json +18 -0
- package/apps/cf-worker/vitest.config.ts +31 -0
- package/apps/cf-worker/wrangler.test.toml +33 -0
- package/apps/cf-worker/wrangler.toml +98 -0
- package/apps/local-cli/package.json +35 -0
- package/apps/local-cli/src/line-scanner.ts +60 -0
- package/apps/local-cli/src/main.ts +145 -0
- package/apps/local-cli/src/motd-file.ts +45 -0
- package/apps/local-cli/src/server.ts +409 -0
- package/apps/local-cli/tests/e2e.test.ts +346 -0
- package/apps/local-cli/tests/id-factory.test.ts +25 -0
- package/apps/local-cli/tests/line-scanner.test.ts +81 -0
- package/apps/local-cli/tests/motd-file.test.ts +71 -0
- package/apps/local-cli/tests/tcp.test.ts +358 -0
- package/apps/local-cli/tsconfig.build.json +17 -0
- package/apps/local-cli/tsconfig.test.json +15 -0
- package/apps/local-cli/vitest.config.ts +24 -0
- package/biome.json +52 -0
- package/package.json +35 -0
- package/packages/cf-adapter/package.json +38 -0
- package/packages/cf-adapter/src/cf-runtime.ts +308 -0
- package/packages/cf-adapter/src/channel-do.ts +374 -0
- package/packages/cf-adapter/src/connection-do.ts +542 -0
- package/packages/cf-adapter/src/env.ts +67 -0
- package/packages/cf-adapter/src/index.ts +38 -0
- package/packages/cf-adapter/src/registry-do.ts +130 -0
- package/packages/cf-adapter/src/serialize.ts +142 -0
- package/packages/cf-adapter/src/sharding.ts +101 -0
- package/packages/cf-adapter/tests/cf-harness.ts +264 -0
- package/packages/cf-adapter/tests/cf-integration.test.ts +73 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +527 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +480 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +329 -0
- package/packages/cf-adapter/tests/integration/harness.test.ts +229 -0
- package/packages/cf-adapter/tests/integration/harness.ts +102 -0
- package/packages/cf-adapter/tests/integration/in-memory-harness.ts +242 -0
- package/packages/cf-adapter/tests/integration/index.ts +17 -0
- package/packages/cf-adapter/tests/integration/scenarios.test.ts +20 -0
- package/packages/cf-adapter/tests/integration/scenarios.ts +495 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +335 -0
- package/packages/cf-adapter/tests/sharding.test.ts +100 -0
- package/packages/cf-adapter/tests/worker/main.ts +33 -0
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +50 -0
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +57 -0
- package/packages/cf-adapter/tsconfig.build.json +16 -0
- package/packages/cf-adapter/tsconfig.test.json +18 -0
- package/packages/cf-adapter/vitest.config.ts +32 -0
- package/packages/cf-adapter/wrangler.test.toml +50 -0
- package/packages/in-memory-runtime/package.json +29 -0
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +245 -0
- package/packages/in-memory-runtime/src/index.ts +9 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +502 -0
- package/packages/in-memory-runtime/tsconfig.build.json +16 -0
- package/packages/in-memory-runtime/tsconfig.test.json +14 -0
- package/packages/in-memory-runtime/vitest.config.ts +21 -0
- package/packages/irc-core/package.json +29 -0
- package/packages/irc-core/src/caps/capabilities.ts +96 -0
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/commands/away.ts +82 -0
- package/packages/irc-core/src/commands/cap.ts +257 -0
- package/packages/irc-core/src/commands/chghost.ts +30 -0
- package/packages/irc-core/src/commands/index.ts +40 -0
- package/packages/irc-core/src/commands/invite.ts +156 -0
- package/packages/irc-core/src/commands/isupport.ts +133 -0
- package/packages/irc-core/src/commands/join.ts +309 -0
- package/packages/irc-core/src/commands/kick.ts +162 -0
- package/packages/irc-core/src/commands/list.ts +126 -0
- package/packages/irc-core/src/commands/mode.ts +655 -0
- package/packages/irc-core/src/commands/motd.ts +146 -0
- package/packages/irc-core/src/commands/names.ts +164 -0
- package/packages/irc-core/src/commands/part.ts +118 -0
- package/packages/irc-core/src/commands/ping.ts +47 -0
- package/packages/irc-core/src/commands/privmsg.ts +256 -0
- package/packages/irc-core/src/commands/quit.ts +70 -0
- package/packages/irc-core/src/commands/registration.ts +251 -0
- package/packages/irc-core/src/commands/topic.ts +171 -0
- package/packages/irc-core/src/commands/who.ts +169 -0
- package/packages/irc-core/src/commands/whois.ts +165 -0
- package/packages/irc-core/src/effects.ts +184 -0
- package/packages/irc-core/src/index.ts +20 -0
- package/packages/irc-core/src/ports.ts +153 -0
- package/packages/irc-core/src/protocol/batch.ts +85 -0
- package/packages/irc-core/src/protocol/index.ts +18 -0
- package/packages/irc-core/src/protocol/messages.ts +36 -0
- package/packages/irc-core/src/protocol/numerics.ts +155 -0
- package/packages/irc-core/src/protocol/outbound.ts +145 -0
- package/packages/irc-core/src/protocol/parser.ts +175 -0
- package/packages/irc-core/src/protocol/serializer.ts +71 -0
- package/packages/irc-core/src/state/channel.ts +293 -0
- package/packages/irc-core/src/state/connection.ts +153 -0
- package/packages/irc-core/src/state/index.ts +25 -0
- package/packages/irc-core/src/state/registry.ts +22 -0
- package/packages/irc-core/src/types.ts +83 -0
- package/packages/irc-core/tests/batch.test.ts +79 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +148 -0
- package/packages/irc-core/tests/commands/away.test.ts +233 -0
- package/packages/irc-core/tests/commands/batch.test.ts +178 -0
- package/packages/irc-core/tests/commands/cap.test.ts +499 -0
- package/packages/irc-core/tests/commands/chghost.test.ts +186 -0
- package/packages/irc-core/tests/commands/echo-message.test.ts +212 -0
- package/packages/irc-core/tests/commands/extended-join.test.ts +147 -0
- package/packages/irc-core/tests/commands/invite-notify.test.ts +160 -0
- package/packages/irc-core/tests/commands/invite.test.ts +321 -0
- package/packages/irc-core/tests/commands/isupport.test.ts +209 -0
- package/packages/irc-core/tests/commands/join.test.ts +687 -0
- package/packages/irc-core/tests/commands/kick.test.ts +316 -0
- package/packages/irc-core/tests/commands/list.test.ts +452 -0
- package/packages/irc-core/tests/commands/mode.test.ts +1048 -0
- package/packages/irc-core/tests/commands/motd.test.ts +322 -0
- package/packages/irc-core/tests/commands/names.test.ts +342 -0
- package/packages/irc-core/tests/commands/part.test.ts +265 -0
- package/packages/irc-core/tests/commands/ping.test.ts +144 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +665 -0
- package/packages/irc-core/tests/commands/quit.test.ts +220 -0
- package/packages/irc-core/tests/commands/registration.test.ts +599 -0
- package/packages/irc-core/tests/commands/topic.test.ts +337 -0
- package/packages/irc-core/tests/commands/who.test.ts +441 -0
- package/packages/irc-core/tests/commands/whois.test.ts +422 -0
- package/packages/irc-core/tests/effects.test.ts +147 -0
- package/packages/irc-core/tests/message-tags.test.ts +182 -0
- package/packages/irc-core/tests/numerics.test.ts +98 -0
- package/packages/irc-core/tests/outbound.test.ts +232 -0
- package/packages/irc-core/tests/parser.test.ts +162 -0
- package/packages/irc-core/tests/ports.test.ts +101 -0
- package/packages/irc-core/tests/serializer.test.ts +164 -0
- package/packages/irc-core/tests/state/channel.test.ts +351 -0
- package/packages/irc-core/tests/state/connection.test.ts +122 -0
- package/packages/irc-core/tests/state/registry.test.ts +20 -0
- package/packages/irc-core/tests/types.test.ts +127 -0
- package/packages/irc-core/tsconfig.build.json +12 -0
- package/packages/irc-core/tsconfig.test.json +10 -0
- package/packages/irc-core/vitest.config.ts +21 -0
- package/packages/irc-server/package.json +28 -0
- package/packages/irc-server/src/actor.ts +449 -0
- package/packages/irc-server/src/dispatch.ts +120 -0
- package/packages/irc-server/src/index.ts +11 -0
- package/packages/irc-server/src/runtime.ts +61 -0
- package/packages/irc-server/tests/actor.test.ts +816 -0
- package/packages/irc-server/tests/dispatch.test.ts +512 -0
- package/packages/irc-server/tests/runtime.test.ts +52 -0
- package/packages/irc-server/tsconfig.build.json +13 -0
- package/packages/irc-server/tsconfig.test.json +11 -0
- package/packages/irc-server/vitest.config.ts +21 -0
- package/pnpm-workspace.yaml +4 -0
- package/tools/tcp-ws-forwarder/package.json +32 -0
- package/tools/tcp-ws-forwarder/src/forwarder.ts +244 -0
- package/tools/tcp-ws-forwarder/src/line-scanner.ts +55 -0
- package/tools/tcp-ws-forwarder/src/main.ts +114 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +618 -0
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +51 -0
- package/tools/tcp-ws-forwarder/tests/line-scanner.test.ts +75 -0
- package/tools/tcp-ws-forwarder/tsconfig.build.json +12 -0
- package/tools/tcp-ws-forwarder/tsconfig.test.json +10 -0
- package/tools/tcp-ws-forwarder/vitest.config.ts +27 -0
- package/tsconfig.base.json +25 -0
- package/turbo.json +29 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable IRC scenario runner — exported so adapter packages (cf-adapter,
|
|
3
|
+
* aws-stack) can register their own {@link IrcHarnessFactory} and re-run
|
|
4
|
+
* the exact same scenario suite under their native runtime.
|
|
5
|
+
*
|
|
6
|
+
* Each scenario is written once against the {@link IrcHarness} seam. The
|
|
7
|
+
* runner takes a list of factories and produces one `describe` block per
|
|
8
|
+
* factory, mirroring the parametrized matrix called for in PLAN §8
|
|
9
|
+
* ("Runtime contract: vitest parametrized over IrcRuntime. Same scenarios
|
|
10
|
+
* pass in-memory + CF + AWS.").
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import type { IrcHarness, IrcHarnessFactory } from './harness.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs the full IRC scenario suite against every factory in `factories`.
|
|
18
|
+
*
|
|
19
|
+
* Adapter packages call this from their own vitest config:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { describe } from 'vitest';
|
|
23
|
+
* import { runIrcScenarios } from './scenarios.js';
|
|
24
|
+
* import { cfHarnessFactory } from './cf-harness.js';
|
|
25
|
+
*
|
|
26
|
+
* runIrcScenarios([cfHarnessFactory]);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* The function defines the `describe`/`it` tree at module load time so
|
|
30
|
+
* vitest discovers them like any other test file.
|
|
31
|
+
*/
|
|
32
|
+
export function runIrcScenarios(factories: readonly IrcHarnessFactory[]): void {
|
|
33
|
+
/** Finds the first numeric in an IRC line (`:srv 001 nick ...` → `"001"`). */
|
|
34
|
+
function numericOf(line: string): string | undefined {
|
|
35
|
+
const parts = line.split(' ');
|
|
36
|
+
for (const p of parts) {
|
|
37
|
+
if (/^\d{3}$/u.test(p)) return p;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Synchronization helper: alice (already in #chan) waits until she sees
|
|
44
|
+
* bob's JOIN broadcast. By the time this resolves, the runtime has also
|
|
45
|
+
* finished updating the roster and pushing the 353 NAMES list to bob.
|
|
46
|
+
*/
|
|
47
|
+
type ClientWaitFor = (
|
|
48
|
+
predicate: (line: string) => boolean,
|
|
49
|
+
timeoutMs?: number,
|
|
50
|
+
) => Promise<string>;
|
|
51
|
+
|
|
52
|
+
async function untilAliceSeesBobJoin(
|
|
53
|
+
alice: { waitForLine: ClientWaitFor },
|
|
54
|
+
channel: string,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
await alice.waitForLine((l) => l.includes('JOIN') && l.includes(channel) && l.includes('bob!'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const factory of factories) {
|
|
60
|
+
describe(`IrcRuntime contract — ${factory.name}`, () => {
|
|
61
|
+
let harness: IrcHarness;
|
|
62
|
+
|
|
63
|
+
it('scenario 01: NICK + USER produces the welcome 001..005 + MOTD block', async () => {
|
|
64
|
+
harness = await factory.create();
|
|
65
|
+
const alice = await harness.spawnClient();
|
|
66
|
+
await alice.send('NICK alice');
|
|
67
|
+
await alice.send('USER alice 0 * :Alice In Wonderland');
|
|
68
|
+
await alice.waitForNumeric('001');
|
|
69
|
+
const codes = alice.received.map(numericOf).filter((c): c is string => c !== undefined);
|
|
70
|
+
expect(codes.slice(0, 4)).toEqual(['001', '002', '003', '004']);
|
|
71
|
+
expect(codes).toContain('005');
|
|
72
|
+
expect(codes).toContain('375');
|
|
73
|
+
expect(codes).toContain('372');
|
|
74
|
+
expect(codes).toContain('376');
|
|
75
|
+
await harness.close();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('scenario 02: CAP LS returns the server capability list', async () => {
|
|
79
|
+
harness = await factory.create();
|
|
80
|
+
const c = await harness.spawnClient();
|
|
81
|
+
await c.send('CAP LS');
|
|
82
|
+
await c.waitForLine((l) => l.includes(' CAP * LS :'));
|
|
83
|
+
await harness.close();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('scenario 03: CAP REQ for a supported cap is ACKed', async () => {
|
|
87
|
+
harness = await factory.create();
|
|
88
|
+
const c = await harness.spawnClient();
|
|
89
|
+
await c.send('CAP LS');
|
|
90
|
+
await c.waitForLine((l) => l.includes(' CAP * LS :'));
|
|
91
|
+
await c.send('CAP REQ :echo-message');
|
|
92
|
+
await c.waitForLine((l) => l.includes(' CAP * ACK :echo-message'));
|
|
93
|
+
await harness.close();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('scenario 04: CAP END completes registration when NICK and USER are set', async () => {
|
|
97
|
+
harness = await factory.create();
|
|
98
|
+
const c = await harness.spawnClient();
|
|
99
|
+
await c.send('CAP LS');
|
|
100
|
+
await c.send('NICK bob');
|
|
101
|
+
await c.send('USER bob 0 * :Bob');
|
|
102
|
+
expect(c.received.some((l) => numericOf(l) === '001')).toBe(false);
|
|
103
|
+
await c.send('CAP END');
|
|
104
|
+
await c.waitForNumeric('001');
|
|
105
|
+
await harness.close();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('scenario 05: invalid nick grammar → 432 ERR_ERRONEUSNICKNAME', async () => {
|
|
109
|
+
harness = await factory.create();
|
|
110
|
+
const c = await harness.spawnClient();
|
|
111
|
+
await c.send('NICK 1bad');
|
|
112
|
+
await c.waitForNumeric('432');
|
|
113
|
+
await harness.close();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('scenario 06: channel MODE +t (topic-lock) gates topic changes to ops (482)', async () => {
|
|
117
|
+
harness = await factory.create();
|
|
118
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
119
|
+
await alice.waitForNumeric('001');
|
|
120
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
121
|
+
await bob.waitForNumeric('001');
|
|
122
|
+
await alice.send('JOIN #lock');
|
|
123
|
+
await alice.waitForNumeric('366');
|
|
124
|
+
await bob.send('JOIN #lock');
|
|
125
|
+
await untilAliceSeesBobJoin(alice, '#lock');
|
|
126
|
+
await alice.send('MODE #lock +t');
|
|
127
|
+
await alice.waitForLine((l) => l.includes('MODE #lock +t'));
|
|
128
|
+
await bob.send('TOPIC #lock :bob should not be able to set this');
|
|
129
|
+
await bob.waitForNumeric('482');
|
|
130
|
+
await harness.close();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('scenario 07: USER after registration → 462 ERR_ALREADYREGISTRED', async () => {
|
|
134
|
+
harness = await factory.create();
|
|
135
|
+
const c = await harness.spawnClient({ nick: 'alice' });
|
|
136
|
+
await c.waitForNumeric('001');
|
|
137
|
+
await c.send('USER alice 0 * :Alice');
|
|
138
|
+
await c.waitForNumeric('462');
|
|
139
|
+
await harness.close();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('scenario 08: PING <token> → PONG <token>', async () => {
|
|
143
|
+
harness = await factory.create();
|
|
144
|
+
const c = await harness.spawnClient({ nick: 'alice' });
|
|
145
|
+
await c.waitForNumeric('001');
|
|
146
|
+
await c.send('PING :abc-123');
|
|
147
|
+
await c.waitForLine((l) => l === 'PONG :abc-123');
|
|
148
|
+
await harness.close();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('scenario 09: JOIN #chan → self sees JOIN + 353 NAMES + 366 end-of-names', async () => {
|
|
152
|
+
harness = await factory.create();
|
|
153
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
154
|
+
await alice.waitForNumeric('001');
|
|
155
|
+
await alice.send('JOIN #foo');
|
|
156
|
+
await alice.waitForLine((l) => l.includes('JOIN #foo'));
|
|
157
|
+
await alice.waitForNumeric('353');
|
|
158
|
+
await alice.waitForNumeric('366');
|
|
159
|
+
const namesLine = alice.received.find((l) => numericOf(l) === '353');
|
|
160
|
+
expect(namesLine).toBeDefined();
|
|
161
|
+
expect(namesLine ?? '').toContain('@alice');
|
|
162
|
+
await harness.close();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('scenario 10: JOIN #a,#b joins both channels', async () => {
|
|
166
|
+
harness = await factory.create();
|
|
167
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
168
|
+
await alice.waitForNumeric('001');
|
|
169
|
+
await alice.send('JOIN #a,#b');
|
|
170
|
+
await alice.waitForLine((l) => l.includes('JOIN #a'));
|
|
171
|
+
await alice.waitForLine((l) => l.includes('JOIN #b'));
|
|
172
|
+
const endNames = alice.received.filter((l) => numericOf(l) === '366');
|
|
173
|
+
expect(endNames.length).toBe(2);
|
|
174
|
+
await harness.close();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('scenario 11: JOIN 0 parts all previously joined channels', async () => {
|
|
178
|
+
harness = await factory.create();
|
|
179
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
180
|
+
await alice.waitForNumeric('001');
|
|
181
|
+
await alice.send('JOIN #foo,#bar');
|
|
182
|
+
await alice.waitForLine((l) => l.includes('JOIN #foo'));
|
|
183
|
+
await alice.waitForLine((l) => l.includes('JOIN #bar'));
|
|
184
|
+
alice.received.length = 0;
|
|
185
|
+
await alice.send('JOIN 0');
|
|
186
|
+
await alice.waitForLine((l) => l.includes('PART'));
|
|
187
|
+
const parts = alice.received.filter((l) => l.includes('PART'));
|
|
188
|
+
expect(parts.length).toBe(2);
|
|
189
|
+
expect(parts.some((l) => l.includes('#foo'))).toBe(true);
|
|
190
|
+
expect(parts.some((l) => l.includes('#bar'))).toBe(true);
|
|
191
|
+
await harness.close();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('scenario 12: PART #chan broadcasts PART and removes the user', async () => {
|
|
195
|
+
harness = await factory.create();
|
|
196
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
197
|
+
await alice.waitForNumeric('001');
|
|
198
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
199
|
+
await bob.waitForNumeric('001');
|
|
200
|
+
await alice.send('JOIN #room');
|
|
201
|
+
await bob.send('JOIN #room');
|
|
202
|
+
await untilAliceSeesBobJoin(alice, '#room');
|
|
203
|
+
await alice.send('PART #room :leaving');
|
|
204
|
+
await bob.waitForLine((l) => l.includes('PART #room') && l.includes('leaving'));
|
|
205
|
+
await harness.close();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('scenario 13: channel PRIVMSG broadcasts to other members, not the sender', async () => {
|
|
209
|
+
harness = await factory.create();
|
|
210
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
211
|
+
await alice.waitForNumeric('001');
|
|
212
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
213
|
+
await bob.waitForNumeric('001');
|
|
214
|
+
await alice.send('JOIN #room');
|
|
215
|
+
await bob.send('JOIN #room');
|
|
216
|
+
await untilAliceSeesBobJoin(alice, '#room');
|
|
217
|
+
alice.received.length = 0;
|
|
218
|
+
bob.received.length = 0;
|
|
219
|
+
await alice.send('PRIVMSG #room :hello team');
|
|
220
|
+
await bob.waitForLine((l) => l.includes('PRIVMSG #room :hello team'));
|
|
221
|
+
expect(alice.received.some((l) => l.includes('PRIVMSG #room :hello team'))).toBe(false);
|
|
222
|
+
await harness.close();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('scenario 14: private PRIVMSG to another user is delivered to recipient only', async () => {
|
|
226
|
+
harness = await factory.create();
|
|
227
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
228
|
+
await alice.waitForNumeric('001');
|
|
229
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
230
|
+
await bob.waitForNumeric('001');
|
|
231
|
+
bob.received.length = 0;
|
|
232
|
+
await alice.send('PRIVMSG bob :hey');
|
|
233
|
+
await bob.waitForLine((l) => l.includes('PRIVMSG bob :hey') && l.startsWith(':alice'));
|
|
234
|
+
await harness.close();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('scenario 15: PRIVMSG to an offline nick → 401 ERR_NOSUCHNICK', async () => {
|
|
238
|
+
harness = await factory.create();
|
|
239
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
240
|
+
await alice.waitForNumeric('001');
|
|
241
|
+
await alice.send('PRIVMSG ghost :hello?');
|
|
242
|
+
await alice.waitForNumeric('401');
|
|
243
|
+
await harness.close();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('scenario 16: TOPIC set then read', async () => {
|
|
247
|
+
harness = await factory.create();
|
|
248
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
249
|
+
await alice.waitForNumeric('001');
|
|
250
|
+
await alice.send('JOIN #topic');
|
|
251
|
+
await alice.waitForNumeric('366');
|
|
252
|
+
alice.received.length = 0;
|
|
253
|
+
await alice.send('TOPIC #topic :Welcome to the topic');
|
|
254
|
+
await alice.waitForLine((l) => l.includes('TOPIC #topic'));
|
|
255
|
+
alice.received.length = 0;
|
|
256
|
+
await alice.send('TOPIC #topic');
|
|
257
|
+
await alice.waitForNumeric('332');
|
|
258
|
+
await alice.waitForNumeric('333');
|
|
259
|
+
await harness.close();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('scenario 17: NAMES #chan → 353 list + 366 end', async () => {
|
|
263
|
+
harness = await factory.create();
|
|
264
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
265
|
+
await alice.waitForNumeric('001');
|
|
266
|
+
await alice.send('JOIN #names');
|
|
267
|
+
await alice.waitForNumeric('366');
|
|
268
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
269
|
+
await bob.waitForNumeric('001');
|
|
270
|
+
await bob.send('JOIN #names');
|
|
271
|
+
await untilAliceSeesBobJoin(alice, '#names');
|
|
272
|
+
alice.received.length = 0;
|
|
273
|
+
await alice.send('NAMES #names');
|
|
274
|
+
const namesLine = await alice.waitForNumeric('353');
|
|
275
|
+
expect(namesLine).toContain('alice');
|
|
276
|
+
expect(namesLine).toContain('bob');
|
|
277
|
+
await alice.waitForNumeric('366');
|
|
278
|
+
await harness.close();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('scenario 18: MOTD on a configured server emits 375 + 372×N + 376', async () => {
|
|
282
|
+
harness = await factory.create();
|
|
283
|
+
const c = await harness.spawnClient({ nick: 'alice' });
|
|
284
|
+
await c.waitForNumeric('001');
|
|
285
|
+
c.received.length = 0;
|
|
286
|
+
await c.send('MOTD');
|
|
287
|
+
await c.waitForNumeric('375');
|
|
288
|
+
const body = await c.waitForNumeric('372');
|
|
289
|
+
expect(body).toContain('ServerlessIRCd integration-test harness');
|
|
290
|
+
await c.waitForNumeric('376');
|
|
291
|
+
await harness.close();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('scenario 19: channel MODE +t by op is broadcast to the channel', async () => {
|
|
295
|
+
harness = await factory.create();
|
|
296
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
297
|
+
await alice.waitForNumeric('001');
|
|
298
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
299
|
+
await bob.waitForNumeric('001');
|
|
300
|
+
await alice.send('JOIN #mode');
|
|
301
|
+
await bob.send('JOIN #mode');
|
|
302
|
+
await untilAliceSeesBobJoin(alice, '#mode');
|
|
303
|
+
bob.received.length = 0;
|
|
304
|
+
await alice.send('MODE #mode +t');
|
|
305
|
+
await bob.waitForLine((l) => l.includes('MODE #mode +t'));
|
|
306
|
+
await harness.close();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('scenario 20: user MODE +i echoes back to the user', async () => {
|
|
310
|
+
harness = await factory.create();
|
|
311
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
312
|
+
await alice.waitForNumeric('001');
|
|
313
|
+
alice.received.length = 0;
|
|
314
|
+
await alice.send('MODE alice +i');
|
|
315
|
+
await alice.waitForLine((l) => l.includes('MODE alice +i'));
|
|
316
|
+
await harness.close();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('scenario 21: KICK by op broadcasts to channel peers and removes the target', async () => {
|
|
320
|
+
harness = await factory.create();
|
|
321
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
322
|
+
await alice.waitForNumeric('001');
|
|
323
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
324
|
+
await bob.waitForNumeric('001');
|
|
325
|
+
await alice.send('JOIN #kick');
|
|
326
|
+
await bob.send('JOIN #kick');
|
|
327
|
+
await untilAliceSeesBobJoin(alice, '#kick');
|
|
328
|
+
alice.received.length = 0;
|
|
329
|
+
await alice.send('KICK #kick bob :shape up');
|
|
330
|
+
await alice.waitForLine((l) => l.includes('KICK #kick bob') && l.includes('shape up'));
|
|
331
|
+
alice.received.length = 0;
|
|
332
|
+
await alice.send('NAMES #kick');
|
|
333
|
+
const namesLine = await alice.waitForNumeric('353');
|
|
334
|
+
expect(namesLine).toContain('alice');
|
|
335
|
+
expect(namesLine).not.toContain('bob');
|
|
336
|
+
await harness.close();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('scenario 22: channel MODE +i (invite-only) rejects a fresh JOIN with 473', async () => {
|
|
340
|
+
harness = await factory.create();
|
|
341
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
342
|
+
await alice.waitForNumeric('001');
|
|
343
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
344
|
+
await bob.waitForNumeric('001');
|
|
345
|
+
await alice.send('JOIN #inviteonly');
|
|
346
|
+
await alice.waitForNumeric('366');
|
|
347
|
+
await alice.send('MODE #inviteonly +i');
|
|
348
|
+
await alice.waitForLine((l) => l.includes('MODE #inviteonly +i'));
|
|
349
|
+
await bob.send('JOIN #inviteonly');
|
|
350
|
+
await bob.waitForNumeric('473');
|
|
351
|
+
await harness.close();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('scenario 23: QUIT broadcasts to channel peers and releases the nick', async () => {
|
|
355
|
+
harness = await factory.create();
|
|
356
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
357
|
+
await alice.waitForNumeric('001');
|
|
358
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
359
|
+
await bob.waitForNumeric('001');
|
|
360
|
+
await alice.send('JOIN #quit');
|
|
361
|
+
await bob.send('JOIN #quit');
|
|
362
|
+
await untilAliceSeesBobJoin(alice, '#quit');
|
|
363
|
+
bob.received.length = 0;
|
|
364
|
+
await alice.send('QUIT :goodbye');
|
|
365
|
+
await bob.waitForLine((l) => l.includes('QUIT') && l.includes('goodbye'));
|
|
366
|
+
const aliceAgain = await harness.spawnClient();
|
|
367
|
+
await aliceAgain.send('NICK alice');
|
|
368
|
+
await aliceAgain.send('USER alice 0 * :Alice Again');
|
|
369
|
+
await aliceAgain.waitForNumeric('001');
|
|
370
|
+
await harness.close();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('scenario 24: second joiner sees the first member in the NAMES list', async () => {
|
|
374
|
+
harness = await factory.create();
|
|
375
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
376
|
+
await alice.waitForNumeric('001');
|
|
377
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
378
|
+
await bob.waitForNumeric('001');
|
|
379
|
+
bob.received.length = 0;
|
|
380
|
+
await alice.send('JOIN #shared');
|
|
381
|
+
await alice.waitForNumeric('366');
|
|
382
|
+
await bob.send('JOIN #shared');
|
|
383
|
+
const namesLine = await bob.waitForNumeric('353');
|
|
384
|
+
expect(namesLine).toContain('alice');
|
|
385
|
+
expect(namesLine).toContain('bob');
|
|
386
|
+
await harness.close();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('scenario 25: PRIVMSG by an echo-message client is reflected back to the sender', async () => {
|
|
390
|
+
harness = await factory.create();
|
|
391
|
+
const alice = await harness.spawnClient();
|
|
392
|
+
await alice.send('CAP LS');
|
|
393
|
+
await alice.waitForLine((l) => l.includes(' CAP * LS :'));
|
|
394
|
+
await alice.send('CAP REQ :echo-message');
|
|
395
|
+
await alice.waitForLine((l) => l.includes(' CAP * ACK :echo-message'));
|
|
396
|
+
await alice.send('NICK alice');
|
|
397
|
+
await alice.send('USER alice 0 * :Alice');
|
|
398
|
+
await alice.send('CAP END');
|
|
399
|
+
await alice.waitForNumeric('001');
|
|
400
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
401
|
+
await bob.waitForNumeric('001');
|
|
402
|
+
await alice.send('JOIN #echo');
|
|
403
|
+
await bob.send('JOIN #echo');
|
|
404
|
+
await untilAliceSeesBobJoin(alice, '#echo');
|
|
405
|
+
alice.received.length = 0;
|
|
406
|
+
bob.received.length = 0;
|
|
407
|
+
await alice.send('PRIVMSG #echo :mirrored');
|
|
408
|
+
await alice.waitForLine(
|
|
409
|
+
(l) => l.startsWith(':alice') && l.includes('PRIVMSG #echo :mirrored'),
|
|
410
|
+
);
|
|
411
|
+
await bob.waitForLine((l) => l.includes('PRIVMSG #echo :mirrored'));
|
|
412
|
+
await harness.close();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('scenario 26: NOTICE to a channel never produces a numeric error', async () => {
|
|
416
|
+
harness = await factory.create();
|
|
417
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
418
|
+
await alice.waitForNumeric('001');
|
|
419
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
420
|
+
await bob.waitForNumeric('001');
|
|
421
|
+
await alice.send('JOIN #notice');
|
|
422
|
+
await bob.send('JOIN #notice');
|
|
423
|
+
await untilAliceSeesBobJoin(alice, '#notice');
|
|
424
|
+
alice.received.length = 0;
|
|
425
|
+
bob.received.length = 0;
|
|
426
|
+
await alice.send('NOTICE #notice :quiet note');
|
|
427
|
+
await bob.waitForLine((l) => l.includes('NOTICE #notice :quiet note'));
|
|
428
|
+
const numericAfterNotice = alice.received.some((l) => numericOf(l) !== undefined);
|
|
429
|
+
expect(numericAfterNotice).toBe(false);
|
|
430
|
+
await harness.close();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('scenario 27: PASS after registration is rejected with 462', async () => {
|
|
434
|
+
harness = await factory.create();
|
|
435
|
+
const c = await harness.spawnClient({ nick: 'alice' });
|
|
436
|
+
await c.waitForNumeric('001');
|
|
437
|
+
c.received.length = 0;
|
|
438
|
+
await c.send('PASS hunter2');
|
|
439
|
+
await c.waitForNumeric('462');
|
|
440
|
+
await harness.close();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('scenario 28: joined frame with multiple commands processes each in order', async () => {
|
|
444
|
+
harness = await factory.create();
|
|
445
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
446
|
+
await alice.waitForNumeric('001');
|
|
447
|
+
alice.received.length = 0;
|
|
448
|
+
await alice.send('PING :a\r\nPING :b\r\nPING :c');
|
|
449
|
+
await alice.waitForLine((l) => l === 'PONG :c');
|
|
450
|
+
const pongs = alice.received.filter((l) => l.startsWith('PONG :'));
|
|
451
|
+
expect(pongs).toEqual(['PONG :a', 'PONG :b', 'PONG :c']);
|
|
452
|
+
await harness.close();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('scenario 29: unparseable line emits 421 and the actor stays alive', async () => {
|
|
456
|
+
harness = await factory.create();
|
|
457
|
+
const c = await harness.spawnClient({ nick: 'alice' });
|
|
458
|
+
await c.waitForNumeric('001');
|
|
459
|
+
c.received.length = 0;
|
|
460
|
+
await c.send('12345');
|
|
461
|
+
await c.waitForNumeric('421');
|
|
462
|
+
await c.send('PING :still-alive');
|
|
463
|
+
await c.waitForLine((l) => l === 'PONG :still-alive');
|
|
464
|
+
await harness.close();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('scenario 30: channel MODE +o grants op and broadcasts the change', async () => {
|
|
468
|
+
harness = await factory.create();
|
|
469
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
470
|
+
await alice.waitForNumeric('001');
|
|
471
|
+
const bob = await harness.spawnClient({ nick: 'bob' });
|
|
472
|
+
await bob.waitForNumeric('001');
|
|
473
|
+
await alice.send('JOIN #op');
|
|
474
|
+
await bob.send('JOIN #op');
|
|
475
|
+
await untilAliceSeesBobJoin(alice, '#op');
|
|
476
|
+
alice.received.length = 0;
|
|
477
|
+
bob.received.length = 0;
|
|
478
|
+
await alice.send('MODE #op +o bob');
|
|
479
|
+
await bob.waitForLine((l) => l.includes('MODE #op +o bob'));
|
|
480
|
+
await alice.waitForLine((l) => l.includes('MODE #op +o bob'));
|
|
481
|
+
await harness.close();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('scenario 31: NICK change post-registration echoes :old NICK new', async () => {
|
|
485
|
+
harness = await factory.create();
|
|
486
|
+
const alice = await harness.spawnClient({ nick: 'alice' });
|
|
487
|
+
await alice.waitForNumeric('001');
|
|
488
|
+
alice.received.length = 0;
|
|
489
|
+
await alice.send('NICK alice2');
|
|
490
|
+
await alice.waitForLine((l) => l.startsWith(':alice!') && l.includes('NICK alice2'));
|
|
491
|
+
await harness.close();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|