serverless-ircd 0.7.0 → 0.8.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 +3 -3
- package/.gitmodules +1 -1
- package/CHANGELOG.md +273 -29
- package/README.md +155 -55
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +186 -18
- package/apps/aws-stack/tests/stack.test.ts +400 -56
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/worker.ts +4 -4
- package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
- package/apps/cf-worker/tests/smoke.test.ts +5 -5
- package/apps/cf-worker/wrangler.test.toml +6 -6
- package/apps/cf-worker/wrangler.toml +7 -5
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +8 -0
- package/apps/local-cli/src/main.ts +16 -0
- package/apps/local-cli/src/server.ts +1 -0
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/web/landing/index.html +14 -16
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +23 -16
- package/apps/web/src/build-env.ts +1 -1
- package/apps/web/src/config-schema.ts +6 -6
- package/apps/web/tests/build-smoke.test.ts +14 -14
- package/apps/web/tests/config-schema.test.ts +1 -1
- package/docs/AWS-Deployment.md +21 -8
- package/docs/Cloudflare-Deployment-Guide.md +22 -6
- package/docs/PlanExtensions.md +113 -3
- package/docs/Release-Process.md +23 -13
- package/docs/Services.md +546 -0
- package/docs/WebClientGuide.md +32 -31
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +5 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
- package/packages/aws-adapter/src/config-loader.ts +11 -0
- package/packages/aws-adapter/src/connection-counter.ts +89 -0
- package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
- package/packages/aws-adapter/src/handlers/connect.ts +55 -51
- package/packages/aws-adapter/src/handlers/default.ts +36 -4
- package/packages/aws-adapter/src/handlers/index.ts +15 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +15 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
- package/packages/aws-adapter/src/index.ts +4 -0
- package/packages/aws-adapter/src/stats.ts +6 -1
- package/packages/aws-adapter/src/tables.ts +34 -4
- package/packages/aws-adapter/tests/aws-harness.ts +3 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
- package/packages/aws-adapter/tests/connect.test.ts +158 -32
- package/packages/aws-adapter/tests/connection-counter.test.ts +127 -0
- package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
- package/packages/aws-adapter/tests/handlers.test.ts +105 -3
- package/packages/aws-adapter/tests/tables.test.ts +6 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/config-loader.ts +11 -0
- package/packages/cf-adapter/src/connection-do.ts +112 -2
- package/packages/cf-adapter/src/d1-services-store.ts +703 -0
- package/packages/cf-adapter/src/env.ts +8 -0
- package/packages/cf-adapter/src/index.ts +5 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
- package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
- package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
- package/packages/cf-adapter/tests/serialize.test.ts +1 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
- package/packages/irc-core/src/commands/account-auth.ts +172 -0
- package/packages/irc-core/src/commands/chanserv.ts +882 -0
- package/packages/irc-core/src/commands/hostserv.ts +487 -0
- package/packages/irc-core/src/commands/index.ts +12 -0
- package/packages/irc-core/src/commands/join.ts +164 -8
- package/packages/irc-core/src/commands/markread.ts +202 -0
- package/packages/irc-core/src/commands/memoserv.ts +319 -0
- package/packages/irc-core/src/commands/mode.ts +96 -4
- package/packages/irc-core/src/commands/nickserv.ts +390 -0
- package/packages/irc-core/src/commands/oper.ts +18 -1
- package/packages/irc-core/src/commands/operserv.ts +346 -0
- package/packages/irc-core/src/commands/pre-away.ts +3 -1
- package/packages/irc-core/src/commands/privmsg.ts +42 -0
- package/packages/irc-core/src/commands/read-marker.ts +8 -8
- package/packages/irc-core/src/commands/registration.ts +61 -6
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +37 -0
- package/packages/irc-core/src/config.ts +36 -5
- package/packages/irc-core/src/effects.ts +56 -1
- package/packages/irc-core/src/ports.ts +1653 -84
- package/packages/irc-core/src/protocol/numerics.ts +8 -0
- package/packages/irc-core/src/state/channel.ts +21 -1
- package/packages/irc-core/src/state/connection.ts +25 -1
- package/packages/irc-core/src/types.ts +48 -12
- package/packages/irc-core/tests/commands/chanserv.test.ts +1668 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
- package/packages/irc-core/tests/commands/join.test.ts +393 -1
- package/packages/irc-core/tests/commands/markread.test.ts +361 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
- package/packages/irc-core/tests/commands/mode.test.ts +381 -2
- package/packages/irc-core/tests/commands/nickserv.test.ts +807 -0
- package/packages/irc-core/tests/commands/oper.test.ts +13 -0
- package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
- package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
- package/packages/irc-core/tests/commands/registration.test.ts +788 -14
- package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
- package/packages/irc-core/tests/commands/topic.test.ts +94 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
- package/packages/irc-core/tests/config.test.ts +49 -5
- package/packages/irc-core/tests/effects.test.ts +19 -0
- package/packages/irc-core/tests/message-store.test.ts +63 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
- package/packages/irc-core/tests/services-store.test.ts +1289 -0
- package/packages/irc-core/tests/state/channel.test.ts +3 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +71 -16
- package/packages/irc-server/src/dispatch.ts +94 -7
- package/packages/irc-server/src/routing.ts +19 -0
- package/packages/irc-server/tests/actor.test.ts +623 -12
- package/packages/irc-server/tests/dispatch.test.ts +270 -2
- package/packages/irc-server/tests/routing.test.ts +6 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +29 -3
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
- package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
|
@@ -0,0 +1,1668 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
CHANSERV_NICK,
|
|
4
|
+
DEFAULT_CHANNEL_LEVELS,
|
|
5
|
+
SHORTHAND_LEVELS,
|
|
6
|
+
chanservReducer,
|
|
7
|
+
positiveMlockLetters,
|
|
8
|
+
} from '../../src/commands/chanserv';
|
|
9
|
+
import type { Effect as EffectType } from '../../src/effects';
|
|
10
|
+
import type { ServicesStore } from '../../src/ports';
|
|
11
|
+
import {
|
|
12
|
+
EmptyMotdProvider,
|
|
13
|
+
FakeClock,
|
|
14
|
+
InMemoryServicesStore,
|
|
15
|
+
SequentialIdFactory,
|
|
16
|
+
} from '../../src/ports';
|
|
17
|
+
import type { IrcMessage } from '../../src/protocol/messages';
|
|
18
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
19
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
20
|
+
|
|
21
|
+
const NOW = 1_700_000_000_000;
|
|
22
|
+
|
|
23
|
+
const serverConfig: ServerConfig = {
|
|
24
|
+
serverName: 'irc.example.com',
|
|
25
|
+
networkName: 'ExampleNet',
|
|
26
|
+
maxChannelsPerUser: 30,
|
|
27
|
+
maxTargetsPerCommand: 10,
|
|
28
|
+
maxListEntries: 50,
|
|
29
|
+
nickLen: 30,
|
|
30
|
+
channelLen: 50,
|
|
31
|
+
topicLen: 390,
|
|
32
|
+
quitMessage: 'Client Quit',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function makeConn(id = 'c1', nick = 'alice'): ConnectionState {
|
|
36
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
37
|
+
s.nick = nick;
|
|
38
|
+
s.user = nick;
|
|
39
|
+
s.host = 'example.com';
|
|
40
|
+
s.realname = nick;
|
|
41
|
+
s.registration = 'registered';
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeCtx(
|
|
46
|
+
state: ConnectionState,
|
|
47
|
+
services?: ServicesStore,
|
|
48
|
+
clock = new FakeClock(NOW),
|
|
49
|
+
): Ctx {
|
|
50
|
+
return buildCtx({
|
|
51
|
+
serverConfig,
|
|
52
|
+
clock,
|
|
53
|
+
ids: new SequentialIdFactory(),
|
|
54
|
+
motd: EmptyMotdProvider,
|
|
55
|
+
connection: state,
|
|
56
|
+
...(services !== undefined ? { services } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function privmsg(body: string): IrcMessage {
|
|
61
|
+
return { command: 'PRIVMSG', params: [CHANSERV_NICK, body], tags: {} };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Extracts the text of every Send line emitted by the reducer. */
|
|
65
|
+
function sentTexts(effects: EffectType[]): string[] {
|
|
66
|
+
return effects.flatMap((e) => (e.tag === 'Send' ? e.lines.map((l) => l.text) : []));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('chanservReducer — REGISTER', () => {
|
|
70
|
+
it('registers a fresh channel for an identified founder and sets +r', () => {
|
|
71
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
72
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
73
|
+
const conn = makeConn();
|
|
74
|
+
conn.account = 'alice';
|
|
75
|
+
conn.userModes.registered = true;
|
|
76
|
+
const ctx = makeCtx(conn, services);
|
|
77
|
+
|
|
78
|
+
const out = chanservReducer(conn, privmsg('REGISTER #home'), ctx);
|
|
79
|
+
|
|
80
|
+
expect(services.getChannelFounder('#home')).toBe('alice');
|
|
81
|
+
const applyDeltas = out.effects.filter((e) => e.tag === 'ApplyChannelDelta');
|
|
82
|
+
expect(applyDeltas.length).toBeGreaterThan(0);
|
|
83
|
+
for (const e of applyDeltas) {
|
|
84
|
+
if (e.tag === 'ApplyChannelDelta') {
|
|
85
|
+
const changes = e.delta.modeChanges ?? [];
|
|
86
|
+
expect(changes).toContainEqual({ mode: 'registered', set: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
expect(sentTexts(out.effects)).toContain(
|
|
90
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is now registered.',
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects REGISTER when the connection is not identified', () => {
|
|
95
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
96
|
+
const conn = makeConn();
|
|
97
|
+
const ctx = makeCtx(conn, services);
|
|
98
|
+
|
|
99
|
+
const out = chanservReducer(conn, privmsg('REGISTER #home'), ctx);
|
|
100
|
+
|
|
101
|
+
expect(services.getChannelFounder('#home')).toBeUndefined();
|
|
102
|
+
expect(sentTexts(out.effects)).toContain(
|
|
103
|
+
':ChanServ!ChanServ@services NOTICE alice :You must identify before registering a channel.',
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects REGISTER on an already-registered channel', () => {
|
|
108
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
109
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
110
|
+
services.registerChannel('#home', 'alice');
|
|
111
|
+
const conn = makeConn();
|
|
112
|
+
conn.account = 'alice';
|
|
113
|
+
conn.userModes.registered = true;
|
|
114
|
+
const ctx = makeCtx(conn, services);
|
|
115
|
+
|
|
116
|
+
const out = chanservReducer(conn, privmsg('REGISTER #home'), ctx);
|
|
117
|
+
|
|
118
|
+
expect(sentTexts(out.effects)).toContain(
|
|
119
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is already registered.',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects REGISTER with missing channel parameter', () => {
|
|
124
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
125
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
126
|
+
const conn = makeConn();
|
|
127
|
+
conn.account = 'alice';
|
|
128
|
+
conn.userModes.registered = true;
|
|
129
|
+
const ctx = makeCtx(conn, services);
|
|
130
|
+
|
|
131
|
+
const out = chanservReducer(conn, privmsg('REGISTER'), ctx);
|
|
132
|
+
|
|
133
|
+
expect(sentTexts(out.effects)).toContain(
|
|
134
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: REGISTER <#channel>',
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('rejects REGISTER with an invalid channel name', () => {
|
|
139
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
140
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
141
|
+
const conn = makeConn();
|
|
142
|
+
conn.account = 'alice';
|
|
143
|
+
conn.userModes.registered = true;
|
|
144
|
+
const ctx = makeCtx(conn, services);
|
|
145
|
+
|
|
146
|
+
const out = chanservReducer(conn, privmsg('REGISTER bogus'), ctx);
|
|
147
|
+
|
|
148
|
+
expect(services.getChannelFounder('bogus')).toBeUndefined();
|
|
149
|
+
expect(sentTexts(out.effects)).toContain(
|
|
150
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: REGISTER <#channel>',
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('rejects REGISTER with a channel name longer than channelLen', () => {
|
|
155
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
156
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
157
|
+
const conn = makeConn();
|
|
158
|
+
conn.account = 'alice';
|
|
159
|
+
conn.userModes.registered = true;
|
|
160
|
+
const ctx = makeCtx(conn, services);
|
|
161
|
+
|
|
162
|
+
const longName = `#${'a'.repeat(60)}`;
|
|
163
|
+
const out = chanservReducer(conn, privmsg(`REGISTER ${longName}`), ctx);
|
|
164
|
+
|
|
165
|
+
expect(services.getChannelFounder(longName)).toBeUndefined();
|
|
166
|
+
expect(sentTexts(out.effects)).toContain(
|
|
167
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: REGISTER <#channel>',
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('returns a help NOTICE when no body is supplied', () => {
|
|
172
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
173
|
+
const conn = makeConn();
|
|
174
|
+
const ctx = makeCtx(conn, services);
|
|
175
|
+
|
|
176
|
+
const out = chanservReducer(
|
|
177
|
+
conn,
|
|
178
|
+
{ command: 'PRIVMSG', params: [CHANSERV_NICK], tags: {} },
|
|
179
|
+
ctx,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(sentTexts(out.effects).some((t) => t.includes('Available commands'))).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('returns a NOTICE when no services store is bound', () => {
|
|
186
|
+
const conn = makeConn();
|
|
187
|
+
const ctx = makeCtx(conn);
|
|
188
|
+
|
|
189
|
+
const out = chanservReducer(conn, privmsg('REGISTER #home'), ctx);
|
|
190
|
+
|
|
191
|
+
expect(sentTexts(out.effects)).toContain(
|
|
192
|
+
':ChanServ!ChanServ@services NOTICE alice :Services are not available on this server.',
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('updates lastSeen to ctx.clock.now()', () => {
|
|
197
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
198
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
199
|
+
const conn = makeConn();
|
|
200
|
+
conn.account = 'alice';
|
|
201
|
+
conn.userModes.registered = true;
|
|
202
|
+
const ctx = makeCtx(conn, services, new FakeClock(NOW + 500));
|
|
203
|
+
|
|
204
|
+
chanservReducer(conn, privmsg('INFO #home'), ctx);
|
|
205
|
+
|
|
206
|
+
expect(conn.lastSeen).toBe(NOW + 500);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// DROP
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
describe('chanservReducer — DROP', () => {
|
|
215
|
+
it('drops a registered channel when invoked by the founder', () => {
|
|
216
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
217
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
218
|
+
services.registerChannel('#home', 'alice');
|
|
219
|
+
const conn = makeConn();
|
|
220
|
+
conn.account = 'alice';
|
|
221
|
+
conn.userModes.registered = true;
|
|
222
|
+
const ctx = makeCtx(conn, services);
|
|
223
|
+
|
|
224
|
+
const out = chanservReducer(conn, privmsg('DROP #home'), ctx);
|
|
225
|
+
|
|
226
|
+
expect(services.getChannelFounder('#home')).toBeUndefined();
|
|
227
|
+
expect(sentTexts(out.effects)).toContain(
|
|
228
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home has been dropped.',
|
|
229
|
+
);
|
|
230
|
+
// DROP clears the services-derived channel modes (r/R/M).
|
|
231
|
+
const applyDeltas = out.effects.filter((e) => e.tag === 'ApplyChannelDelta');
|
|
232
|
+
expect(applyDeltas.length).toBeGreaterThan(0);
|
|
233
|
+
for (const e of applyDeltas) {
|
|
234
|
+
if (e.tag === 'ApplyChannelDelta') {
|
|
235
|
+
const changes = e.delta.modeChanges ?? [];
|
|
236
|
+
expect(changes).toContainEqual({ mode: 'registered', set: false });
|
|
237
|
+
expect(changes).toContainEqual({ mode: 'blockUnidentified', set: false });
|
|
238
|
+
expect(changes).toContainEqual({ mode: 'moderatedIdent', set: false });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('rejects DROP when the connection is not identified', () => {
|
|
244
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
245
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
246
|
+
services.registerChannel('#home', 'alice');
|
|
247
|
+
const conn = makeConn();
|
|
248
|
+
const ctx = makeCtx(conn, services);
|
|
249
|
+
|
|
250
|
+
const out = chanservReducer(conn, privmsg('DROP #home'), ctx);
|
|
251
|
+
|
|
252
|
+
expect(services.getChannelFounder('#home')).toBe('alice');
|
|
253
|
+
expect(sentTexts(out.effects)).toContain(
|
|
254
|
+
':ChanServ!ChanServ@services NOTICE alice :You must identify before dropping a channel.',
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('rejects DROP from a non-founder identified account', () => {
|
|
259
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
260
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
261
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
262
|
+
services.registerChannel('#home', 'alice');
|
|
263
|
+
const conn = makeConn();
|
|
264
|
+
conn.account = 'bob';
|
|
265
|
+
conn.userModes.registered = true;
|
|
266
|
+
const ctx = makeCtx(conn, services);
|
|
267
|
+
|
|
268
|
+
const out = chanservReducer(conn, privmsg('DROP #home'), ctx);
|
|
269
|
+
|
|
270
|
+
expect(services.getChannelFounder('#home')).toBe('alice');
|
|
271
|
+
expect(sentTexts(out.effects)).toContain(
|
|
272
|
+
':ChanServ!ChanServ@services NOTICE alice :Only the founder of #home may drop it.',
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('rejects DROP on an unregistered channel', () => {
|
|
277
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
278
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
279
|
+
const conn = makeConn();
|
|
280
|
+
conn.account = 'alice';
|
|
281
|
+
conn.userModes.registered = true;
|
|
282
|
+
const ctx = makeCtx(conn, services);
|
|
283
|
+
|
|
284
|
+
const out = chanservReducer(conn, privmsg('DROP #home'), ctx);
|
|
285
|
+
|
|
286
|
+
expect(sentTexts(out.effects)).toContain(
|
|
287
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('rejects DROP with missing channel parameter', () => {
|
|
292
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
293
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
294
|
+
services.registerChannel('#home', 'alice');
|
|
295
|
+
const conn = makeConn();
|
|
296
|
+
conn.account = 'alice';
|
|
297
|
+
conn.userModes.registered = true;
|
|
298
|
+
const ctx = makeCtx(conn, services);
|
|
299
|
+
|
|
300
|
+
const out = chanservReducer(conn, privmsg('DROP'), ctx);
|
|
301
|
+
|
|
302
|
+
expect(sentTexts(out.effects)).toContain(
|
|
303
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: DROP <#channel>',
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// INFO
|
|
310
|
+
// ============================================================================
|
|
311
|
+
|
|
312
|
+
describe('chanservReducer — INFO', () => {
|
|
313
|
+
it('reports info for a registered channel', () => {
|
|
314
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
315
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
316
|
+
services.registerChannel('#home', 'alice');
|
|
317
|
+
const conn = makeConn();
|
|
318
|
+
conn.account = 'alice';
|
|
319
|
+
conn.userModes.registered = true;
|
|
320
|
+
const ctx = makeCtx(conn, services);
|
|
321
|
+
|
|
322
|
+
const out = chanservReducer(conn, privmsg('INFO #home'), ctx);
|
|
323
|
+
|
|
324
|
+
const texts = sentTexts(out.effects);
|
|
325
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :Channel: #home');
|
|
326
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :Founder: alice');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('reports not-registered for an unknown channel', () => {
|
|
330
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
331
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
332
|
+
const conn = makeConn();
|
|
333
|
+
conn.account = 'alice';
|
|
334
|
+
conn.userModes.registered = true;
|
|
335
|
+
const ctx = makeCtx(conn, services);
|
|
336
|
+
|
|
337
|
+
const out = chanservReducer(conn, privmsg('INFO #home'), ctx);
|
|
338
|
+
|
|
339
|
+
expect(sentTexts(out.effects)).toContain(
|
|
340
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('rejects INFO with missing channel parameter', () => {
|
|
345
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
346
|
+
const conn = makeConn();
|
|
347
|
+
const ctx = makeCtx(conn, services);
|
|
348
|
+
|
|
349
|
+
const out = chanservReducer(conn, privmsg('INFO'), ctx);
|
|
350
|
+
|
|
351
|
+
expect(sentTexts(out.effects)).toContain(
|
|
352
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: INFO <#channel>',
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// SET FOUNDER
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
361
|
+
describe('chanservReducer — SET FOUNDER', () => {
|
|
362
|
+
it('transfers the founder to a registered nick account', () => {
|
|
363
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
364
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
365
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
366
|
+
services.registerChannel('#home', 'alice');
|
|
367
|
+
const conn = makeConn();
|
|
368
|
+
conn.account = 'alice';
|
|
369
|
+
conn.userModes.registered = true;
|
|
370
|
+
const ctx = makeCtx(conn, services);
|
|
371
|
+
|
|
372
|
+
const out = chanservReducer(conn, privmsg('SET FOUNDER #home bob'), ctx);
|
|
373
|
+
|
|
374
|
+
expect(services.getChannelFounder('#home')).toBe('bob');
|
|
375
|
+
expect(sentTexts(out.effects)).toContain(
|
|
376
|
+
':ChanServ!ChanServ@services NOTICE alice :Founder for #home is now set to bob.',
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('rejects SET FOUNDER when target nick is not registered', () => {
|
|
381
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
382
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
383
|
+
services.registerChannel('#home', 'alice');
|
|
384
|
+
const conn = makeConn();
|
|
385
|
+
conn.account = 'alice';
|
|
386
|
+
conn.userModes.registered = true;
|
|
387
|
+
const ctx = makeCtx(conn, services);
|
|
388
|
+
|
|
389
|
+
const out = chanservReducer(conn, privmsg('SET FOUNDER #home ghost'), ctx);
|
|
390
|
+
|
|
391
|
+
expect(services.getChannelFounder('#home')).toBe('alice');
|
|
392
|
+
expect(sentTexts(out.effects)).toContain(
|
|
393
|
+
':ChanServ!ChanServ@services NOTICE alice :Nick ghost is not registered.',
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('rejects SET FOUNDER with missing target account', () => {
|
|
398
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
399
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
400
|
+
services.registerChannel('#home', 'alice');
|
|
401
|
+
const conn = makeConn();
|
|
402
|
+
conn.account = 'alice';
|
|
403
|
+
conn.userModes.registered = true;
|
|
404
|
+
const ctx = makeCtx(conn, services);
|
|
405
|
+
|
|
406
|
+
const out = chanservReducer(conn, privmsg('SET FOUNDER #home'), ctx);
|
|
407
|
+
|
|
408
|
+
expect(sentTexts(out.effects)).toContain(
|
|
409
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: SET FOUNDER <#channel> <account>',
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('rejects SET FOUNDER from a non-founder', () => {
|
|
414
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
415
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
416
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
417
|
+
services.registerChannel('#home', 'alice');
|
|
418
|
+
const conn = makeConn();
|
|
419
|
+
conn.account = 'bob';
|
|
420
|
+
conn.userModes.registered = true;
|
|
421
|
+
const ctx = makeCtx(conn, services);
|
|
422
|
+
|
|
423
|
+
const out = chanservReducer(conn, privmsg('SET FOUNDER #home bob'), ctx);
|
|
424
|
+
|
|
425
|
+
expect(services.getChannelFounder('#home')).toBe('alice');
|
|
426
|
+
expect(sentTexts(out.effects)).toContain(
|
|
427
|
+
':ChanServ!ChanServ@services NOTICE alice :Only the founder of #home may change its settings.',
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ============================================================================
|
|
433
|
+
// SET MLOCK
|
|
434
|
+
// ============================================================================
|
|
435
|
+
|
|
436
|
+
describe('chanservReducer — SET MLOCK', () => {
|
|
437
|
+
it('stores a valid modestring', () => {
|
|
438
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
439
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
440
|
+
services.registerChannel('#home', 'alice');
|
|
441
|
+
const conn = makeConn();
|
|
442
|
+
conn.account = 'alice';
|
|
443
|
+
conn.userModes.registered = true;
|
|
444
|
+
const ctx = makeCtx(conn, services);
|
|
445
|
+
|
|
446
|
+
const out = chanservReducer(conn, privmsg('SET MLOCK #home +nt'), ctx);
|
|
447
|
+
|
|
448
|
+
expect(services.getChannelMlock('#home')).toBe('+nt');
|
|
449
|
+
expect(sentTexts(out.effects)).toContain(
|
|
450
|
+
':ChanServ!ChanServ@services NOTICE alice :Mode lock for #home is now set to +nt.',
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('stores a modestring with no explicit sign (treated as +)', () => {
|
|
455
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
456
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
457
|
+
services.registerChannel('#home', 'alice');
|
|
458
|
+
const conn = makeConn();
|
|
459
|
+
conn.account = 'alice';
|
|
460
|
+
conn.userModes.registered = true;
|
|
461
|
+
const ctx = makeCtx(conn, services);
|
|
462
|
+
|
|
463
|
+
const out = chanservReducer(conn, privmsg('SET MLOCK #home nt'), ctx);
|
|
464
|
+
|
|
465
|
+
expect(services.getChannelMlock('#home')).toBe('nt');
|
|
466
|
+
expect(sentTexts(out.effects)).toContain(
|
|
467
|
+
':ChanServ!ChanServ@services NOTICE alice :Mode lock for #home is now set to nt.',
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('stores a modestring with a leading - sign (used to lock out a mode)', () => {
|
|
472
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
473
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
474
|
+
services.registerChannel('#home', 'alice');
|
|
475
|
+
const conn = makeConn();
|
|
476
|
+
conn.account = 'alice';
|
|
477
|
+
conn.userModes.registered = true;
|
|
478
|
+
const ctx = makeCtx(conn, services);
|
|
479
|
+
|
|
480
|
+
chanservReducer(conn, privmsg('SET MLOCK #home -n'), ctx);
|
|
481
|
+
|
|
482
|
+
expect(services.getChannelMlock('#home')).toBe('-n');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('rejects an MLOCK that is only a sign (empty body)', () => {
|
|
486
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
487
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
488
|
+
services.registerChannel('#home', 'alice');
|
|
489
|
+
const conn = makeConn();
|
|
490
|
+
conn.account = 'alice';
|
|
491
|
+
conn.userModes.registered = true;
|
|
492
|
+
const ctx = makeCtx(conn, services);
|
|
493
|
+
|
|
494
|
+
const out = chanservReducer(conn, privmsg('SET MLOCK #home +'), ctx);
|
|
495
|
+
|
|
496
|
+
expect(services.getChannelMlock('#home')).toBe('');
|
|
497
|
+
expect(sentTexts(out.effects)).toContain(
|
|
498
|
+
':ChanServ!ChanServ@services NOTICE alice :Invalid mode lock: +',
|
|
499
|
+
);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('clears MLOCK with the * sentinel', () => {
|
|
503
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
504
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
505
|
+
services.registerChannel('#home', 'alice');
|
|
506
|
+
services.setChannelMlock('#home', '+nt');
|
|
507
|
+
const conn = makeConn();
|
|
508
|
+
conn.account = 'alice';
|
|
509
|
+
conn.userModes.registered = true;
|
|
510
|
+
const ctx = makeCtx(conn, services);
|
|
511
|
+
|
|
512
|
+
const out = chanservReducer(conn, privmsg('SET MLOCK #home *'), ctx);
|
|
513
|
+
|
|
514
|
+
expect(services.getChannelMlock('#home')).toBe('');
|
|
515
|
+
expect(sentTexts(out.effects)).toContain(
|
|
516
|
+
':ChanServ!ChanServ@services NOTICE alice :Mode lock for #home has been cleared.',
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('rejects an MLOCK with disallowed letters', () => {
|
|
521
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
522
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
523
|
+
services.registerChannel('#home', 'alice');
|
|
524
|
+
const conn = makeConn();
|
|
525
|
+
conn.account = 'alice';
|
|
526
|
+
conn.userModes.registered = true;
|
|
527
|
+
const ctx = makeCtx(conn, services);
|
|
528
|
+
|
|
529
|
+
const out = chanservReducer(conn, privmsg('SET MLOCK #home +z'), ctx);
|
|
530
|
+
|
|
531
|
+
expect(services.getChannelMlock('#home')).toBe('');
|
|
532
|
+
expect(sentTexts(out.effects)).toContain(
|
|
533
|
+
':ChanServ!ChanServ@services NOTICE alice :Invalid mode lock: +z',
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ============================================================================
|
|
539
|
+
// SET RESTRICTED
|
|
540
|
+
// ============================================================================
|
|
541
|
+
|
|
542
|
+
describe('chanservReducer — SET RESTRICTED', () => {
|
|
543
|
+
it('enables RESTRICTED and sets +R channel mode', () => {
|
|
544
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
545
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
546
|
+
services.registerChannel('#home', 'alice');
|
|
547
|
+
const conn = makeConn();
|
|
548
|
+
conn.account = 'alice';
|
|
549
|
+
conn.userModes.registered = true;
|
|
550
|
+
const ctx = makeCtx(conn, services);
|
|
551
|
+
|
|
552
|
+
const out = chanservReducer(conn, privmsg('SET RESTRICTED #home ON'), ctx);
|
|
553
|
+
|
|
554
|
+
expect(services.getChannel('#home')?.restricted).toBe(true);
|
|
555
|
+
const applyDeltas = out.effects.filter((e) => e.tag === 'ApplyChannelDelta');
|
|
556
|
+
expect(applyDeltas.length).toBeGreaterThan(0);
|
|
557
|
+
for (const e of applyDeltas) {
|
|
558
|
+
if (e.tag === 'ApplyChannelDelta') {
|
|
559
|
+
expect(e.delta.modeChanges).toContainEqual({ mode: 'blockUnidentified', set: true });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('disables RESTRICTED and clears +R channel mode', () => {
|
|
565
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
566
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
567
|
+
services.registerChannel('#home', 'alice');
|
|
568
|
+
services.setChannelRestricted('#home', true);
|
|
569
|
+
const conn = makeConn();
|
|
570
|
+
conn.account = 'alice';
|
|
571
|
+
conn.userModes.registered = true;
|
|
572
|
+
const ctx = makeCtx(conn, services);
|
|
573
|
+
|
|
574
|
+
const out = chanservReducer(conn, privmsg('SET RESTRICTED #home OFF'), ctx);
|
|
575
|
+
|
|
576
|
+
expect(services.getChannel('#home')?.restricted).toBe(false);
|
|
577
|
+
for (const e of out.effects) {
|
|
578
|
+
if (e.tag === 'ApplyChannelDelta') {
|
|
579
|
+
expect(e.delta.modeChanges).toContainEqual({ mode: 'blockUnidentified', set: false });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('rejects SET RESTRICTED with a missing or invalid value', () => {
|
|
585
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
586
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
587
|
+
services.registerChannel('#home', 'alice');
|
|
588
|
+
const conn = makeConn();
|
|
589
|
+
conn.account = 'alice';
|
|
590
|
+
conn.userModes.registered = true;
|
|
591
|
+
const ctx = makeCtx(conn, services);
|
|
592
|
+
|
|
593
|
+
const out = chanservReducer(conn, privmsg('SET RESTRICTED #home MAYBE'), ctx);
|
|
594
|
+
|
|
595
|
+
expect(sentTexts(out.effects)).toContain(
|
|
596
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: SET RESTRICTED <#channel> ON|OFF',
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// ============================================================================
|
|
602
|
+
// SET KEEPTOPIC
|
|
603
|
+
// ============================================================================
|
|
604
|
+
|
|
605
|
+
describe('chanservReducer — SET KEEPTOPIC', () => {
|
|
606
|
+
it('enables KEEPTOPIC', () => {
|
|
607
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
608
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
609
|
+
services.registerChannel('#home', 'alice');
|
|
610
|
+
const conn = makeConn();
|
|
611
|
+
conn.account = 'alice';
|
|
612
|
+
conn.userModes.registered = true;
|
|
613
|
+
const ctx = makeCtx(conn, services);
|
|
614
|
+
|
|
615
|
+
const out = chanservReducer(conn, privmsg('SET KEEPTOPIC #home ON'), ctx);
|
|
616
|
+
|
|
617
|
+
expect(services.getChannel('#home')?.keepTopic).toBe(true);
|
|
618
|
+
expect(sentTexts(out.effects)).toContain(
|
|
619
|
+
':ChanServ!ChanServ@services NOTICE alice :Keep-topic for #home is now ON.',
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('disables KEEPTOPIC and drops the persisted topic snapshot', () => {
|
|
624
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
625
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
626
|
+
services.registerChannel('#home', 'alice');
|
|
627
|
+
services.setChannelKeepTopic('#home', true);
|
|
628
|
+
services.setChannelTopic('#home', { text: 'old', setter: 's', setAt: 0 });
|
|
629
|
+
const conn = makeConn();
|
|
630
|
+
conn.account = 'alice';
|
|
631
|
+
conn.userModes.registered = true;
|
|
632
|
+
const ctx = makeCtx(conn, services);
|
|
633
|
+
|
|
634
|
+
chanservReducer(conn, privmsg('SET KEEPTOPIC #home OFF'), ctx);
|
|
635
|
+
|
|
636
|
+
expect(services.getChannel('#home')?.keepTopic).toBe(false);
|
|
637
|
+
expect(services.getChannelTopic('#home')).toBeUndefined();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('rejects SET KEEPTOPIC with an invalid value', () => {
|
|
641
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
642
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
643
|
+
services.registerChannel('#home', 'alice');
|
|
644
|
+
const conn = makeConn();
|
|
645
|
+
conn.account = 'alice';
|
|
646
|
+
conn.userModes.registered = true;
|
|
647
|
+
const ctx = makeCtx(conn, services);
|
|
648
|
+
|
|
649
|
+
const out = chanservReducer(conn, privmsg('SET KEEPTOPIC #home YEP'), ctx);
|
|
650
|
+
|
|
651
|
+
expect(sentTexts(out.effects)).toContain(
|
|
652
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: SET KEEPTOPIC <#channel> ON|OFF',
|
|
653
|
+
);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// ============================================================================
|
|
658
|
+
// SET misc
|
|
659
|
+
// ============================================================================
|
|
660
|
+
|
|
661
|
+
describe('chanservReducer — SET misc', () => {
|
|
662
|
+
it('rejects SET on an unregistered channel', () => {
|
|
663
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
664
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
665
|
+
const conn = makeConn();
|
|
666
|
+
conn.account = 'alice';
|
|
667
|
+
conn.userModes.registered = true;
|
|
668
|
+
const ctx = makeCtx(conn, services);
|
|
669
|
+
|
|
670
|
+
const out = chanservReducer(conn, privmsg('SET KEEPTOPIC #home ON'), ctx);
|
|
671
|
+
|
|
672
|
+
expect(sentTexts(out.effects)).toContain(
|
|
673
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
674
|
+
);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('rejects SET when the caller is not identified', () => {
|
|
678
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
679
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
680
|
+
services.registerChannel('#home', 'alice');
|
|
681
|
+
const conn = makeConn();
|
|
682
|
+
const ctx = makeCtx(conn, services);
|
|
683
|
+
|
|
684
|
+
const out = chanservReducer(conn, privmsg('SET KEEPTOPIC #home ON'), ctx);
|
|
685
|
+
|
|
686
|
+
expect(services.getChannel('#home')?.keepTopic).toBe(false);
|
|
687
|
+
expect(sentTexts(out.effects)).toContain(
|
|
688
|
+
':ChanServ!ChanServ@services NOTICE alice :You must identify before changing channel settings.',
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('returns the SET help notice for an unknown subkey', () => {
|
|
693
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
694
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
695
|
+
services.registerChannel('#home', 'alice');
|
|
696
|
+
const conn = makeConn();
|
|
697
|
+
conn.account = 'alice';
|
|
698
|
+
conn.userModes.registered = true;
|
|
699
|
+
const ctx = makeCtx(conn, services);
|
|
700
|
+
|
|
701
|
+
const out = chanservReducer(conn, privmsg('SET BOGUS #home ON'), ctx);
|
|
702
|
+
|
|
703
|
+
expect(sentTexts(out.effects)).toContain(
|
|
704
|
+
':ChanServ!ChanServ@services NOTICE alice :Available SET subcommands: FOUNDER, MLOCK, RESTRICTED, KEEPTOPIC',
|
|
705
|
+
);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('returns the FOUNDER syntax notice when channel name is missing', () => {
|
|
709
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
710
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
711
|
+
const conn = makeConn();
|
|
712
|
+
conn.account = 'alice';
|
|
713
|
+
conn.userModes.registered = true;
|
|
714
|
+
const ctx = makeCtx(conn, services);
|
|
715
|
+
|
|
716
|
+
const out = chanservReducer(conn, privmsg('SET FOUNDER'), ctx);
|
|
717
|
+
|
|
718
|
+
expect(sentTexts(out.effects)).toContain(
|
|
719
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: SET FOUNDER <#channel> <account>',
|
|
720
|
+
);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('returns the MLOCK syntax notice when channel name is missing', () => {
|
|
724
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
725
|
+
const conn = makeConn();
|
|
726
|
+
const ctx = makeCtx(conn, services);
|
|
727
|
+
|
|
728
|
+
const out = chanservReducer(conn, privmsg('SET MLOCK'), ctx);
|
|
729
|
+
|
|
730
|
+
expect(sentTexts(out.effects)).toContain(
|
|
731
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: SET MLOCK <#channel> <modestring>',
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('returns the RESTRICTED syntax notice when channel name is missing', () => {
|
|
736
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
737
|
+
const conn = makeConn();
|
|
738
|
+
const ctx = makeCtx(conn, services);
|
|
739
|
+
|
|
740
|
+
const out = chanservReducer(conn, privmsg('SET RESTRICTED'), ctx);
|
|
741
|
+
|
|
742
|
+
expect(sentTexts(out.effects)).toContain(
|
|
743
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: SET RESTRICTED <#channel> ON|OFF',
|
|
744
|
+
);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('returns the KEEPTOPIC syntax notice when channel name is missing', () => {
|
|
748
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
749
|
+
const conn = makeConn();
|
|
750
|
+
const ctx = makeCtx(conn, services);
|
|
751
|
+
|
|
752
|
+
const out = chanservReducer(conn, privmsg('SET KEEPTOPIC'), ctx);
|
|
753
|
+
|
|
754
|
+
expect(sentTexts(out.effects)).toContain(
|
|
755
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: SET KEEPTOPIC <#channel> ON|OFF',
|
|
756
|
+
);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('is case-insensitive on the subcommand', () => {
|
|
760
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
761
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
762
|
+
services.registerChannel('#home', 'alice');
|
|
763
|
+
const conn = makeConn();
|
|
764
|
+
conn.account = 'alice';
|
|
765
|
+
conn.userModes.registered = true;
|
|
766
|
+
const ctx = makeCtx(conn, services);
|
|
767
|
+
|
|
768
|
+
chanservReducer(conn, privmsg('register #home2'), ctx);
|
|
769
|
+
|
|
770
|
+
expect(services.getChannelFounder('#home2')).toBe('alice');
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('returns an unknown-command notice for an unknown subcommand', () => {
|
|
774
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
775
|
+
const conn = makeConn();
|
|
776
|
+
const ctx = makeCtx(conn, services);
|
|
777
|
+
|
|
778
|
+
const out = chanservReducer(conn, privmsg('FROBNICATE'), ctx);
|
|
779
|
+
|
|
780
|
+
expect(sentTexts(out.effects)).toContain(
|
|
781
|
+
':ChanServ!ChanServ@services NOTICE alice :Unknown command. Available: REGISTER, DROP, INFO, SET, ACCESS, LEVELS, SOP, AOP, HOP, VOP',
|
|
782
|
+
);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('returns a help NOTICE when the body has only whitespace', () => {
|
|
786
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
787
|
+
const conn = makeConn();
|
|
788
|
+
const ctx = makeCtx(conn, services);
|
|
789
|
+
|
|
790
|
+
const out = chanservReducer(conn, privmsg(' '), ctx);
|
|
791
|
+
|
|
792
|
+
expect(sentTexts(out.effects).some((t) => t.includes('Available commands'))).toBe(true);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('handles a connection without a nick in the NOTICE addressing', () => {
|
|
796
|
+
const conn = makeConn();
|
|
797
|
+
// biome-ignore lint/performance/noDelete: exactOptionalPropertyTypes forbids `= undefined`.
|
|
798
|
+
delete conn.nick;
|
|
799
|
+
const ctx = makeCtx(conn);
|
|
800
|
+
|
|
801
|
+
const out = chanservReducer(conn, privmsg('REGISTER #home'), ctx);
|
|
802
|
+
|
|
803
|
+
expect(sentTexts(out.effects)).toContain(
|
|
804
|
+
':ChanServ!ChanServ@services NOTICE * :Services are not available on this server.',
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// ============================================================================
|
|
810
|
+
// CHANSERV_NICK constant
|
|
811
|
+
// ============================================================================
|
|
812
|
+
|
|
813
|
+
describe('CHANSERV_NICK constant', () => {
|
|
814
|
+
it('is the canonical ChanServ pseudo-client nick', () => {
|
|
815
|
+
expect(CHANSERV_NICK).toBe('ChanServ');
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// ============================================================================
|
|
820
|
+
// positiveMlockLetters helper
|
|
821
|
+
// ============================================================================
|
|
822
|
+
|
|
823
|
+
describe('positiveMlockLetters', () => {
|
|
824
|
+
it('returns the unsigned letters as positive', () => {
|
|
825
|
+
expect(positiveMlockLetters('nt')).toEqual(new Set(['n', 't']));
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('returns the + letters as positive', () => {
|
|
829
|
+
expect(positiveMlockLetters('+nt')).toEqual(new Set(['n', 't']));
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('excludes letters after a - sign', () => {
|
|
833
|
+
expect(positiveMlockLetters('+nt-l')).toEqual(new Set(['n', 't']));
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it('re-includes letters after a -+ flip-flop', () => {
|
|
837
|
+
expect(positiveMlockLetters('+n-t+m')).toEqual(new Set(['n', 'm']));
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('returns an empty set for an empty modestring', () => {
|
|
841
|
+
expect(positiveMlockLetters('')).toEqual(new Set<string>());
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it('returns an empty set for a modestring with only signs', () => {
|
|
845
|
+
expect(positiveMlockLetters('+-')).toEqual(new Set<string>());
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// ============================================================================
|
|
850
|
+
// ACCESS ADD
|
|
851
|
+
// ============================================================================
|
|
852
|
+
|
|
853
|
+
describe('chanservReducer — ACCESS ADD', () => {
|
|
854
|
+
it('stores an explicit numeric level for an account', () => {
|
|
855
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
856
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
857
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
858
|
+
services.registerChannel('#home', 'alice');
|
|
859
|
+
const conn = makeConn();
|
|
860
|
+
conn.account = 'alice';
|
|
861
|
+
conn.userModes.registered = true;
|
|
862
|
+
const ctx = makeCtx(conn, services);
|
|
863
|
+
|
|
864
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home ADD bob 5'), ctx);
|
|
865
|
+
|
|
866
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(5);
|
|
867
|
+
expect(sentTexts(out.effects)).toContain(
|
|
868
|
+
':ChanServ!ChanServ@services NOTICE alice :Bob added to the access list for #home at level 5.',
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('replaces an existing access level on a second ADD', () => {
|
|
873
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
874
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
875
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
876
|
+
services.registerChannel('#home', 'alice');
|
|
877
|
+
services.setChannelAccess('#home', 'bob', 3);
|
|
878
|
+
const conn = makeConn();
|
|
879
|
+
conn.account = 'alice';
|
|
880
|
+
conn.userModes.registered = true;
|
|
881
|
+
const ctx = makeCtx(conn, services);
|
|
882
|
+
|
|
883
|
+
chanservReducer(conn, privmsg('ACCESS #home ADD bob 10'), ctx);
|
|
884
|
+
|
|
885
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(10);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it('rejects ACCESS ADD with a non-numeric level', () => {
|
|
889
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
890
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
891
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
892
|
+
services.registerChannel('#home', 'alice');
|
|
893
|
+
const conn = makeConn();
|
|
894
|
+
conn.account = 'alice';
|
|
895
|
+
conn.userModes.registered = true;
|
|
896
|
+
const ctx = makeCtx(conn, services);
|
|
897
|
+
|
|
898
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home ADD bob foo'), ctx);
|
|
899
|
+
|
|
900
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(0);
|
|
901
|
+
expect(sentTexts(out.effects)).toContain(
|
|
902
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: ACCESS <#channel> ADD <account> <level>.',
|
|
903
|
+
);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('rejects ACCESS ADD with a missing target account', () => {
|
|
907
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
908
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
909
|
+
services.registerChannel('#home', 'alice');
|
|
910
|
+
const conn = makeConn();
|
|
911
|
+
conn.account = 'alice';
|
|
912
|
+
conn.userModes.registered = true;
|
|
913
|
+
const ctx = makeCtx(conn, services);
|
|
914
|
+
|
|
915
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home ADD'), ctx);
|
|
916
|
+
|
|
917
|
+
expect(sentTexts(out.effects)).toContain(
|
|
918
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: ACCESS <#channel> ADD <account> <level>.',
|
|
919
|
+
);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('rejects ACCESS ADD when the target account is not registered', () => {
|
|
923
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
924
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
925
|
+
services.registerChannel('#home', 'alice');
|
|
926
|
+
const conn = makeConn();
|
|
927
|
+
conn.account = 'alice';
|
|
928
|
+
conn.userModes.registered = true;
|
|
929
|
+
const ctx = makeCtx(conn, services);
|
|
930
|
+
|
|
931
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home ADD ghost 5'), ctx);
|
|
932
|
+
|
|
933
|
+
expect(services.getChannelAccess('#home', 'ghost')).toBe(0);
|
|
934
|
+
expect(sentTexts(out.effects)).toContain(
|
|
935
|
+
':ChanServ!ChanServ@services NOTICE alice :Nick ghost is not registered.',
|
|
936
|
+
);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('rejects ACCESS ADD on an unregistered channel', () => {
|
|
940
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
941
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
942
|
+
const conn = makeConn();
|
|
943
|
+
conn.account = 'alice';
|
|
944
|
+
conn.userModes.registered = true;
|
|
945
|
+
const ctx = makeCtx(conn, services);
|
|
946
|
+
|
|
947
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home ADD bob 5'), ctx);
|
|
948
|
+
|
|
949
|
+
expect(sentTexts(out.effects)).toContain(
|
|
950
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
951
|
+
);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it('rejects ACCESS ADD when the caller is not identified', () => {
|
|
955
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
956
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
957
|
+
services.registerChannel('#home', 'alice');
|
|
958
|
+
const conn = makeConn();
|
|
959
|
+
const ctx = makeCtx(conn, services);
|
|
960
|
+
|
|
961
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home ADD bob 5'), ctx);
|
|
962
|
+
|
|
963
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(0);
|
|
964
|
+
expect(sentTexts(out.effects)).toContain(
|
|
965
|
+
':ChanServ!ChanServ@services NOTICE alice :You must identify before changing channel settings.',
|
|
966
|
+
);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it('rejects ACCESS ADD from a non-founder account', () => {
|
|
970
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
971
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
972
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
973
|
+
services.registerChannel('#home', 'alice');
|
|
974
|
+
const conn = makeConn();
|
|
975
|
+
conn.account = 'bob';
|
|
976
|
+
conn.userModes.registered = true;
|
|
977
|
+
const ctx = makeCtx(conn, services);
|
|
978
|
+
|
|
979
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home ADD bob 5'), ctx);
|
|
980
|
+
|
|
981
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(0);
|
|
982
|
+
expect(sentTexts(out.effects)).toContain(
|
|
983
|
+
':ChanServ!ChanServ@services NOTICE alice :Only the founder of #home may change its settings.',
|
|
984
|
+
);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('rejects ACCESS ADD with a missing channel parameter', () => {
|
|
988
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
989
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
990
|
+
services.registerChannel('#home', 'alice');
|
|
991
|
+
const conn = makeConn();
|
|
992
|
+
conn.account = 'alice';
|
|
993
|
+
conn.userModes.registered = true;
|
|
994
|
+
const ctx = makeCtx(conn, services);
|
|
995
|
+
|
|
996
|
+
const out = chanservReducer(conn, privmsg('ACCESS ADD bob 5'), ctx);
|
|
997
|
+
|
|
998
|
+
expect(sentTexts(out.effects)).toContain(
|
|
999
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: ACCESS <#channel> ADD|DEL|LIST [args].',
|
|
1000
|
+
);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it('rejects ACCESS ADD with an invalid channel name', () => {
|
|
1004
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1005
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1006
|
+
services.registerChannel('#home', 'alice');
|
|
1007
|
+
const conn = makeConn();
|
|
1008
|
+
conn.account = 'alice';
|
|
1009
|
+
conn.userModes.registered = true;
|
|
1010
|
+
const ctx = makeCtx(conn, services);
|
|
1011
|
+
|
|
1012
|
+
const out = chanservReducer(conn, privmsg('ACCESS bogus ADD bob 5'), ctx);
|
|
1013
|
+
|
|
1014
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1015
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: ACCESS <#channel> ADD|DEL|LIST [args].',
|
|
1016
|
+
);
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// ============================================================================
|
|
1021
|
+
// ACCESS DEL
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
|
|
1024
|
+
describe('chanservReducer — ACCESS DEL', () => {
|
|
1025
|
+
it('removes the access entry for an account', () => {
|
|
1026
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1027
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1028
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1029
|
+
services.registerChannel('#home', 'alice');
|
|
1030
|
+
services.setChannelAccess('#home', 'bob', 5);
|
|
1031
|
+
const conn = makeConn();
|
|
1032
|
+
conn.account = 'alice';
|
|
1033
|
+
conn.userModes.registered = true;
|
|
1034
|
+
const ctx = makeCtx(conn, services);
|
|
1035
|
+
|
|
1036
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home DEL bob'), ctx);
|
|
1037
|
+
|
|
1038
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(0);
|
|
1039
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1040
|
+
':ChanServ!ChanServ@services NOTICE alice :Bob removed from the access list for #home.',
|
|
1041
|
+
);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it('succeeds even when the account had no access entry', () => {
|
|
1045
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1046
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1047
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1048
|
+
services.registerChannel('#home', 'alice');
|
|
1049
|
+
const conn = makeConn();
|
|
1050
|
+
conn.account = 'alice';
|
|
1051
|
+
conn.userModes.registered = true;
|
|
1052
|
+
const ctx = makeCtx(conn, services);
|
|
1053
|
+
|
|
1054
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home DEL bob'), ctx);
|
|
1055
|
+
|
|
1056
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(0);
|
|
1057
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1058
|
+
':ChanServ!ChanServ@services NOTICE alice :Bob removed from the access list for #home.',
|
|
1059
|
+
);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
it('rejects ACCESS DEL with a missing target account', () => {
|
|
1063
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1064
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1065
|
+
services.registerChannel('#home', 'alice');
|
|
1066
|
+
const conn = makeConn();
|
|
1067
|
+
conn.account = 'alice';
|
|
1068
|
+
conn.userModes.registered = true;
|
|
1069
|
+
const ctx = makeCtx(conn, services);
|
|
1070
|
+
|
|
1071
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home DEL'), ctx);
|
|
1072
|
+
|
|
1073
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1074
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: ACCESS <#channel> DEL <account>.',
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// ============================================================================
|
|
1080
|
+
// ACCESS LIST
|
|
1081
|
+
// ============================================================================
|
|
1082
|
+
|
|
1083
|
+
describe('chanservReducer — ACCESS LIST', () => {
|
|
1084
|
+
it('lists every access entry with its level', () => {
|
|
1085
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1086
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1087
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1088
|
+
services.registerNick('carol', 'pw', 'c@e');
|
|
1089
|
+
services.registerChannel('#home', 'alice');
|
|
1090
|
+
services.setChannelAccess('#home', 'bob', 5);
|
|
1091
|
+
services.setChannelAccess('#home', 'carol', 3);
|
|
1092
|
+
const conn = makeConn();
|
|
1093
|
+
conn.account = 'alice';
|
|
1094
|
+
conn.userModes.registered = true;
|
|
1095
|
+
const ctx = makeCtx(conn, services);
|
|
1096
|
+
|
|
1097
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home LIST'), ctx);
|
|
1098
|
+
|
|
1099
|
+
const texts = sentTexts(out.effects);
|
|
1100
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :Channel: #home');
|
|
1101
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :Founder: alice');
|
|
1102
|
+
expect(texts.some((t) => t.includes('Nick bob') && t.includes('level 5'))).toBe(true);
|
|
1103
|
+
expect(texts.some((t) => t.includes('Nick carol') && t.includes('level 3'))).toBe(true);
|
|
1104
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :End of access list.');
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it('lists only the founder for an empty access list', () => {
|
|
1108
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1109
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1110
|
+
services.registerChannel('#home', 'alice');
|
|
1111
|
+
const conn = makeConn();
|
|
1112
|
+
conn.account = 'alice';
|
|
1113
|
+
conn.userModes.registered = true;
|
|
1114
|
+
const ctx = makeCtx(conn, services);
|
|
1115
|
+
|
|
1116
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home LIST'), ctx);
|
|
1117
|
+
|
|
1118
|
+
const texts = sentTexts(out.effects);
|
|
1119
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :End of access list.');
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('rejects ACCESS LIST on an unregistered channel', () => {
|
|
1123
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1124
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1125
|
+
const conn = makeConn();
|
|
1126
|
+
conn.account = 'alice';
|
|
1127
|
+
conn.userModes.registered = true;
|
|
1128
|
+
const ctx = makeCtx(conn, services);
|
|
1129
|
+
|
|
1130
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home LIST'), ctx);
|
|
1131
|
+
|
|
1132
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1133
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
1134
|
+
);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it('returns the syntax notice for an unknown ACCESS action', () => {
|
|
1138
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1139
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1140
|
+
services.registerChannel('#home', 'alice');
|
|
1141
|
+
const conn = makeConn();
|
|
1142
|
+
conn.account = 'alice';
|
|
1143
|
+
conn.userModes.registered = true;
|
|
1144
|
+
const ctx = makeCtx(conn, services);
|
|
1145
|
+
|
|
1146
|
+
const out = chanservReducer(conn, privmsg('ACCESS #home FROB bob 5'), ctx);
|
|
1147
|
+
|
|
1148
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1149
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: ACCESS <#channel> ADD|DEL|LIST [args].',
|
|
1150
|
+
);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// ============================================================================
|
|
1155
|
+
// SOP / AOP / HOP / VOP shorthand
|
|
1156
|
+
// ============================================================================
|
|
1157
|
+
|
|
1158
|
+
describe('chanservReducer — SOP/AOP/HOP/VOP shorthand', () => {
|
|
1159
|
+
it('AOP <chan> ADD <nick> stores the AOP default level', () => {
|
|
1160
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1161
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1162
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1163
|
+
services.registerChannel('#home', 'alice');
|
|
1164
|
+
const conn = makeConn();
|
|
1165
|
+
conn.account = 'alice';
|
|
1166
|
+
conn.userModes.registered = true;
|
|
1167
|
+
const ctx = makeCtx(conn, services);
|
|
1168
|
+
|
|
1169
|
+
chanservReducer(conn, privmsg('AOP #home ADD bob'), ctx);
|
|
1170
|
+
|
|
1171
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(SHORTHAND_LEVELS.AOP);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it('VOP <chan> ADD <nick> stores the VOP default level', () => {
|
|
1175
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1176
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1177
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1178
|
+
services.registerChannel('#home', 'alice');
|
|
1179
|
+
const conn = makeConn();
|
|
1180
|
+
conn.account = 'alice';
|
|
1181
|
+
conn.userModes.registered = true;
|
|
1182
|
+
const ctx = makeCtx(conn, services);
|
|
1183
|
+
|
|
1184
|
+
chanservReducer(conn, privmsg('VOP #home ADD bob'), ctx);
|
|
1185
|
+
|
|
1186
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(SHORTHAND_LEVELS.VOP);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it('HOP <chan> ADD <nick> stores the HOP default level', () => {
|
|
1190
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1191
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1192
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1193
|
+
services.registerChannel('#home', 'alice');
|
|
1194
|
+
const conn = makeConn();
|
|
1195
|
+
conn.account = 'alice';
|
|
1196
|
+
conn.userModes.registered = true;
|
|
1197
|
+
const ctx = makeCtx(conn, services);
|
|
1198
|
+
|
|
1199
|
+
chanservReducer(conn, privmsg('HOP #home ADD bob'), ctx);
|
|
1200
|
+
|
|
1201
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(SHORTHAND_LEVELS.HOP);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('SOP <chan> ADD <nick> stores the SOP default level', () => {
|
|
1205
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1206
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1207
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1208
|
+
services.registerChannel('#home', 'alice');
|
|
1209
|
+
const conn = makeConn();
|
|
1210
|
+
conn.account = 'alice';
|
|
1211
|
+
conn.userModes.registered = true;
|
|
1212
|
+
const ctx = makeCtx(conn, services);
|
|
1213
|
+
|
|
1214
|
+
chanservReducer(conn, privmsg('SOP #home ADD bob'), ctx);
|
|
1215
|
+
|
|
1216
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(SHORTHAND_LEVELS.SOP);
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it('AOP <chan> DEL <nick> removes the access entry', () => {
|
|
1220
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1221
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1222
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1223
|
+
services.registerChannel('#home', 'alice');
|
|
1224
|
+
services.setChannelAccess('#home', 'bob', SHORTHAND_LEVELS.AOP);
|
|
1225
|
+
const conn = makeConn();
|
|
1226
|
+
conn.account = 'alice';
|
|
1227
|
+
conn.userModes.registered = true;
|
|
1228
|
+
const ctx = makeCtx(conn, services);
|
|
1229
|
+
|
|
1230
|
+
chanservReducer(conn, privmsg('AOP #home DEL bob'), ctx);
|
|
1231
|
+
|
|
1232
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(0);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
it('AOP <chan> LIST delegates to ACCESS LIST', () => {
|
|
1236
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1237
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1238
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1239
|
+
services.registerChannel('#home', 'alice');
|
|
1240
|
+
services.setChannelAccess('#home', 'bob', SHORTHAND_LEVELS.AOP);
|
|
1241
|
+
const conn = makeConn();
|
|
1242
|
+
conn.account = 'alice';
|
|
1243
|
+
conn.userModes.registered = true;
|
|
1244
|
+
const ctx = makeCtx(conn, services);
|
|
1245
|
+
|
|
1246
|
+
const out = chanservReducer(conn, privmsg('AOP #home LIST'), ctx);
|
|
1247
|
+
|
|
1248
|
+
const texts = sentTexts(out.effects);
|
|
1249
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :End of access list.');
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('rejects AOP shorthand with a missing channel parameter', () => {
|
|
1253
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1254
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1255
|
+
services.registerChannel('#home', 'alice');
|
|
1256
|
+
const conn = makeConn();
|
|
1257
|
+
conn.account = 'alice';
|
|
1258
|
+
conn.userModes.registered = true;
|
|
1259
|
+
const ctx = makeCtx(conn, services);
|
|
1260
|
+
|
|
1261
|
+
const out = chanservReducer(conn, privmsg('AOP ADD bob'), ctx);
|
|
1262
|
+
|
|
1263
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1264
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: AOP <#channel> ADD|DEL|LIST [account].',
|
|
1265
|
+
);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it('rejects AOP shorthand with an unknown action', () => {
|
|
1269
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1270
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1271
|
+
services.registerChannel('#home', 'alice');
|
|
1272
|
+
const conn = makeConn();
|
|
1273
|
+
conn.account = 'alice';
|
|
1274
|
+
conn.userModes.registered = true;
|
|
1275
|
+
const ctx = makeCtx(conn, services);
|
|
1276
|
+
|
|
1277
|
+
const out = chanservReducer(conn, privmsg('AOP #home FROB bob'), ctx);
|
|
1278
|
+
|
|
1279
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1280
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: AOP <#channel> ADD|DEL|LIST [account].',
|
|
1281
|
+
);
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
it('rejects AOP ADD with a missing target account', () => {
|
|
1285
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1286
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1287
|
+
services.registerChannel('#home', 'alice');
|
|
1288
|
+
const conn = makeConn();
|
|
1289
|
+
conn.account = 'alice';
|
|
1290
|
+
conn.userModes.registered = true;
|
|
1291
|
+
const ctx = makeCtx(conn, services);
|
|
1292
|
+
|
|
1293
|
+
const out = chanservReducer(conn, privmsg('AOP #home ADD'), ctx);
|
|
1294
|
+
|
|
1295
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1296
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: AOP <#channel> ADD|DEL|LIST [account].',
|
|
1297
|
+
);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it('rejects AOP DEL with a missing target account', () => {
|
|
1301
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1302
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1303
|
+
services.registerChannel('#home', 'alice');
|
|
1304
|
+
const conn = makeConn();
|
|
1305
|
+
conn.account = 'alice';
|
|
1306
|
+
conn.userModes.registered = true;
|
|
1307
|
+
const ctx = makeCtx(conn, services);
|
|
1308
|
+
|
|
1309
|
+
const out = chanservReducer(conn, privmsg('AOP #home DEL'), ctx);
|
|
1310
|
+
|
|
1311
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1312
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: AOP <#channel> ADD|DEL|LIST [account].',
|
|
1313
|
+
);
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
it('is case-insensitive on the shorthand verb', () => {
|
|
1317
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1318
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1319
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1320
|
+
services.registerChannel('#home', 'alice');
|
|
1321
|
+
const conn = makeConn();
|
|
1322
|
+
conn.account = 'alice';
|
|
1323
|
+
conn.userModes.registered = true;
|
|
1324
|
+
const ctx = makeCtx(conn, services);
|
|
1325
|
+
|
|
1326
|
+
chanservReducer(conn, privmsg('aop #home add bob'), ctx);
|
|
1327
|
+
|
|
1328
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(SHORTHAND_LEVELS.AOP);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
it('rejects AOP ADD when the target nick is not registered', () => {
|
|
1332
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1333
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1334
|
+
services.registerChannel('#home', 'alice');
|
|
1335
|
+
const conn = makeConn();
|
|
1336
|
+
conn.account = 'alice';
|
|
1337
|
+
conn.userModes.registered = true;
|
|
1338
|
+
const ctx = makeCtx(conn, services);
|
|
1339
|
+
|
|
1340
|
+
const out = chanservReducer(conn, privmsg('AOP #home ADD ghost'), ctx);
|
|
1341
|
+
|
|
1342
|
+
expect(services.getChannelAccess('#home', 'ghost')).toBe(0);
|
|
1343
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1344
|
+
':ChanServ!ChanServ@services NOTICE alice :Nick ghost is not registered.',
|
|
1345
|
+
);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
it('rejects AOP ADD when the caller is not the founder', () => {
|
|
1349
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1350
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1351
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1352
|
+
services.registerChannel('#home', 'alice');
|
|
1353
|
+
const conn = makeConn();
|
|
1354
|
+
conn.account = 'bob';
|
|
1355
|
+
conn.userModes.registered = true;
|
|
1356
|
+
const ctx = makeCtx(conn, services);
|
|
1357
|
+
|
|
1358
|
+
const out = chanservReducer(conn, privmsg('AOP #home ADD bob'), ctx);
|
|
1359
|
+
|
|
1360
|
+
expect(services.getChannelAccess('#home', 'bob')).toBe(0);
|
|
1361
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1362
|
+
':ChanServ!ChanServ@services NOTICE alice :Only the founder of #home may change its settings.',
|
|
1363
|
+
);
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
// ============================================================================
|
|
1368
|
+
// LEVELS
|
|
1369
|
+
// ============================================================================
|
|
1370
|
+
|
|
1371
|
+
describe('chanservReducer — LEVELS SET', () => {
|
|
1372
|
+
it('persists a redefined threshold for AUTOOP', () => {
|
|
1373
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1374
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1375
|
+
services.registerChannel('#home', 'alice');
|
|
1376
|
+
const conn = makeConn();
|
|
1377
|
+
conn.account = 'alice';
|
|
1378
|
+
conn.userModes.registered = true;
|
|
1379
|
+
const ctx = makeCtx(conn, services);
|
|
1380
|
+
|
|
1381
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home SET AUTOOP 7'), ctx);
|
|
1382
|
+
|
|
1383
|
+
expect(services.getChannelLevel('#home', 'AUTOOP')).toBe(7);
|
|
1384
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1385
|
+
':ChanServ!ChanServ@services NOTICE alice :Level AUTOOP for #home is now 7.',
|
|
1386
|
+
);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('persists a redefined threshold for AUTOVOICE', () => {
|
|
1390
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1391
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1392
|
+
services.registerChannel('#home', 'alice');
|
|
1393
|
+
const conn = makeConn();
|
|
1394
|
+
conn.account = 'alice';
|
|
1395
|
+
conn.userModes.registered = true;
|
|
1396
|
+
const ctx = makeCtx(conn, services);
|
|
1397
|
+
|
|
1398
|
+
chanservReducer(conn, privmsg('LEVELS #home SET AUTOVOICE 2'), ctx);
|
|
1399
|
+
|
|
1400
|
+
expect(services.getChannelLevel('#home', 'AUTOVOICE')).toBe(2);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it('rejects LEVELS SET on an unknown op name', () => {
|
|
1404
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1405
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1406
|
+
services.registerChannel('#home', 'alice');
|
|
1407
|
+
const conn = makeConn();
|
|
1408
|
+
conn.account = 'alice';
|
|
1409
|
+
conn.userModes.registered = true;
|
|
1410
|
+
const ctx = makeCtx(conn, services);
|
|
1411
|
+
|
|
1412
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home SET BOGUS 5'), ctx);
|
|
1413
|
+
|
|
1414
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1415
|
+
':ChanServ!ChanServ@services NOTICE alice :Unknown level BOGUS. Available: AUTOOP, AUTOHALFOP, AUTOVOICE.',
|
|
1416
|
+
);
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it('rejects LEVELS SET with a non-numeric level', () => {
|
|
1420
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1421
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1422
|
+
services.registerChannel('#home', 'alice');
|
|
1423
|
+
const conn = makeConn();
|
|
1424
|
+
conn.account = 'alice';
|
|
1425
|
+
conn.userModes.registered = true;
|
|
1426
|
+
const ctx = makeCtx(conn, services);
|
|
1427
|
+
|
|
1428
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home SET AUTOOP high'), ctx);
|
|
1429
|
+
|
|
1430
|
+
expect(services.getChannelLevel('#home', 'AUTOOP')).toBeUndefined();
|
|
1431
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1432
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: LEVELS <#channel> SET <op> <level>.',
|
|
1433
|
+
);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it('rejects LEVELS SET with a missing op or level', () => {
|
|
1437
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1438
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1439
|
+
services.registerChannel('#home', 'alice');
|
|
1440
|
+
const conn = makeConn();
|
|
1441
|
+
conn.account = 'alice';
|
|
1442
|
+
conn.userModes.registered = true;
|
|
1443
|
+
const ctx = makeCtx(conn, services);
|
|
1444
|
+
|
|
1445
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home SET AUTOOP'), ctx);
|
|
1446
|
+
|
|
1447
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1448
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: LEVELS <#channel> SET <op> <level>.',
|
|
1449
|
+
);
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it('rejects LEVELS SET on an unregistered channel', () => {
|
|
1453
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1454
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1455
|
+
const conn = makeConn();
|
|
1456
|
+
conn.account = 'alice';
|
|
1457
|
+
conn.userModes.registered = true;
|
|
1458
|
+
const ctx = makeCtx(conn, services);
|
|
1459
|
+
|
|
1460
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home SET AUTOOP 7'), ctx);
|
|
1461
|
+
|
|
1462
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1463
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
1464
|
+
);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
it('rejects LEVELS SET with a missing channel parameter', () => {
|
|
1468
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1469
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1470
|
+
services.registerChannel('#home', 'alice');
|
|
1471
|
+
const conn = makeConn();
|
|
1472
|
+
conn.account = 'alice';
|
|
1473
|
+
conn.userModes.registered = true;
|
|
1474
|
+
const ctx = makeCtx(conn, services);
|
|
1475
|
+
|
|
1476
|
+
const out = chanservReducer(conn, privmsg('LEVELS SET AUTOOP 7'), ctx);
|
|
1477
|
+
|
|
1478
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1479
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: LEVELS <#channel> SET|LIST|RESET [args].',
|
|
1480
|
+
);
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
describe('chanservReducer — LEVELS LIST', () => {
|
|
1485
|
+
it('lists every threshold, falling back to defaults for unset ops', () => {
|
|
1486
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1487
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1488
|
+
services.registerChannel('#home', 'alice');
|
|
1489
|
+
services.setChannelLevel('#home', 'AUTOOP', 7);
|
|
1490
|
+
const conn = makeConn();
|
|
1491
|
+
conn.account = 'alice';
|
|
1492
|
+
conn.userModes.registered = true;
|
|
1493
|
+
const ctx = makeCtx(conn, services);
|
|
1494
|
+
|
|
1495
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home LIST'), ctx);
|
|
1496
|
+
|
|
1497
|
+
const texts = sentTexts(out.effects);
|
|
1498
|
+
expect(texts.some((t) => t.includes('AUTOOP') && t.includes('7'))).toBe(true);
|
|
1499
|
+
expect(
|
|
1500
|
+
texts.some(
|
|
1501
|
+
(t) => t.includes('AUTOVOICE') && t.includes(`${DEFAULT_CHANNEL_LEVELS.AUTOVOICE}`),
|
|
1502
|
+
),
|
|
1503
|
+
).toBe(true);
|
|
1504
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :End of level list.');
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
it('lists the defaults when no overrides are set', () => {
|
|
1508
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1509
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1510
|
+
services.registerChannel('#home', 'alice');
|
|
1511
|
+
const conn = makeConn();
|
|
1512
|
+
conn.account = 'alice';
|
|
1513
|
+
conn.userModes.registered = true;
|
|
1514
|
+
const ctx = makeCtx(conn, services);
|
|
1515
|
+
|
|
1516
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home LIST'), ctx);
|
|
1517
|
+
|
|
1518
|
+
const texts = sentTexts(out.effects);
|
|
1519
|
+
expect(texts).toContain(':ChanServ!ChanServ@services NOTICE alice :End of level list.');
|
|
1520
|
+
expect(
|
|
1521
|
+
texts.some((t) => t.includes('AUTOOP') && t.includes(`${DEFAULT_CHANNEL_LEVELS.AUTOOP}`)),
|
|
1522
|
+
).toBe(true);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
it('rejects LEVELS LIST on an unregistered channel', () => {
|
|
1526
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1527
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1528
|
+
const conn = makeConn();
|
|
1529
|
+
conn.account = 'alice';
|
|
1530
|
+
conn.userModes.registered = true;
|
|
1531
|
+
const ctx = makeCtx(conn, services);
|
|
1532
|
+
|
|
1533
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home LIST'), ctx);
|
|
1534
|
+
|
|
1535
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1536
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
1537
|
+
);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it('reuses the existing levels map on a second SET for the same channel', () => {
|
|
1541
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1542
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1543
|
+
services.registerChannel('#home', 'alice');
|
|
1544
|
+
const conn = makeConn();
|
|
1545
|
+
conn.account = 'alice';
|
|
1546
|
+
conn.userModes.registered = true;
|
|
1547
|
+
const ctx = makeCtx(conn, services);
|
|
1548
|
+
|
|
1549
|
+
chanservReducer(conn, privmsg('LEVELS #home SET AUTOOP 7'), ctx);
|
|
1550
|
+
chanservReducer(conn, privmsg('LEVELS #home SET AUTOVOICE 2'), ctx);
|
|
1551
|
+
|
|
1552
|
+
expect(services.getChannelLevel('#home', 'AUTOOP')).toBe(7);
|
|
1553
|
+
expect(services.getChannelLevel('#home', 'AUTOVOICE')).toBe(2);
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
describe('chanservReducer — LEVELS RESET', () => {
|
|
1558
|
+
it('clears every override so defaults take effect again', () => {
|
|
1559
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1560
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1561
|
+
services.registerChannel('#home', 'alice');
|
|
1562
|
+
services.setChannelLevel('#home', 'AUTOOP', 7);
|
|
1563
|
+
const conn = makeConn();
|
|
1564
|
+
conn.account = 'alice';
|
|
1565
|
+
conn.userModes.registered = true;
|
|
1566
|
+
const ctx = makeCtx(conn, services);
|
|
1567
|
+
|
|
1568
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home RESET'), ctx);
|
|
1569
|
+
|
|
1570
|
+
expect(services.getChannelLevel('#home', 'AUTOOP')).toBeUndefined();
|
|
1571
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1572
|
+
':ChanServ!ChanServ@services NOTICE alice :Levels for #home reset to defaults.',
|
|
1573
|
+
);
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
describe('chanservReducer — LEVELS misc', () => {
|
|
1578
|
+
it('returns the LEVELS syntax notice for an unknown action', () => {
|
|
1579
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1580
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1581
|
+
services.registerChannel('#home', 'alice');
|
|
1582
|
+
const conn = makeConn();
|
|
1583
|
+
conn.account = 'alice';
|
|
1584
|
+
conn.userModes.registered = true;
|
|
1585
|
+
const ctx = makeCtx(conn, services);
|
|
1586
|
+
|
|
1587
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home FROB'), ctx);
|
|
1588
|
+
|
|
1589
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1590
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: LEVELS <#channel> SET|LIST|RESET [args].',
|
|
1591
|
+
);
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
it('rejects LEVELS when the caller is not the founder', () => {
|
|
1595
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1596
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1597
|
+
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1598
|
+
services.registerChannel('#home', 'alice');
|
|
1599
|
+
const conn = makeConn();
|
|
1600
|
+
conn.account = 'bob';
|
|
1601
|
+
conn.userModes.registered = true;
|
|
1602
|
+
const ctx = makeCtx(conn, services);
|
|
1603
|
+
|
|
1604
|
+
const out = chanservReducer(conn, privmsg('LEVELS #home SET AUTOOP 7'), ctx);
|
|
1605
|
+
|
|
1606
|
+
expect(services.getChannelLevel('#home', 'AUTOOP')).toBeUndefined();
|
|
1607
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1608
|
+
':ChanServ!ChanServ@services NOTICE alice :Only the founder of #home may change its settings.',
|
|
1609
|
+
);
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// ============================================================================
|
|
1614
|
+
// HELP / unknown notices with new commands
|
|
1615
|
+
// ============================================================================
|
|
1616
|
+
|
|
1617
|
+
describe('chanservReducer — help notice includes ACCESS/LEVELS', () => {
|
|
1618
|
+
it('lists every available command on empty body', () => {
|
|
1619
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1620
|
+
const conn = makeConn();
|
|
1621
|
+
const ctx = makeCtx(conn, services);
|
|
1622
|
+
|
|
1623
|
+
const out = chanservReducer(conn, privmsg(''), ctx);
|
|
1624
|
+
|
|
1625
|
+
const texts = sentTexts(out.effects);
|
|
1626
|
+
expect(texts.some((t) => t.includes('REGISTER'))).toBe(true);
|
|
1627
|
+
expect(texts.some((t) => t.includes('ACCESS'))).toBe(true);
|
|
1628
|
+
expect(texts.some((t) => t.includes('LEVELS'))).toBe(true);
|
|
1629
|
+
expect(texts.some((t) => t.includes('SOP'))).toBe(true);
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
it('lists ACCESS/LEVELS in the unknown-command notice', () => {
|
|
1633
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1634
|
+
const conn = makeConn();
|
|
1635
|
+
const ctx = makeCtx(conn, services);
|
|
1636
|
+
|
|
1637
|
+
const out = chanservReducer(conn, privmsg('FROBNICATE'), ctx);
|
|
1638
|
+
|
|
1639
|
+
const texts = sentTexts(out.effects);
|
|
1640
|
+
expect(texts.some((t) => t.includes('ACCESS'))).toBe(true);
|
|
1641
|
+
expect(texts.some((t) => t.includes('LEVELS'))).toBe(true);
|
|
1642
|
+
});
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// ============================================================================
|
|
1646
|
+
// Constants
|
|
1647
|
+
// ============================================================================
|
|
1648
|
+
|
|
1649
|
+
describe('SHORTHAND_LEVELS / DEFAULT_CHANNEL_LEVELS constants', () => {
|
|
1650
|
+
it('SOP sorts above AOP, AOP above HOP, HOP above VOP', () => {
|
|
1651
|
+
expect(SHORTHAND_LEVELS.SOP).toBeGreaterThan(SHORTHAND_LEVELS.AOP);
|
|
1652
|
+
expect(SHORTHAND_LEVELS.AOP).toBeGreaterThan(SHORTHAND_LEVELS.HOP);
|
|
1653
|
+
expect(SHORTHAND_LEVELS.HOP).toBeGreaterThan(SHORTHAND_LEVELS.VOP);
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
it('DEFAULT_CHANNEL_LEVELS exposes the auto-* thresholds', () => {
|
|
1657
|
+
expect(DEFAULT_CHANNEL_LEVELS.AUTOOP).toBe(SHORTHAND_LEVELS.AOP);
|
|
1658
|
+
expect(DEFAULT_CHANNEL_LEVELS.AUTOHALFOP).toBe(SHORTHAND_LEVELS.HOP);
|
|
1659
|
+
expect(DEFAULT_CHANNEL_LEVELS.AUTOVOICE).toBe(SHORTHAND_LEVELS.VOP);
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
it('every value in DEFAULT_CHANNEL_LEVELS is a positive integer', () => {
|
|
1663
|
+
for (const v of Object.values(DEFAULT_CHANNEL_LEVELS)) {
|
|
1664
|
+
expect(Number.isInteger(v)).toBe(true);
|
|
1665
|
+
expect(v).toBeGreaterThan(0);
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
});
|