serverless-ircd 0.3.0 → 0.5.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 +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +349 -18
- package/README.md +132 -30
- package/apps/aws-stack/README.md +6 -5
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +150 -10
- package/apps/aws-stack/tests/stack.test.ts +145 -4
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +149 -24
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +75 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +96 -9
- package/packages/aws-adapter/src/handlers/default.ts +98 -7
- package/packages/aws-adapter/src/handlers/index.ts +109 -4
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +8 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +174 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +302 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +5 -1
- package/packages/cf-adapter/src/cf-runtime.ts +68 -5
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +278 -85
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +54 -11
- package/packages/cf-adapter/src/index.ts +11 -8
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +12 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
- package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +15 -8
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/wrangler.test.toml +15 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +24 -3
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +17 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +6 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +4 -8
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +65 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +33 -30
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +360 -12
- package/packages/irc-core/src/protocol/numerics.ts +48 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +46 -2
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +57 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +102 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +393 -16
- package/packages/irc-server/src/dispatch.ts +1 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +15 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +104 -0
- package/packages/irc-server/tests/actor.test.ts +1489 -4
- package/packages/irc-server/tests/dispatch.test.ts +37 -17
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +5 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +141 -3
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +10 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +9 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +107 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
passReducer,
|
|
10
10
|
userReducer,
|
|
11
11
|
} from '../../src/commands/registration';
|
|
12
|
+
import { DEFAULT_CREATED_TEXT, DEFAULT_SERVER_VERSION } from '../../src/config';
|
|
12
13
|
import { Effect } from '../../src/effects';
|
|
13
14
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
14
15
|
import {
|
|
15
16
|
EmptyMotdProvider,
|
|
16
17
|
FakeClock,
|
|
18
|
+
InMemoryNickHistoryStore,
|
|
17
19
|
SequentialIdFactory,
|
|
18
20
|
StaticMotdProvider,
|
|
19
21
|
} from '../../src/ports';
|
|
@@ -47,6 +49,22 @@ function makeCtx(
|
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/** Like {@link makeCtx} but also threads a bound nick-history store. */
|
|
53
|
+
function makeCtxWithHistory(
|
|
54
|
+
state: ConnectionState,
|
|
55
|
+
history: InMemoryNickHistoryStore,
|
|
56
|
+
clock = new FakeClock(1_000),
|
|
57
|
+
): Ctx {
|
|
58
|
+
return buildCtx({
|
|
59
|
+
serverConfig,
|
|
60
|
+
clock,
|
|
61
|
+
ids: new SequentialIdFactory(),
|
|
62
|
+
motd: EmptyMotdProvider,
|
|
63
|
+
connection: state,
|
|
64
|
+
history,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
function makeState(): ConnectionState {
|
|
51
69
|
return createConnection({ id: 'c1', connectedSince: 0 });
|
|
52
70
|
}
|
|
@@ -86,9 +104,9 @@ function expectedWelcome(nick: string, user: string, host: string | undefined):
|
|
|
86
104
|
);
|
|
87
105
|
return [
|
|
88
106
|
L(`${prefix} 001 ${nick} :Welcome to the ExampleNet IRC Network, ${hostmask}`),
|
|
89
|
-
L(`${prefix} 002 ${nick} :Your host is ${sv}, running version
|
|
90
|
-
L(`${prefix} 003 ${nick} :This server was created in
|
|
91
|
-
L(`${prefix} 004 ${nick} ${sv}
|
|
107
|
+
L(`${prefix} 002 ${nick} :Your host is ${sv}, running version ${DEFAULT_SERVER_VERSION}`),
|
|
108
|
+
L(`${prefix} 003 ${nick} :This server was created in ${DEFAULT_CREATED_TEXT}`),
|
|
109
|
+
L(`${prefix} 004 ${nick} ${sv} ${DEFAULT_SERVER_VERSION} iosw biklmnstp`),
|
|
92
110
|
...isupport,
|
|
93
111
|
L(`${prefix} 422 ${nick} :MOTD File is missing`),
|
|
94
112
|
];
|
|
@@ -327,6 +345,63 @@ describe('nickReducer', () => {
|
|
|
327
345
|
expect(out.effects).toEqual([]);
|
|
328
346
|
expect(out.state.nick).toBeUndefined();
|
|
329
347
|
});
|
|
348
|
+
|
|
349
|
+
it('records the old nick into ctx.history on a post-registration nick change', () => {
|
|
350
|
+
const state = registeredState();
|
|
351
|
+
const clock = new FakeClock(5_000);
|
|
352
|
+
const history = new InMemoryNickHistoryStore(clock);
|
|
353
|
+
const ctx = makeCtxWithHistory(state, history, clock);
|
|
354
|
+
|
|
355
|
+
nickReducer(state, { command: 'NICK', params: ['bob'], tags: {} }, ctx);
|
|
356
|
+
|
|
357
|
+
const entries = history.query('alice', 10);
|
|
358
|
+
expect(entries).toHaveLength(1);
|
|
359
|
+
expect(entries[0]?.nick).toBe('alice');
|
|
360
|
+
expect(entries[0]?.connId).toBe('c1');
|
|
361
|
+
expect(entries[0]?.username).toBe('alice');
|
|
362
|
+
expect(entries[0]?.hostname).toBe('example.com');
|
|
363
|
+
expect(entries[0]?.realname).toBe('Alice');
|
|
364
|
+
expect(entries[0]?.signoffTime).toBe(5_000);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('does not record history on a pre-registration NICK', () => {
|
|
368
|
+
const state = makeState();
|
|
369
|
+
const clock = new FakeClock(5_000);
|
|
370
|
+
const history = new InMemoryNickHistoryStore(clock);
|
|
371
|
+
const ctx = makeCtxWithHistory(state, history, clock);
|
|
372
|
+
|
|
373
|
+
nickReducer(state, { command: 'NICK', params: ['alice'], tags: {} }, ctx);
|
|
374
|
+
|
|
375
|
+
expect(history.query('alice', 10)).toEqual([]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('does not record history on a nick change when no store is bound', () => {
|
|
379
|
+
const state = registeredState();
|
|
380
|
+
const ctx = makeCtx(state);
|
|
381
|
+
|
|
382
|
+
const out = nickReducer(state, { command: 'NICK', params: ['bob'], tags: {} }, ctx);
|
|
383
|
+
|
|
384
|
+
// No crash; the no-store path is a graceful no-op.
|
|
385
|
+
expect(out.state.nick).toBe('bob');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('records a history entry with only nick/connId when user/host/realname are absent', () => {
|
|
389
|
+
const state = makeState();
|
|
390
|
+
state.nick = 'alice';
|
|
391
|
+
state.registration = 'registered';
|
|
392
|
+
const clock = new FakeClock(5_000);
|
|
393
|
+
const history = new InMemoryNickHistoryStore(clock);
|
|
394
|
+
const ctx = makeCtxWithHistory(state, history, clock);
|
|
395
|
+
|
|
396
|
+
nickReducer(state, { command: 'NICK', params: ['bob'], tags: {} }, ctx);
|
|
397
|
+
|
|
398
|
+
const entries = history.query('alice', 10);
|
|
399
|
+
expect(entries).toHaveLength(1);
|
|
400
|
+
expect(entries[0]?.nick).toBe('alice');
|
|
401
|
+
expect(entries[0]?.username).toBeUndefined();
|
|
402
|
+
expect(entries[0]?.hostname).toBeUndefined();
|
|
403
|
+
expect(entries[0]?.realname).toBeUndefined();
|
|
404
|
+
});
|
|
330
405
|
});
|
|
331
406
|
|
|
332
407
|
// ============================================================================
|
|
@@ -568,6 +643,76 @@ describe('buildWelcomeLines', () => {
|
|
|
568
643
|
});
|
|
569
644
|
});
|
|
570
645
|
|
|
646
|
+
// ============================================================================
|
|
647
|
+
// buildWelcomeLines — server version / created text driven from ServerConfig
|
|
648
|
+
// ============================================================================
|
|
649
|
+
|
|
650
|
+
describe('buildWelcomeLines — config-driven version & created text', () => {
|
|
651
|
+
/** Builds a ctx whose serverConfig carries the supplied version/created. */
|
|
652
|
+
function makeCtxWithMeta(
|
|
653
|
+
state: ConnectionState,
|
|
654
|
+
serverVersion: string | undefined,
|
|
655
|
+
createdAt: string | number | undefined,
|
|
656
|
+
): Ctx {
|
|
657
|
+
const cfg: ServerConfig = {
|
|
658
|
+
...serverConfig,
|
|
659
|
+
...(serverVersion !== undefined ? { serverVersion } : {}),
|
|
660
|
+
...(createdAt !== undefined ? { createdAt } : {}),
|
|
661
|
+
};
|
|
662
|
+
return buildCtx({
|
|
663
|
+
serverConfig: cfg,
|
|
664
|
+
clock: new FakeClock(1_000),
|
|
665
|
+
ids: new SequentialIdFactory(),
|
|
666
|
+
motd: EmptyMotdProvider,
|
|
667
|
+
connection: state,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function registeredConn(): ConnectionState {
|
|
672
|
+
const s = makeState();
|
|
673
|
+
s.nick = 'alice';
|
|
674
|
+
s.user = 'alice';
|
|
675
|
+
s.host = 'example.com';
|
|
676
|
+
return s;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
it('reflects the configured serverVersion in the 002 and 004 lines', () => {
|
|
680
|
+
const ctx = makeCtxWithMeta(registeredConn(), '9.9.9', undefined);
|
|
681
|
+
const lines = buildWelcomeLines(ctx.connection, ctx);
|
|
682
|
+
const line002 = lines.find((l) => l.text.split(' ')[1] === '002');
|
|
683
|
+
const line004 = lines.find((l) => l.text.split(' ')[1] === '004');
|
|
684
|
+
expect(line002?.text).toContain('9.9.9');
|
|
685
|
+
expect(line004?.text).toContain('9.9.9');
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('reflects the configured createdAt string in the 003 line', () => {
|
|
689
|
+
const ctx = makeCtxWithMeta(registeredConn(), undefined, '2024-06-01');
|
|
690
|
+
const lines = buildWelcomeLines(ctx.connection, ctx);
|
|
691
|
+
const line003 = lines.find((l) => l.text.split(' ')[1] === '003');
|
|
692
|
+
expect(line003?.text).toContain('2024-06-01');
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('formats a numeric createdAt (epoch ms) into the 003 line', () => {
|
|
696
|
+
const ctx = makeCtxWithMeta(registeredConn(), undefined, 1_700_000_000_000);
|
|
697
|
+
const lines = buildWelcomeLines(ctx.connection, ctx);
|
|
698
|
+
const line003 = lines.find((l) => l.text.split(' ')[1] === '003');
|
|
699
|
+
// 1700000000000ms → 2023-11-14T22:13:20.000Z ; the formatted text must
|
|
700
|
+
// contain a human-readable rendering of that instant, not the raw number.
|
|
701
|
+
expect(line003?.text).not.toContain('1700000000000');
|
|
702
|
+
expect(line003?.text).toContain('2023');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('falls back to the default version when serverVersion is omitted', () => {
|
|
706
|
+
const ctx = makeCtxWithMeta(registeredConn(), undefined, undefined);
|
|
707
|
+
const lines = buildWelcomeLines(ctx.connection, ctx);
|
|
708
|
+
const line002 = lines.find((l) => l.text.split(' ')[1] === '002');
|
|
709
|
+
// The hardcoded placeholder '0.2.0' must no longer appear; the default
|
|
710
|
+
// is the irc-core package version (DEFAULT_SERVER_VERSION).
|
|
711
|
+
expect(line002?.text).not.toContain('0.2.0');
|
|
712
|
+
expect(line002?.text).toContain(DEFAULT_SERVER_VERSION);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
571
716
|
// ============================================================================
|
|
572
717
|
// buildWelcomeLines — MOTD driven from MotdProvider
|
|
573
718
|
// ============================================================================
|
|
@@ -950,9 +1095,9 @@ function expectedWelcomeWithHost(nick: string, hostmask: string): RawLine[] {
|
|
|
950
1095
|
);
|
|
951
1096
|
return [
|
|
952
1097
|
L(`${prefix} 001 ${nick} :Welcome to the ExampleNet IRC Network, ${hostmask}`),
|
|
953
|
-
L(`${prefix} 002 ${nick} :Your host is ${sv}, running version
|
|
954
|
-
L(`${prefix} 003 ${nick} :This server was created in
|
|
955
|
-
L(`${prefix} 004 ${nick} ${sv}
|
|
1098
|
+
L(`${prefix} 002 ${nick} :Your host is ${sv}, running version ${DEFAULT_SERVER_VERSION}`),
|
|
1099
|
+
L(`${prefix} 003 ${nick} :This server was created in ${DEFAULT_CREATED_TEXT}`),
|
|
1100
|
+
L(`${prefix} 004 ${nick} ${sv} ${DEFAULT_SERVER_VERSION} iosw biklmnstp`),
|
|
956
1101
|
...isupport,
|
|
957
1102
|
L(`${prefix} 422 ${nick} :MOTD File is missing`),
|
|
958
1103
|
];
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildRehashTrailing, rehashLine, rehashReducer } from '../../src/commands/rehash';
|
|
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 { IrcMessage } from '../../src/protocol/messages';
|
|
7
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
8
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
9
|
+
|
|
10
|
+
const baseServerConfig: 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(state: ConnectionState, config: ServerConfig = baseServerConfig): Ctx {
|
|
23
|
+
return buildCtx({
|
|
24
|
+
serverConfig: config,
|
|
25
|
+
clock: new FakeClock(5_000),
|
|
26
|
+
ids: new SequentialIdFactory(),
|
|
27
|
+
motd: EmptyMotdProvider,
|
|
28
|
+
connection: state,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeState(oper = false): ConnectionState {
|
|
33
|
+
const s = createConnection({ id: 'c1', connectedSince: 0 });
|
|
34
|
+
s.nick = 'alice';
|
|
35
|
+
s.user = 'alice';
|
|
36
|
+
s.host = 'example.com';
|
|
37
|
+
s.realname = 'Alice';
|
|
38
|
+
s.registration = 'registered';
|
|
39
|
+
s.userModes.oper = oper;
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const L = (text: string): RawLine => ({ text });
|
|
44
|
+
|
|
45
|
+
const rehash = (): IrcMessage => ({ command: 'REHASH', params: [], tags: {} });
|
|
46
|
+
|
|
47
|
+
describe('rehashReducer — oper gating', () => {
|
|
48
|
+
it('emits 382 RPL_REHASHING and signals reload when the caller is oper', () => {
|
|
49
|
+
const state = makeState(true);
|
|
50
|
+
const ctx = makeCtx(state);
|
|
51
|
+
|
|
52
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
53
|
+
|
|
54
|
+
expect(out.shouldReload).toBe(true);
|
|
55
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
56
|
+
Effect.send('c1', [L(':irc.example.com 382 alice :Rehashing from KV')]),
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('emits 481 ERR_NOPRIVILEGES and does not signal reload for a non-oper', () => {
|
|
61
|
+
const state = makeState(false);
|
|
62
|
+
const ctx = makeCtx(state);
|
|
63
|
+
|
|
64
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
65
|
+
|
|
66
|
+
expect(out.shouldReload).toBe(false);
|
|
67
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
68
|
+
Effect.send('c1', [
|
|
69
|
+
L(":irc.example.com 481 alice :Permission Denied - You're not an IRC operator"),
|
|
70
|
+
]),
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts a lower-case command token', () => {
|
|
75
|
+
const state = makeState(true);
|
|
76
|
+
const ctx = makeCtx(state);
|
|
77
|
+
|
|
78
|
+
const out = rehashReducer(state, { command: 'rehash', params: [], tags: {} }, ctx, 'KV');
|
|
79
|
+
expect(out.shouldReload).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('rehashReducer — state handling', () => {
|
|
84
|
+
it('returns the same state reference (mutation permitted, no copy)', () => {
|
|
85
|
+
const state = makeState(true);
|
|
86
|
+
const ctx = makeCtx(state);
|
|
87
|
+
|
|
88
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
89
|
+
|
|
90
|
+
expect(out.state).toBe(state);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('updates connection lastSeen to ctx.clock.now()', () => {
|
|
94
|
+
const state = makeState(true);
|
|
95
|
+
expect(state.lastSeen).toBe(0);
|
|
96
|
+
const ctx = makeCtx(state);
|
|
97
|
+
|
|
98
|
+
rehashReducer(state, rehash(), ctx, 'KV');
|
|
99
|
+
|
|
100
|
+
expect(state.lastSeen).toBe(5_000);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('also updates lastSeen for the non-oper rejection path', () => {
|
|
104
|
+
const state = makeState(false);
|
|
105
|
+
const ctx = makeCtx(state);
|
|
106
|
+
|
|
107
|
+
rehashReducer(state, rehash(), ctx, 'KV');
|
|
108
|
+
|
|
109
|
+
expect(state.lastSeen).toBe(5_000);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses "*" as the nick in 382 when the connection has no nick', () => {
|
|
113
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
114
|
+
state.userModes.oper = true;
|
|
115
|
+
const ctx = makeCtx(state);
|
|
116
|
+
|
|
117
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
118
|
+
|
|
119
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
120
|
+
Effect.send('c1', [L(':irc.example.com 382 * :Rehashing from KV')]),
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('uses "*" as the nick in 481 when an unregistered non-oper has no nick', () => {
|
|
125
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
126
|
+
const ctx = makeCtx(state);
|
|
127
|
+
|
|
128
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
129
|
+
|
|
130
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
131
|
+
Effect.send('c1', [
|
|
132
|
+
L(":irc.example.com 481 * :Permission Denied - You're not an IRC operator"),
|
|
133
|
+
]),
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('reflects the configured source label in the 382 trailing text', () => {
|
|
138
|
+
const state = makeState(true);
|
|
139
|
+
const ctx = makeCtx(state);
|
|
140
|
+
|
|
141
|
+
const out = rehashReducer(state, rehash(), ctx, 'Secrets Manager');
|
|
142
|
+
|
|
143
|
+
expect((out.effects[0] as { lines: RawLine[] }).lines[0]?.text).toContain(
|
|
144
|
+
':Rehashing from Secrets Manager',
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('rehash formatting helpers', () => {
|
|
150
|
+
it('buildRehashTrailing composes the success phrase from the source label', () => {
|
|
151
|
+
expect(buildRehashTrailing('config file')).toBe('Rehashing from config file');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('buildRehashTrailing appends the error suffix when supplied', () => {
|
|
155
|
+
expect(buildRehashTrailing('KV', 'store unreachable')).toBe(
|
|
156
|
+
'Rehashing from KV failed: store unreachable',
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('rehashLine renders the full :server 382 nick :trailing wire line', () => {
|
|
161
|
+
expect(rehashLine('irc.example.com', 'alice', 'KV')).toEqual(
|
|
162
|
+
L(':irc.example.com 382 alice :Rehashing from KV'),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rehashLine renders the error-suffixed line for the failure path', () => {
|
|
167
|
+
expect(rehashLine('irc.example.com', 'alice', 'KV', 'boom')).toEqual(
|
|
168
|
+
L(':irc.example.com 382 alice :Rehashing from KV failed: boom'),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -4,7 +4,7 @@ import { capReducer } from '../../src/commands/cap';
|
|
|
4
4
|
import { authenticateReducer } from '../../src/commands/sasl';
|
|
5
5
|
import { Effect } from '../../src/effects';
|
|
6
6
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
7
|
-
import type { AccountStore, SaslPayload, SaslResult } from '../../src/ports';
|
|
7
|
+
import type { AccountStore, MtlsIdentityProvider, SaslPayload, SaslResult } from '../../src/ports';
|
|
8
8
|
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
9
9
|
import { encodeBase64 } from '../../src/protocol/base64';
|
|
10
10
|
import type { IrcMessage } from '../../src/protocol/messages';
|
|
@@ -40,7 +40,11 @@ function saslReadyState(): ConnectionState {
|
|
|
40
40
|
return s;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function makeCtx(
|
|
43
|
+
function makeCtx(
|
|
44
|
+
state: ConnectionState,
|
|
45
|
+
accounts?: AccountStore,
|
|
46
|
+
mtlsIdentity?: MtlsIdentityProvider,
|
|
47
|
+
): Ctx {
|
|
44
48
|
return buildCtx({
|
|
45
49
|
serverConfig,
|
|
46
50
|
clock: new FakeClock(1_000),
|
|
@@ -48,6 +52,7 @@ function makeCtx(state: ConnectionState, accounts?: AccountStore): Ctx {
|
|
|
48
52
|
motd: EmptyMotdProvider,
|
|
49
53
|
connection: state,
|
|
50
54
|
...(accounts !== undefined ? { accounts } : {}),
|
|
55
|
+
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -61,6 +66,19 @@ class FakeAccountStore implements AccountStore {
|
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/** A fake MtlsIdentityProvider that returns a canned identity for one connId. */
|
|
70
|
+
class FakeMtlsIdentityProvider implements MtlsIdentityProvider {
|
|
71
|
+
readonly calls: string[] = [];
|
|
72
|
+
constructor(
|
|
73
|
+
private readonly connId: string,
|
|
74
|
+
private readonly identity: string | undefined,
|
|
75
|
+
) {}
|
|
76
|
+
getIdentity(connId: string): string | undefined {
|
|
77
|
+
this.calls.push(connId);
|
|
78
|
+
return connId === this.connId ? this.identity : undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
/** Builds the PLAIN base64 payload `authzid\0authcid\0password`. */
|
|
65
83
|
function plainPayload(authcid: string, password: string, authzid = ''): string {
|
|
66
84
|
return encodeBase64(`${authzid}\0${authcid}\0${password}`);
|
|
@@ -243,25 +261,103 @@ describe('authenticateReducer — no AccountStore configured', () => {
|
|
|
243
261
|
});
|
|
244
262
|
|
|
245
263
|
// ============================================================================
|
|
246
|
-
// AUTHENTICATE EXTERNAL —
|
|
264
|
+
// AUTHENTICATE EXTERNAL — mTLS-backed SASL EXTERNAL
|
|
247
265
|
// ============================================================================
|
|
248
266
|
|
|
249
|
-
describe('authenticateReducer — EXTERNAL
|
|
250
|
-
it('
|
|
267
|
+
describe('authenticateReducer — EXTERNAL with mTLS', () => {
|
|
268
|
+
it('enters the payload phase when an mTLS provider is configured', () => {
|
|
269
|
+
const state = saslReadyState();
|
|
270
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
271
|
+
const ctx = makeCtx(state, undefined, provider);
|
|
272
|
+
const out = authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
273
|
+
|
|
274
|
+
expect(state.saslMech).toBe('EXTERNAL');
|
|
275
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([L('AUTHENTICATE +')]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns 908 ERR_SASLMECHS when no mTLS provider is configured', () => {
|
|
251
279
|
const state = saslReadyState();
|
|
252
280
|
const ctx = makeCtx(state);
|
|
253
281
|
const out = authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
254
282
|
|
|
255
283
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
256
|
-
L(`:${SV}
|
|
284
|
+
L(`:${SV} 908 alice PLAIN :are the available SASL mechanisms`),
|
|
257
285
|
]);
|
|
286
|
+
expect(state.saslMech).toBeUndefined();
|
|
258
287
|
});
|
|
259
288
|
|
|
260
|
-
it('
|
|
289
|
+
it('succeeds (900/903) when a valid cert identity is presented', () => {
|
|
261
290
|
const state = saslReadyState();
|
|
262
|
-
const
|
|
291
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
292
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
293
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
294
|
+
|
|
263
295
|
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
296
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
297
|
+
|
|
298
|
+
expect(state.account).toBe('alice');
|
|
299
|
+
expect(accounts.calls).toEqual([
|
|
300
|
+
{ mech: 'EXTERNAL', payload: { kind: 'EXTERNAL', identity: 'CN=alice' } },
|
|
301
|
+
]);
|
|
302
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
303
|
+
L(`:${SV} 900 alice alice!alice@example.com alice :You are now logged in as alice`),
|
|
304
|
+
L(`:${SV} 903 alice :SASL authentication successful`),
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('clears saslMech/saslBuffer after a successful EXTERNAL', () => {
|
|
309
|
+
const state = saslReadyState();
|
|
310
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
311
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
312
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
313
|
+
|
|
314
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
315
|
+
authenticateReducer(state, authenticate('+'), ctx);
|
|
316
|
+
|
|
264
317
|
expect(state.saslMech).toBeUndefined();
|
|
318
|
+
expect(state.saslBuffer).toBeUndefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('fails with 904 when the provider has no identity for the connection', () => {
|
|
322
|
+
const state = saslReadyState();
|
|
323
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
324
|
+
const provider = new FakeMtlsIdentityProvider('c1', undefined);
|
|
325
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
326
|
+
|
|
327
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
328
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
329
|
+
|
|
330
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
331
|
+
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
332
|
+
]);
|
|
333
|
+
expect(state.account).toBeUndefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('fails with 904 when the account store rejects the identity', () => {
|
|
337
|
+
const state = saslReadyState();
|
|
338
|
+
const accounts = new FakeAccountStore({ ok: false, reason: 'untrusted certificate' });
|
|
339
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=evil');
|
|
340
|
+
const ctx = makeCtx(state, accounts, provider);
|
|
341
|
+
|
|
342
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
343
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
344
|
+
|
|
345
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
346
|
+
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
347
|
+
]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('fails with 904 when no account store is configured', () => {
|
|
351
|
+
const state = saslReadyState();
|
|
352
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
353
|
+
const ctx = makeCtx(state, undefined, provider);
|
|
354
|
+
|
|
355
|
+
authenticateReducer(state, authenticate('EXTERNAL'), ctx);
|
|
356
|
+
const out = authenticateReducer(state, authenticate('+'), ctx);
|
|
357
|
+
|
|
358
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
359
|
+
L(`:${SV} 904 alice :SASL authentication failed`),
|
|
360
|
+
]);
|
|
265
361
|
});
|
|
266
362
|
});
|
|
267
363
|
|
|
@@ -275,6 +371,17 @@ describe('authenticateReducer — unknown mechanism', () => {
|
|
|
275
371
|
const ctx = makeCtx(state);
|
|
276
372
|
const out = authenticateReducer(state, authenticate('CRAM-MD5'), ctx);
|
|
277
373
|
|
|
374
|
+
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
375
|
+
L(`:${SV} 908 alice PLAIN :are the available SASL mechanisms`),
|
|
376
|
+
]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('lists PLAIN,EXTERNAL in 908 when an mTLS provider is configured', () => {
|
|
380
|
+
const state = saslReadyState();
|
|
381
|
+
const provider = new FakeMtlsIdentityProvider('c1', 'CN=alice');
|
|
382
|
+
const ctx = makeCtx(state, undefined, provider);
|
|
383
|
+
const out = authenticateReducer(state, authenticate('CRAM-MD5'), ctx);
|
|
384
|
+
|
|
278
385
|
expect(sendLinesOf(out.effects[0])).toEqual<RawLine[]>([
|
|
279
386
|
L(`:${SV} 908 alice PLAIN,EXTERNAL :are the available SASL mechanisms`),
|
|
280
387
|
]);
|
|
@@ -529,8 +636,9 @@ describe('authenticateReducer — CAP REQ sasl integration', () => {
|
|
|
529
636
|
expect(state.account).toBe('alice');
|
|
530
637
|
});
|
|
531
638
|
|
|
532
|
-
it('
|
|
639
|
+
it('advertises the base sasl cap value (PLAIN) without mTLS', () => {
|
|
533
640
|
const advertised = getLsString();
|
|
534
|
-
expect(advertised).toContain('sasl=PLAIN
|
|
641
|
+
expect(advertised).toContain('sasl=PLAIN');
|
|
642
|
+
expect(advertised).not.toContain('sasl=PLAIN,EXTERNAL');
|
|
535
643
|
});
|
|
536
644
|
});
|