serverless-ircd 0.4.0 → 0.6.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 +34 -2
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/CHANGELOG.md +435 -0
- package/README.md +206 -27
- package/apps/aws-stack/README.md +37 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +106 -13
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +49 -3
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.toml +5 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +138 -28
- package/apps/local-cli/tests/e2e.test.ts +113 -29
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
- package/biome.json +1 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +15 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +69 -0
- package/packages/aws-adapter/src/handlers/connect.ts +36 -5
- package/packages/aws-adapter/src/handlers/default.ts +66 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +40 -2
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +140 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +100 -4
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +238 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +6 -1
- package/packages/cf-adapter/src/cf-runtime.ts +66 -1
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/connection-do.ts +185 -54
- package/packages/cf-adapter/src/env.ts +25 -6
- package/packages/cf-adapter/src/index.ts +2 -0
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/serialize.ts +25 -4
- 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 +1 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
- package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -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 +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- 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 +7 -7
- 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/vitest.config.ts +1 -1
- package/packages/cf-adapter/wrangler.test.toml +7 -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 +1 -1
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +38 -4
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +42 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/isupport.ts +59 -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 +12 -9
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- 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/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- 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/tagmsg.ts +71 -1
- 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/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +72 -10
- package/packages/irc-core/src/effects.ts +41 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +568 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +57 -11
- package/packages/irc-core/src/protocol/outbound.ts +36 -4
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +120 -1
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -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 +129 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- 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 +357 -0
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +90 -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/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- 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-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +699 -19
- package/packages/irc-server/src/dispatch.ts +109 -16
- package/packages/irc-server/src/routing.ts +3 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +10 -7
- package/packages/irc-server/tests/actor.test.ts +2523 -42
- package/packages/irc-server/tests/dispatch.test.ts +300 -2
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +1 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +9 -1
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/package.json +6 -1
- package/tools/seed-cf-accounts.ts +4 -1
- 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
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
10
10
|
import {
|
|
11
|
-
DEFAULT_SERVER_CONFIG,
|
|
12
11
|
InMemoryAccountStore,
|
|
13
12
|
type ParsedServerConfig,
|
|
14
13
|
StaticMotdProvider,
|
|
15
14
|
} from '@serverless-ircd/irc-core';
|
|
15
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
16
16
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
17
|
import { AwsRuntime } from '../src/aws-runtime.js';
|
|
18
18
|
import type { PostToConnection } from '../src/aws-runtime.js';
|
|
@@ -30,12 +30,11 @@ import type { TablesConfig } from '../src/tables.js';
|
|
|
30
30
|
const available = process.env.DDB_AVAILABLE === '1';
|
|
31
31
|
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
32
32
|
|
|
33
|
-
const SERVER_CONFIG: ParsedServerConfig = {
|
|
34
|
-
...DEFAULT_SERVER_CONFIG,
|
|
33
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
35
34
|
serverName: 'irc.example.com',
|
|
36
35
|
networkName: 'ExampleNet',
|
|
37
36
|
motdLines: ['Welcome to the test IRC server.'],
|
|
38
|
-
};
|
|
37
|
+
});
|
|
39
38
|
|
|
40
39
|
const MOTD = new StaticMotdProvider(SERVER_CONFIG.motdLines);
|
|
41
40
|
|
|
@@ -96,6 +95,9 @@ async function makeFixture(): Promise<Fixture> {
|
|
|
96
95
|
motd: MOTD,
|
|
97
96
|
messages: bindMessageStore(SERVER_CONFIG),
|
|
98
97
|
managementApi: mgmt as never,
|
|
98
|
+
// Bind a REHASH reload source so the $default dispatch path exercises
|
|
99
|
+
// the configLoader wiring (and an oper's REHASH re-fetches config).
|
|
100
|
+
configLoader: async () => SERVER_CONFIG,
|
|
99
101
|
},
|
|
100
102
|
cleanup: async () => {
|
|
101
103
|
for (const logical of Object.keys(TABLE_DEFS)) {
|
|
@@ -142,6 +144,27 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
142
144
|
expect(info?.registration).toBe('pre-registration');
|
|
143
145
|
});
|
|
144
146
|
|
|
147
|
+
it('echoes the agreed Sec-WebSocket-Protocol header back on a spec-mode connect', async () => {
|
|
148
|
+
const res = await dispatch(
|
|
149
|
+
{
|
|
150
|
+
requestContext: { routeKey: '$connect', connectionId: 'c-sub' },
|
|
151
|
+
headers: { 'Sec-WebSocket-Protocol': 'text.ircv3.net' },
|
|
152
|
+
},
|
|
153
|
+
requireFx().deps,
|
|
154
|
+
);
|
|
155
|
+
expect(res.statusCode).toBe(200);
|
|
156
|
+
expect(res.headers?.['Sec-WebSocket-Protocol']).toBe('text.ircv3.net');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('omits the Sec-WebSocket-Protocol header on a legacy connect', async () => {
|
|
160
|
+
const res = await dispatch(
|
|
161
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c-nosub' } },
|
|
162
|
+
requireFx().deps,
|
|
163
|
+
);
|
|
164
|
+
expect(res.statusCode).toBe(200);
|
|
165
|
+
expect(res.headers?.['Sec-WebSocket-Protocol']).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
145
168
|
it('is idempotent on APIGW retry (same connectionId overwrites)', async () => {
|
|
146
169
|
for (let i = 0; i < 2; i++) {
|
|
147
170
|
await dispatch(
|
|
@@ -361,6 +384,95 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
361
384
|
expect(sink).toContain('PONG :tok');
|
|
362
385
|
});
|
|
363
386
|
|
|
387
|
+
it('delivers each outbound line as its own postToConnection for a spec-mode connection', async () => {
|
|
388
|
+
await dispatch(
|
|
389
|
+
{
|
|
390
|
+
requestContext: { routeKey: '$connect', connectionId: 'spec1' },
|
|
391
|
+
headers: { 'Sec-WebSocket-Protocol': 'text.ircv3.net' },
|
|
392
|
+
},
|
|
393
|
+
requireFx().deps,
|
|
394
|
+
);
|
|
395
|
+
const calls: string[] = [];
|
|
396
|
+
requireFx().mgmt.register('spec1', (data) => {
|
|
397
|
+
calls.push(data);
|
|
398
|
+
});
|
|
399
|
+
// Spec clients send exactly one IRC message per WebSocket frame.
|
|
400
|
+
await dispatch(
|
|
401
|
+
{
|
|
402
|
+
requestContext: { routeKey: '$default', connectionId: 'spec1' },
|
|
403
|
+
body: 'NICK bob\r\n',
|
|
404
|
+
},
|
|
405
|
+
requireFx().deps,
|
|
406
|
+
);
|
|
407
|
+
await dispatch(
|
|
408
|
+
{
|
|
409
|
+
requestContext: { routeKey: '$default', connectionId: 'spec1' },
|
|
410
|
+
body: 'USER bob 0 * :Bob\r\n',
|
|
411
|
+
},
|
|
412
|
+
requireFx().deps,
|
|
413
|
+
);
|
|
414
|
+
// The welcome block is multiple IRC lines; the IRCv3 spec contract
|
|
415
|
+
// sends exactly one line per WebSocket message (no trailing CRLF).
|
|
416
|
+
expect(calls.length).toBeGreaterThan(1);
|
|
417
|
+
for (const data of calls) {
|
|
418
|
+
expect(data).not.toContain('\r\n');
|
|
419
|
+
}
|
|
420
|
+
expect(calls.some((d) => d.includes(' 001 '))).toBe(true);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('delivers one postToConnection per line for a binary.ircv3.net connection', async () => {
|
|
424
|
+
await dispatch(
|
|
425
|
+
{
|
|
426
|
+
requestContext: { routeKey: '$connect', connectionId: 'spec2' },
|
|
427
|
+
headers: { 'Sec-WebSocket-Protocol': 'binary.ircv3.net' },
|
|
428
|
+
},
|
|
429
|
+
requireFx().deps,
|
|
430
|
+
);
|
|
431
|
+
const calls: string[] = [];
|
|
432
|
+
requireFx().mgmt.register('spec2', (data) => {
|
|
433
|
+
calls.push(data);
|
|
434
|
+
});
|
|
435
|
+
await dispatch(
|
|
436
|
+
{
|
|
437
|
+
requestContext: { routeKey: '$default', connectionId: 'spec2' },
|
|
438
|
+
body: 'NICK binbob\r\n',
|
|
439
|
+
},
|
|
440
|
+
requireFx().deps,
|
|
441
|
+
);
|
|
442
|
+
await dispatch(
|
|
443
|
+
{
|
|
444
|
+
requestContext: { routeKey: '$default', connectionId: 'spec2' },
|
|
445
|
+
body: 'USER binbob 0 * :BinBob\r\n',
|
|
446
|
+
},
|
|
447
|
+
requireFx().deps,
|
|
448
|
+
);
|
|
449
|
+
expect(calls.length).toBeGreaterThan(1);
|
|
450
|
+
for (const data of calls) {
|
|
451
|
+
expect(data).not.toContain('\r\n');
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('batches outbound lines into a single postToConnection for a legacy connection', async () => {
|
|
456
|
+
await dispatch(
|
|
457
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'leg1' } },
|
|
458
|
+
requireFx().deps,
|
|
459
|
+
);
|
|
460
|
+
const calls: string[] = [];
|
|
461
|
+
requireFx().mgmt.register('leg1', (data) => {
|
|
462
|
+
calls.push(data);
|
|
463
|
+
});
|
|
464
|
+
await dispatch(
|
|
465
|
+
{
|
|
466
|
+
requestContext: { routeKey: '$default', connectionId: 'leg1' },
|
|
467
|
+
body: 'NICK carol\r\nUSER carol 0 * :Carol\r\n',
|
|
468
|
+
},
|
|
469
|
+
requireFx().deps,
|
|
470
|
+
);
|
|
471
|
+
expect(calls.length).toBe(1);
|
|
472
|
+
expect(calls[0]).toContain('\r\n');
|
|
473
|
+
expect(calls[0]).toContain(' 001 ');
|
|
474
|
+
});
|
|
475
|
+
|
|
364
476
|
it('persists joined channels across $default invocations', async () => {
|
|
365
477
|
await dispatch(
|
|
366
478
|
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
@@ -537,6 +649,128 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
537
649
|
// The 500-branch is covered indirectly by GoneException tests.
|
|
538
650
|
expect(true).toBe(true);
|
|
539
651
|
});
|
|
652
|
+
|
|
653
|
+
it('runs an oper REHASH through the bound configLoader and emits 382', async () => {
|
|
654
|
+
await dispatch(
|
|
655
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'rh1' } },
|
|
656
|
+
requireFx().deps,
|
|
657
|
+
);
|
|
658
|
+
const sink: string[] = [];
|
|
659
|
+
requireFx().mgmt.register('rh1', (data) => {
|
|
660
|
+
for (const line of data.split('\r\n')) {
|
|
661
|
+
if (line.length > 0) sink.push(line);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
// Seed oper creds + a reload source so OPER then REHASH both succeed.
|
|
665
|
+
const depsWithOper: HandlerDeps = {
|
|
666
|
+
...requireFx().deps,
|
|
667
|
+
serverConfig: { ...SERVER_CONFIG, operCreds: [{ user: 'alice', password: 's3cret' }] },
|
|
668
|
+
configLoader: async () => ({
|
|
669
|
+
...SERVER_CONFIG,
|
|
670
|
+
operCreds: [{ user: 'alice', password: 'rotated' }],
|
|
671
|
+
}),
|
|
672
|
+
};
|
|
673
|
+
await dispatch(
|
|
674
|
+
{
|
|
675
|
+
requestContext: { routeKey: '$default', connectionId: 'rh1' },
|
|
676
|
+
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\nOPER alice s3cret\r\nREHASH\r\n',
|
|
677
|
+
},
|
|
678
|
+
depsWithOper,
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
// 381 (oper) + 382 (rehashing from the Secrets Manager source) emitted.
|
|
682
|
+
expect(sink.some((l) => l.includes(' 381 '))).toBe(true);
|
|
683
|
+
expect(sink.some((l) => l.includes(' 382 ') && l.includes('Secrets Manager'))).toBe(true);
|
|
684
|
+
// Never a 421 for REHASH.
|
|
685
|
+
expect(sink.some((l) => l.includes(' 421 ') && l.includes('REHASH'))).toBe(false);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('refreshChannel updates cached state when a channel becomes empty (no topic)', async () => {
|
|
689
|
+
// After the last member PARTs, the channel snapshot returns 0
|
|
690
|
+
// members but the meta row persists. The next frame that
|
|
691
|
+
// references the channel triggers refreshChannel, which must
|
|
692
|
+
// take the empty-members + existing-cache path without throwing
|
|
693
|
+
// and leave the cache usable for subsequent joins.
|
|
694
|
+
await dispatch(
|
|
695
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'e1' } },
|
|
696
|
+
requireFx().deps,
|
|
697
|
+
);
|
|
698
|
+
requireFx().mgmt.register('e1', () => {});
|
|
699
|
+
await dispatch(
|
|
700
|
+
{
|
|
701
|
+
requestContext: { routeKey: '$default', connectionId: 'e1' },
|
|
702
|
+
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #empty\r\n',
|
|
703
|
+
},
|
|
704
|
+
requireFx().deps,
|
|
705
|
+
);
|
|
706
|
+
// PART drops the member count to 0 while the meta row survives.
|
|
707
|
+
await dispatch(
|
|
708
|
+
{
|
|
709
|
+
requestContext: { routeKey: '$default', connectionId: 'e1' },
|
|
710
|
+
body: 'PART #empty\r\n',
|
|
711
|
+
},
|
|
712
|
+
requireFx().deps,
|
|
713
|
+
);
|
|
714
|
+
// Re-JOIN forces prefetchChannelState → refreshChannel, hitting
|
|
715
|
+
// the empty-members + existing-cache branch. The JOIN must
|
|
716
|
+
// succeed (no exception), proving the cache is still consistent.
|
|
717
|
+
const res = await dispatch(
|
|
718
|
+
{
|
|
719
|
+
requestContext: { routeKey: '$default', connectionId: 'e1' },
|
|
720
|
+
body: 'JOIN #empty\r\n',
|
|
721
|
+
},
|
|
722
|
+
requireFx().deps,
|
|
723
|
+
);
|
|
724
|
+
expect(res.statusCode).toBe(200);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('refreshChannel preserves an existing topic when the snapshot has one', async () => {
|
|
728
|
+
// Same shape as the previous test, but a TOPIC is set before the
|
|
729
|
+
// PART so the snapshot's `topic` field is defined. This exercises
|
|
730
|
+
// the `if (snap.topic !== undefined) existing.topic = snap.topic`
|
|
731
|
+
// assignment in the empty-members path. The JOIN reducer does not
|
|
732
|
+
// emit 332 itself, so we query the topic explicitly via `TOPIC
|
|
733
|
+
// #chan` (no args) after re-JOIN — a 332 RPL_TOPIC reply proves
|
|
734
|
+
// the cache retained the topic through the empty refresh.
|
|
735
|
+
await dispatch(
|
|
736
|
+
{ requestContext: { routeKey: '$connect', connectionId: 't1' } },
|
|
737
|
+
requireFx().deps,
|
|
738
|
+
);
|
|
739
|
+
const sink: string[] = [];
|
|
740
|
+
requireFx().mgmt.register('t1', (data) => {
|
|
741
|
+
for (const line of data.split('\r\n')) {
|
|
742
|
+
if (line.length > 0) sink.push(line);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
await dispatch(
|
|
746
|
+
{
|
|
747
|
+
requestContext: { routeKey: '$default', connectionId: 't1' },
|
|
748
|
+
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #topicchan\r\nTOPIC #topicchan :hello\r\n',
|
|
749
|
+
},
|
|
750
|
+
requireFx().deps,
|
|
751
|
+
);
|
|
752
|
+
const sinkAfterFirstJoin = sink.length;
|
|
753
|
+
await dispatch(
|
|
754
|
+
{
|
|
755
|
+
requestContext: { routeKey: '$default', connectionId: 't1' },
|
|
756
|
+
body: 'PART #topicchan\r\n',
|
|
757
|
+
},
|
|
758
|
+
requireFx().deps,
|
|
759
|
+
);
|
|
760
|
+
// Re-JOIN triggers refreshChannel with snap.topic === 'hello',
|
|
761
|
+
// assigning existing.topic. The subsequent TOPIC query reads from
|
|
762
|
+
// the cache and emits 332 RPL_TOPIC.
|
|
763
|
+
const res = await dispatch(
|
|
764
|
+
{
|
|
765
|
+
requestContext: { routeKey: '$default', connectionId: 't1' },
|
|
766
|
+
body: 'JOIN #topicchan\r\nTOPIC #topicchan\r\n',
|
|
767
|
+
},
|
|
768
|
+
requireFx().deps,
|
|
769
|
+
);
|
|
770
|
+
expect(res.statusCode).toBe(200);
|
|
771
|
+
const newReplies = sink.slice(sinkAfterFirstJoin);
|
|
772
|
+
expect(newReplies.some((l) => l.includes(' 332 ') && l.includes('hello'))).toBe(true);
|
|
773
|
+
});
|
|
540
774
|
});
|
|
541
775
|
|
|
542
776
|
// -------------------------------------------------------------------------
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* store (record → query round-trip) without needing DynamoDB Local.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import type { StoredMessage } from '@serverless-ircd/irc-core';
|
|
11
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
11
12
|
import { describe, expect, it } from 'vitest';
|
|
12
13
|
import { bindMessageStore } from '../src/message-store.js';
|
|
13
14
|
|
|
@@ -26,7 +27,7 @@ function sampleMessage(chan: string, text: string): StoredMessage {
|
|
|
26
27
|
|
|
27
28
|
describe('bindMessageStore', () => {
|
|
28
29
|
it('returns a store that records and replays a message via LATEST', () => {
|
|
29
|
-
const store = bindMessageStore(
|
|
30
|
+
const store = bindMessageStore(makeTestServerConfig());
|
|
30
31
|
store.record(sampleMessage('#aws', 'hello aws'));
|
|
31
32
|
|
|
32
33
|
const result = store.query({ chan: '#aws', direction: 'latest', limit: 10 });
|
|
@@ -35,7 +36,7 @@ describe('bindMessageStore', () => {
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it('returns a store scoped per channel (no cross-channel leak)', () => {
|
|
38
|
-
const store = bindMessageStore(
|
|
39
|
+
const store = bindMessageStore(makeTestServerConfig());
|
|
39
40
|
store.record(sampleMessage('#a', 'in a'));
|
|
40
41
|
store.record(sampleMessage('#b', 'in b'));
|
|
41
42
|
|
|
@@ -45,8 +46,8 @@ describe('bindMessageStore', () => {
|
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
it('returns an independent store instance per call', () => {
|
|
48
|
-
const a = bindMessageStore(
|
|
49
|
-
const b = bindMessageStore(
|
|
49
|
+
const a = bindMessageStore(makeTestServerConfig());
|
|
50
|
+
const b = bindMessageStore(makeTestServerConfig());
|
|
50
51
|
a.record(sampleMessage('#aws', 'only in a'));
|
|
51
52
|
|
|
52
53
|
expect(a.query({ chan: '#aws', direction: 'latest', limit: 10 })).toHaveLength(1);
|
|
@@ -9,11 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
type ParsedServerConfig,
|
|
15
|
-
StaticMotdProvider,
|
|
16
|
-
} from '@serverless-ircd/irc-core';
|
|
12
|
+
import { type ParsedServerConfig, StaticMotdProvider } from '@serverless-ircd/irc-core';
|
|
13
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
17
14
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
18
15
|
import { AwsRuntime } from '../src/aws-runtime.js';
|
|
19
16
|
import type { PostToConnection } from '../src/aws-runtime.js';
|
|
@@ -30,12 +27,11 @@ import type { TablesConfig } from '../src/tables.js';
|
|
|
30
27
|
const available = process.env.DDB_AVAILABLE === '1';
|
|
31
28
|
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
32
29
|
|
|
33
|
-
const SERVER_CONFIG: ParsedServerConfig = {
|
|
34
|
-
...DEFAULT_SERVER_CONFIG,
|
|
30
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
35
31
|
serverName: 'irc.example.com',
|
|
36
32
|
networkName: 'ExampleNet',
|
|
37
33
|
motdLines: ['Welcome to the test IRC server.'],
|
|
38
|
-
};
|
|
34
|
+
});
|
|
39
35
|
|
|
40
36
|
const MOTD = new StaticMotdProvider(SERVER_CONFIG.motdLines);
|
|
41
37
|
|
|
@@ -151,6 +147,8 @@ describe.skipIf(!available)('NLB stream handler — byte framing', () => {
|
|
|
151
147
|
motd: MOTD,
|
|
152
148
|
messages: bindMessageStore(SERVER_CONFIG),
|
|
153
149
|
managementApi: f.mgmt,
|
|
150
|
+
// Bind a reload source so the configLoader wiring is exercised.
|
|
151
|
+
configLoader: async () => SERVER_CONFIG,
|
|
154
152
|
});
|
|
155
153
|
expect(res.statusCode).toBe(200);
|
|
156
154
|
|
|
@@ -459,6 +457,62 @@ describe.skipIf(!available)('NLB stream handler — streaming back', () => {
|
|
|
459
457
|
});
|
|
460
458
|
expect(bSink.some((l) => l.includes('PRIVMSG #room :hello from alice'))).toBe(true);
|
|
461
459
|
});
|
|
460
|
+
|
|
461
|
+
it('refreshChannel tolerates an empty channel snapshot without throwing', async () => {
|
|
462
|
+
// After the last member PARTs, the channel meta row persists but
|
|
463
|
+
// queryMembers returns no rows → snap.members.size === 0. The next
|
|
464
|
+
// frame referencing the channel triggers refreshChannel on the
|
|
465
|
+
// empty-members + existing-cache path. This test ensures that path
|
|
466
|
+
// does not throw and the channel remains usable for a re-JOIN.
|
|
467
|
+
const f = requireFx();
|
|
468
|
+
const opts = () => ({
|
|
469
|
+
dynamo: f.client,
|
|
470
|
+
tables: f.tables,
|
|
471
|
+
serverConfig: SERVER_CONFIG,
|
|
472
|
+
motd: MOTD,
|
|
473
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
474
|
+
managementApi: f.mgmt,
|
|
475
|
+
});
|
|
476
|
+
const a = deriveNlbConnectionId('10.0.0.40', '50040');
|
|
477
|
+
f.mgmt.register(a, () => {});
|
|
478
|
+
// Register + JOIN + PART → cache populated then snapshot empty.
|
|
479
|
+
await handleNlbStream(nlbEvent('10.0.0.40', '50040', ''), opts());
|
|
480
|
+
await handleNlbStream(
|
|
481
|
+
nlbEvent('10.0.0.40', '50040', 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #nlbempty\r\n'),
|
|
482
|
+
opts(),
|
|
483
|
+
);
|
|
484
|
+
await handleNlbStream(nlbEvent('10.0.0.40', '50040', 'PART #nlbempty\r\n'), opts());
|
|
485
|
+
// Re-JOIN hits refreshChannel on the empty-members path; must not throw.
|
|
486
|
+
const res = await handleNlbStream(nlbEvent('10.0.0.40', '50040', 'JOIN #nlbempty\r\n'), opts());
|
|
487
|
+
expect(res.statusCode).toBe(200);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('falls back to "unknown"/"0" when NLB headers are absent', async () => {
|
|
491
|
+
// `readHeader` returns undefined when no header key matches. The
|
|
492
|
+
// caller's `?? 'unknown'` / `?? '0'` fallbacks must apply so the
|
|
493
|
+
// handler does not crash on a malformed event (defensive).
|
|
494
|
+
const f = requireFx();
|
|
495
|
+
const eventWithoutHeaders: NlbStreamEvent = {
|
|
496
|
+
requestContext: {
|
|
497
|
+
elb: { targetGroupArn: 'arn:aws:elasticloadbalancing:::targetgroup/test' },
|
|
498
|
+
},
|
|
499
|
+
version: '2.0',
|
|
500
|
+
body: Buffer.from('PING :tok\r\n', 'utf8').toString('base64'),
|
|
501
|
+
isBase64Encoded: true,
|
|
502
|
+
headers: { 'some-other-header': 'value' },
|
|
503
|
+
};
|
|
504
|
+
const res = await handleNlbStream(eventWithoutHeaders, {
|
|
505
|
+
dynamo: f.client,
|
|
506
|
+
tables: f.tables,
|
|
507
|
+
serverConfig: SERVER_CONFIG,
|
|
508
|
+
motd: MOTD,
|
|
509
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
510
|
+
managementApi: f.mgmt,
|
|
511
|
+
});
|
|
512
|
+
expect(res.statusCode).toBe(200);
|
|
513
|
+
// The fallback id (nlb-unknown-0) still received a PONG.
|
|
514
|
+
expect(res.body).toBeDefined();
|
|
515
|
+
});
|
|
462
516
|
});
|
|
463
517
|
|
|
464
518
|
/**
|