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,337 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { topicReducer } from '../../src/commands/topic';
|
|
3
|
+
import { Effect } from '../../src/effects';
|
|
4
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
+
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
6
|
+
import { type ChannelState, createChannel } from '../../src/state/channel';
|
|
7
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
8
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
9
|
+
|
|
10
|
+
const serverConfig: ServerConfig = {
|
|
11
|
+
serverName: 'irc.example.com',
|
|
12
|
+
networkName: 'ExampleNet',
|
|
13
|
+
maxChannelsPerUser: 30,
|
|
14
|
+
maxTargetsPerCommand: 10,
|
|
15
|
+
maxListEntries: 50,
|
|
16
|
+
nickLen: 30,
|
|
17
|
+
channelLen: 50,
|
|
18
|
+
topicLen: 390,
|
|
19
|
+
quitMessage: 'Client Quit',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function makeCtx(conn: ConnectionState, clock = new FakeClock(1_000)): Ctx {
|
|
23
|
+
return buildCtx({
|
|
24
|
+
serverConfig,
|
|
25
|
+
clock,
|
|
26
|
+
ids: new SequentialIdFactory(),
|
|
27
|
+
motd: EmptyMotdProvider,
|
|
28
|
+
connection: conn,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeConn(id = 'c1', nick = 'alice'): ConnectionState {
|
|
33
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
34
|
+
s.nick = nick;
|
|
35
|
+
s.user = nick;
|
|
36
|
+
s.host = 'example.com';
|
|
37
|
+
s.realname = nick.charAt(0).toUpperCase() + nick.slice(1);
|
|
38
|
+
s.registration = 'registered';
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeChan(name = '#foo'): ChannelState {
|
|
43
|
+
return createChannel({ name, nameLower: name.toLowerCase(), createdAt: 0 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function addMember(chan: ChannelState, connId: string, nick: string, op = false): void {
|
|
47
|
+
chan.members.set(connId, { conn: connId, nick, op, voice: false });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const L = (text: string): RawLine => ({ text });
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// topicReducer — reads
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
describe('topicReducer — reads', () => {
|
|
57
|
+
it('emits 331 RPL_NOTOPIC when the channel has no topic', () => {
|
|
58
|
+
const chan = makeChan('#foo');
|
|
59
|
+
addMember(chan, 'c1', 'alice');
|
|
60
|
+
const conn = makeConn();
|
|
61
|
+
const ctx = makeCtx(conn);
|
|
62
|
+
|
|
63
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo'], tags: {} }, ctx);
|
|
64
|
+
|
|
65
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
66
|
+
Effect.send('c1', [L(':irc.example.com 331 alice #foo :No topic is set')]),
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('emits 332 RPL_TOPIC and 333 RPL_TOPICWHOTIME when the channel has a topic', () => {
|
|
71
|
+
const chan = makeChan('#foo');
|
|
72
|
+
chan.topic = { text: 'hello world', setter: 'alice!alice@example.com', setAt: 5_000 };
|
|
73
|
+
addMember(chan, 'c1', 'alice');
|
|
74
|
+
const conn = makeConn();
|
|
75
|
+
const ctx = makeCtx(conn);
|
|
76
|
+
|
|
77
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo'], tags: {} }, ctx);
|
|
78
|
+
|
|
79
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
80
|
+
Effect.send('c1', [
|
|
81
|
+
L(':irc.example.com 332 alice #foo :hello world'),
|
|
82
|
+
L(':irc.example.com 333 alice #foo alice!alice@example.com 5000'),
|
|
83
|
+
]),
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('updates the connection lastSeen to ctx.clock.now() on a read', () => {
|
|
88
|
+
const chan = makeChan('#foo');
|
|
89
|
+
addMember(chan, 'c1', 'alice');
|
|
90
|
+
const conn = makeConn();
|
|
91
|
+
const clock = new FakeClock(11_000);
|
|
92
|
+
const ctx = makeCtx(conn, clock);
|
|
93
|
+
|
|
94
|
+
topicReducer(chan, { command: 'TOPIC', params: ['#foo'], tags: {} }, ctx);
|
|
95
|
+
|
|
96
|
+
expect(conn.lastSeen).toBe(11_000);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// topicReducer — writes
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
describe('topicReducer — writes', () => {
|
|
105
|
+
it('sets the topic and broadcasts TOPIC to the channel when a member writes', () => {
|
|
106
|
+
const chan = makeChan('#foo');
|
|
107
|
+
addMember(chan, 'c1', 'alice');
|
|
108
|
+
const conn = makeConn();
|
|
109
|
+
const ctx = makeCtx(conn);
|
|
110
|
+
|
|
111
|
+
const out = topicReducer(
|
|
112
|
+
chan,
|
|
113
|
+
{ command: 'TOPIC', params: ['#foo', 'new topic'], tags: {} },
|
|
114
|
+
ctx,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(out.state.topic).toEqual({
|
|
118
|
+
text: 'new topic',
|
|
119
|
+
setter: 'alice!alice@example.com',
|
|
120
|
+
setAt: 1_000,
|
|
121
|
+
});
|
|
122
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
123
|
+
Effect.applyChannelDelta('#foo', {
|
|
124
|
+
topic: { text: 'new topic', setter: 'alice!alice@example.com', setAt: 1_000 },
|
|
125
|
+
}),
|
|
126
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com TOPIC #foo :new topic')]),
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('clears the topic when an empty topic is supplied', () => {
|
|
131
|
+
const chan = makeChan('#foo');
|
|
132
|
+
chan.topic = { text: 'old', setter: 'alice!alice@example.com', setAt: 1_000 };
|
|
133
|
+
addMember(chan, 'c1', 'alice');
|
|
134
|
+
const conn = makeConn();
|
|
135
|
+
const ctx = makeCtx(conn);
|
|
136
|
+
|
|
137
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo', ''], tags: {} }, ctx);
|
|
138
|
+
|
|
139
|
+
expect(out.state.topic).toBeUndefined();
|
|
140
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
141
|
+
Effect.applyChannelDelta('#foo', { topic: null }),
|
|
142
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com TOPIC #foo :')]),
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('uses ctx.clock.now() as the setAt timestamp', () => {
|
|
147
|
+
const chan = makeChan('#foo');
|
|
148
|
+
addMember(chan, 'c1', 'alice');
|
|
149
|
+
const conn = makeConn();
|
|
150
|
+
const clock = new FakeClock(99_999);
|
|
151
|
+
const ctx = makeCtx(conn, clock);
|
|
152
|
+
|
|
153
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo', 'hi'], tags: {} }, ctx);
|
|
154
|
+
|
|
155
|
+
expect(out.state.topic?.setAt).toBe(99_999);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('truncates the topic to serverConfig.topicLen bytes', () => {
|
|
159
|
+
const chan = makeChan('#foo');
|
|
160
|
+
addMember(chan, 'c1', 'alice');
|
|
161
|
+
const conn = makeConn();
|
|
162
|
+
const ctx = makeCtx(conn);
|
|
163
|
+
|
|
164
|
+
const longTopic = 'a'.repeat(serverConfig.topicLen + 10);
|
|
165
|
+
const out = topicReducer(
|
|
166
|
+
chan,
|
|
167
|
+
{ command: 'TOPIC', params: ['#foo', longTopic], tags: {} },
|
|
168
|
+
ctx,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(out.state.topic?.text).toBe('a'.repeat(serverConfig.topicLen));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('emits 482 ERR_CHANOPRIVSNEEDED when +t is set and the writer is a non-op member', () => {
|
|
175
|
+
const chan = makeChan('#foo');
|
|
176
|
+
chan.modes.topicLock = true;
|
|
177
|
+
addMember(chan, 'c1', 'alice', false);
|
|
178
|
+
const conn = makeConn();
|
|
179
|
+
const ctx = makeCtx(conn);
|
|
180
|
+
|
|
181
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo', 'new'], tags: {} }, ctx);
|
|
182
|
+
|
|
183
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
184
|
+
Effect.send('c1', [L(":irc.example.com 482 alice #foo :You're not channel operator")]),
|
|
185
|
+
]);
|
|
186
|
+
expect(out.state.topic).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('allows a non-op member to write when +t is NOT set (default)', () => {
|
|
190
|
+
const chan = makeChan('#foo');
|
|
191
|
+
addMember(chan, 'c1', 'alice', false);
|
|
192
|
+
const conn = makeConn();
|
|
193
|
+
const ctx = makeCtx(conn);
|
|
194
|
+
|
|
195
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo', 'new'], tags: {} }, ctx);
|
|
196
|
+
|
|
197
|
+
expect(out.state.topic?.text).toBe('new');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('allows an op to write when +t is set', () => {
|
|
201
|
+
const chan = makeChan('#foo');
|
|
202
|
+
chan.modes.topicLock = true;
|
|
203
|
+
addMember(chan, 'c1', 'alice', true);
|
|
204
|
+
const conn = makeConn();
|
|
205
|
+
const ctx = makeCtx(conn);
|
|
206
|
+
|
|
207
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo', 'new'], tags: {} }, ctx);
|
|
208
|
+
|
|
209
|
+
expect(out.state.topic?.text).toBe('new');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('falls back to ? as the TOPIC source when the writer has no nick (defensive)', () => {
|
|
213
|
+
const chan = makeChan('#foo');
|
|
214
|
+
addMember(chan, 'c1', '?');
|
|
215
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
216
|
+
const ctx = makeCtx(conn);
|
|
217
|
+
|
|
218
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo', 'new'], tags: {} }, ctx);
|
|
219
|
+
|
|
220
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
221
|
+
Effect.applyChannelDelta('#foo', {
|
|
222
|
+
topic: { text: 'new', setter: '?', setAt: 1_000 },
|
|
223
|
+
}),
|
|
224
|
+
Effect.broadcast('#foo', [L(':? TOPIC #foo :new')]),
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ============================================================================
|
|
230
|
+
// topicReducer — rejections
|
|
231
|
+
// ============================================================================
|
|
232
|
+
|
|
233
|
+
describe('topicReducer — rejections', () => {
|
|
234
|
+
it('emits 461 ERR_NEEDMOREPARAMS when no channel is supplied', () => {
|
|
235
|
+
const chan = makeChan('#foo');
|
|
236
|
+
addMember(chan, 'c1', 'alice');
|
|
237
|
+
const conn = makeConn();
|
|
238
|
+
const ctx = makeCtx(conn);
|
|
239
|
+
|
|
240
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: [], tags: {} }, ctx);
|
|
241
|
+
|
|
242
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
243
|
+
Effect.send('c1', [L(':irc.example.com 461 alice TOPIC :Not enough parameters')]),
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('emits 403 ERR_NOSUCHCHANNEL for a channel name without a valid prefix', () => {
|
|
248
|
+
const chan = makeChan('foo');
|
|
249
|
+
const conn = makeConn();
|
|
250
|
+
const ctx = makeCtx(conn);
|
|
251
|
+
|
|
252
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['foo'], tags: {} }, ctx);
|
|
253
|
+
|
|
254
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
255
|
+
Effect.send('c1', [L(':irc.example.com 403 alice foo :No such channel')]),
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('emits 403 ERR_NOSUCHCHANNEL for a channel name containing a comma', () => {
|
|
260
|
+
const chan = makeChan('#foo');
|
|
261
|
+
const conn = makeConn();
|
|
262
|
+
const ctx = makeCtx(conn);
|
|
263
|
+
|
|
264
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo,bar'], tags: {} }, ctx);
|
|
265
|
+
|
|
266
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
267
|
+
Effect.send('c1', [L(':irc.example.com 403 alice #foo,bar :No such channel')]),
|
|
268
|
+
]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('emits 403 ERR_NOSUCHCHANNEL for an empty channel name', () => {
|
|
272
|
+
const chan = makeChan('#foo');
|
|
273
|
+
const conn = makeConn();
|
|
274
|
+
const ctx = makeCtx(conn);
|
|
275
|
+
|
|
276
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: [''], tags: {} }, ctx);
|
|
277
|
+
|
|
278
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
279
|
+
Effect.send('c1', [L(':irc.example.com 403 alice :No such channel')]),
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('emits 442 ERR_NOTONCHANNEL when reading a channel the connection has not joined', () => {
|
|
284
|
+
const chan = makeChan('#foo');
|
|
285
|
+
addMember(chan, 'c2', 'bob');
|
|
286
|
+
const conn = makeConn();
|
|
287
|
+
const ctx = makeCtx(conn);
|
|
288
|
+
|
|
289
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo'], tags: {} }, ctx);
|
|
290
|
+
|
|
291
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
292
|
+
Effect.send('c1', [L(":irc.example.com 442 alice #foo :You're not on that channel")]),
|
|
293
|
+
]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('emits 442 ERR_NOTONCHANNEL when writing a channel the connection has not joined', () => {
|
|
297
|
+
const chan = makeChan('#foo');
|
|
298
|
+
addMember(chan, 'c2', 'bob');
|
|
299
|
+
const conn = makeConn();
|
|
300
|
+
const ctx = makeCtx(conn);
|
|
301
|
+
|
|
302
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo', 'new'], tags: {} }, ctx);
|
|
303
|
+
|
|
304
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
305
|
+
Effect.send('c1', [L(":irc.example.com 442 alice #foo :You're not on that channel")]),
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('uses * in numeric replies when the connection has no nick (defensive)', () => {
|
|
310
|
+
const chan = makeChan('#foo');
|
|
311
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
312
|
+
const ctx = makeCtx(conn);
|
|
313
|
+
|
|
314
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: [], tags: {} }, ctx);
|
|
315
|
+
|
|
316
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
317
|
+
Effect.send('c1', [L(':irc.example.com 461 * TOPIC :Not enough parameters')]),
|
|
318
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('uses * in 332/333 read replies when the reading member has no nick (defensive)', () => {
|
|
322
|
+
const chan = makeChan('#foo');
|
|
323
|
+
chan.topic = { text: 'hello', setter: 'alice!alice@example.com', setAt: 5_000 };
|
|
324
|
+
addMember(chan, 'c1', '?');
|
|
325
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
326
|
+
const ctx = makeCtx(conn);
|
|
327
|
+
|
|
328
|
+
const out = topicReducer(chan, { command: 'TOPIC', params: ['#foo'], tags: {} }, ctx);
|
|
329
|
+
|
|
330
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
331
|
+
Effect.send('c1', [
|
|
332
|
+
L(':irc.example.com 332 * #foo :hello'),
|
|
333
|
+
L(':irc.example.com 333 * #foo alice!alice@example.com 5000'),
|
|
334
|
+
]),
|
|
335
|
+
]);
|
|
336
|
+
});
|
|
337
|
+
});
|