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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Clock,
|
|
13
13
|
type ConnectionState,
|
|
14
14
|
type RawLine,
|
|
15
|
+
type ServerConfig,
|
|
15
16
|
createConnection,
|
|
16
17
|
} from '@serverless-ircd/irc-core';
|
|
17
18
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
@@ -398,6 +399,85 @@ describe.skipIf(!available)('AwsRuntime', () => {
|
|
|
398
399
|
});
|
|
399
400
|
});
|
|
400
401
|
|
|
402
|
+
// -------------------------------------------------------------------------
|
|
403
|
+
// broadcastWallops — Scan-based fan-out to every +w connection
|
|
404
|
+
// -------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
describe('broadcastWallops', () => {
|
|
407
|
+
it('delivers to every +w connection and skips the except id', async () => {
|
|
408
|
+
const receivedA: string[] = [];
|
|
409
|
+
const receivedB: string[] = [];
|
|
410
|
+
const stateA = createConnection({ id: 'w-a', connectedSince: 0 });
|
|
411
|
+
stateA.userModes.wallops = true;
|
|
412
|
+
const stateB = createConnection({ id: 'w-b', connectedSince: 0 });
|
|
413
|
+
stateB.userModes.wallops = true;
|
|
414
|
+
|
|
415
|
+
const mgmt = new LocalPostToConnection();
|
|
416
|
+
mgmt.register('w-a', (data) => {
|
|
417
|
+
for (const line of data.split('\r\n')) {
|
|
418
|
+
if (line.length > 0) receivedA.push(line);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
mgmt.register('w-b', (data) => {
|
|
422
|
+
for (const line of data.split('\r\n')) {
|
|
423
|
+
if (line.length > 0) receivedB.push(line);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const rtA = makeRuntime('w-a', handlersSink(receivedA, stateA), undefined, mgmt);
|
|
428
|
+
const rtB = makeRuntime('w-b', handlersSink(receivedB, stateB), undefined, mgmt);
|
|
429
|
+
|
|
430
|
+
await rtA.persistConnectionState(stateA);
|
|
431
|
+
await rtB.persistConnectionState(stateB);
|
|
432
|
+
|
|
433
|
+
// Caller 'w-a' is excepted; only 'w-b' should receive the line.
|
|
434
|
+
await rtA.broadcastWallops([{ text: ':oper NOTICE * :hello' }], 'w-a');
|
|
435
|
+
expect(receivedA).toEqual([]);
|
|
436
|
+
expect(receivedB).toEqual([':oper NOTICE * :hello']);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('delivers to the bound connection via in-process handlers when not excepted', async () => {
|
|
440
|
+
// The bound connection's own +w delivery goes through handlers.send,
|
|
441
|
+
// not APIGW. With no `except`, the bound conn is also a recipient.
|
|
442
|
+
const received: string[] = [];
|
|
443
|
+
const state = createConnection({ id: 'self', connectedSince: 0 });
|
|
444
|
+
state.userModes.wallops = true;
|
|
445
|
+
const rt = makeRuntime('self', handlersSink(received, state), undefined, null);
|
|
446
|
+
await rt.persistConnectionState(state);
|
|
447
|
+
await rt.broadcastWallops([{ text: ':oper NOTICE * :hi' }]);
|
|
448
|
+
expect(received).toEqual([':oper NOTICE * :hi']);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// -------------------------------------------------------------------------
|
|
453
|
+
// Nick registry — error propagation
|
|
454
|
+
// -------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
describe('reserveNick error propagation', () => {
|
|
457
|
+
it('rethrows non-conditional errors from the PutCommand', async () => {
|
|
458
|
+
// A non-ConditionalCheckFailedException error must propagate rather
|
|
459
|
+
// than be treated as a nick collision. We force a real DynamoDB
|
|
460
|
+
// error by sending the reserveNick PutCommand against a table that
|
|
461
|
+
// does not exist (deleted mid-flight), which yields a
|
|
462
|
+
// ResourceNotFoundException.
|
|
463
|
+
const rt = makeRuntime('c1', noopHandlers());
|
|
464
|
+
// Drop the Nicks table out from under the runtime.
|
|
465
|
+
await fx.client.send(new DeleteTableCommand({ TableName: fx.tables.Nicks }));
|
|
466
|
+
await expect(rt.reserveNick('Ghost', 'c1')).rejects.toThrow(
|
|
467
|
+
/ResourceNotFoundException|non-existent table/,
|
|
468
|
+
);
|
|
469
|
+
// Recreate the table so afterEach cleanup does not fail on a missing table.
|
|
470
|
+
await fx.client.send(
|
|
471
|
+
new CreateTableCommand({
|
|
472
|
+
TableName: fx.tables.Nicks,
|
|
473
|
+
AttributeDefinitions: [{ AttributeName: 'nickLower', AttributeType: 'S' }],
|
|
474
|
+
KeySchema: [{ AttributeName: 'nickLower', KeyType: 'HASH' }],
|
|
475
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
401
481
|
// -------------------------------------------------------------------------
|
|
402
482
|
// listChannels + getChannelConnections
|
|
403
483
|
// -------------------------------------------------------------------------
|
|
@@ -652,3 +732,63 @@ function handlersSink(sink: string[], snapshot?: ConnectionState): AwsRuntimeHan
|
|
|
652
732
|
snapshot: () => snapshot,
|
|
653
733
|
};
|
|
654
734
|
}
|
|
735
|
+
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
// reloadConfig (REHASH) — no DynamoDB access, so this group runs unconditionally.
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
|
|
740
|
+
describe('AwsRuntime — reloadConfig (REHASH)', () => {
|
|
741
|
+
const baseConfig: ServerConfig = {
|
|
742
|
+
serverName: 'irc.example.com',
|
|
743
|
+
networkName: 'ExampleNet',
|
|
744
|
+
maxChannelsPerUser: 30,
|
|
745
|
+
maxTargetsPerCommand: 10,
|
|
746
|
+
maxListEntries: 50,
|
|
747
|
+
nickLen: 30,
|
|
748
|
+
channelLen: 50,
|
|
749
|
+
topicLen: 390,
|
|
750
|
+
quitMessage: 'Client Quit',
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
function makeReloadRuntime(loader?: () => Promise<ServerConfig>): AwsRuntime {
|
|
754
|
+
return new AwsRuntime({
|
|
755
|
+
// reloadConfig never touches DynamoDB; a default client + empty tables
|
|
756
|
+
// suffice for the no-IO reload path.
|
|
757
|
+
dynamo: createDynamoDocumentClient({}),
|
|
758
|
+
tables: {} as TablesConfig,
|
|
759
|
+
connId: 'c1',
|
|
760
|
+
handlers: noopHandlers(),
|
|
761
|
+
managementApi: null,
|
|
762
|
+
...(loader !== undefined ? { configLoader: loader } : {}),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
it('re-invokes the bound loader and returns the fresh config', async () => {
|
|
767
|
+
let password = 'old';
|
|
768
|
+
const rt = makeReloadRuntime(async () => ({
|
|
769
|
+
...baseConfig,
|
|
770
|
+
operCreds: [{ user: 'admin', password }],
|
|
771
|
+
}));
|
|
772
|
+
|
|
773
|
+
const first = await rt.reloadConfig();
|
|
774
|
+
expect(first.operCreds).toEqual([{ user: 'admin', password: 'old' }]);
|
|
775
|
+
|
|
776
|
+
password = 'rotated';
|
|
777
|
+
const second = await rt.reloadConfig();
|
|
778
|
+
expect(second.operCreds).toEqual([{ user: 'admin', password: 'rotated' }]);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('propagates a loader rejection so the actor applies the graceful 382', async () => {
|
|
782
|
+
const rt = makeReloadRuntime(async () => {
|
|
783
|
+
throw new Error('Secrets Manager unreachable');
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
await expect(rt.reloadConfig()).rejects.toThrow('Secrets Manager unreachable');
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it('rejects when no loader was bound at construction', async () => {
|
|
790
|
+
const rt = makeReloadRuntime();
|
|
791
|
+
|
|
792
|
+
await expect(rt.reloadConfig()).rejects.toThrow('no config loader bound');
|
|
793
|
+
});
|
|
794
|
+
});
|
|
@@ -90,6 +90,15 @@ describe('buildLambdaConfigInput', () => {
|
|
|
90
90
|
);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it('passes an overflowing numeric CREATED_AT through verbatim (not Infinity)', () => {
|
|
94
|
+
// A purely-numeric string that overflows Number.MAX_VALUE renders as
|
|
95
|
+
// `Infinity`, which is not a finite epoch-ms. The parser must fall
|
|
96
|
+
// back to returning the raw string rather than emitting `Infinity`
|
|
97
|
+
// into the config (which would serialize as `null` in JSON).
|
|
98
|
+
const huge = '9'.repeat(400);
|
|
99
|
+
expect(buildLambdaConfigInput({ CREATED_AT: huge }).createdAt).toBe(huge);
|
|
100
|
+
});
|
|
101
|
+
|
|
93
102
|
it('omits serverVersion/createdAt when their env vars are unset', () => {
|
|
94
103
|
const out = buildLambdaConfigInput({});
|
|
95
104
|
expect('serverVersion' in out).toBe(false);
|
|
@@ -154,8 +163,8 @@ describe('loadServerConfigFromLambdaEnv', () => {
|
|
|
154
163
|
expect(typeof cfg.nickLen).toBe('number');
|
|
155
164
|
});
|
|
156
165
|
|
|
157
|
-
it('throws when SERVER_NAME is missing', () => {
|
|
158
|
-
expect(() => loadServerConfigFromLambdaEnv({})).
|
|
166
|
+
it('throws a readable error naming serverName when SERVER_NAME is missing', () => {
|
|
167
|
+
expect(() => loadServerConfigFromLambdaEnv({})).toThrowError(/serverName/u);
|
|
159
168
|
});
|
|
160
169
|
|
|
161
170
|
it('round-trips MOTD lines into ParsedServerConfig', () => {
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { type DynamoDBDocumentClient, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
11
|
-
import {
|
|
11
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
12
12
|
import { describe, expect, it } from 'vitest';
|
|
13
13
|
import { handleConnect } from '../src/handlers/connect.js';
|
|
14
|
+
import type { MarshalledConnection } from '../src/serialize.js';
|
|
14
15
|
import type { TablesConfig } from '../src/tables.js';
|
|
15
16
|
|
|
16
17
|
const TABLES: TablesConfig = {
|
|
@@ -24,12 +25,15 @@ const TABLES: TablesConfig = {
|
|
|
24
25
|
interface Stub {
|
|
25
26
|
dynamo: DynamoDBDocumentClient;
|
|
26
27
|
puts: number;
|
|
28
|
+
/** Most recently PutCommand Item (undefined when no put happened). */
|
|
29
|
+
lastPutItem: Record<string, unknown> | undefined;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/** Builds a stub client that serves the supplied `Scan` pages in order. */
|
|
30
33
|
function makeStub(pages: Array<Record<string, unknown>>): Stub {
|
|
31
34
|
let scanIdx = 0;
|
|
32
35
|
const calls = { puts: 0 };
|
|
36
|
+
let lastPutItem: Record<string, unknown> | undefined;
|
|
33
37
|
const dynamo = {
|
|
34
38
|
send: async (cmd: unknown) => {
|
|
35
39
|
if (cmd instanceof ScanCommand) {
|
|
@@ -37,6 +41,7 @@ function makeStub(pages: Array<Record<string, unknown>>): Stub {
|
|
|
37
41
|
}
|
|
38
42
|
if (cmd instanceof PutCommand) {
|
|
39
43
|
calls.puts++;
|
|
44
|
+
lastPutItem = cmd.input.Item as Record<string, unknown> | undefined;
|
|
40
45
|
return {};
|
|
41
46
|
}
|
|
42
47
|
throw new Error('unexpected command');
|
|
@@ -47,6 +52,9 @@ function makeStub(pages: Array<Record<string, unknown>>): Stub {
|
|
|
47
52
|
get puts() {
|
|
48
53
|
return calls.puts;
|
|
49
54
|
},
|
|
55
|
+
get lastPutItem() {
|
|
56
|
+
return lastPutItem;
|
|
57
|
+
},
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -58,9 +66,9 @@ describe('handleConnect admission plumbing', () => {
|
|
|
58
66
|
tables: TABLES,
|
|
59
67
|
connectionId: 'c-paged',
|
|
60
68
|
now: 42,
|
|
61
|
-
serverConfig: {
|
|
69
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
62
70
|
});
|
|
63
|
-
expect(outcome).toEqual({ admitted: true });
|
|
71
|
+
expect(outcome).toEqual({ admitted: true, subprotocol: null });
|
|
64
72
|
expect(stub.puts).toBe(1);
|
|
65
73
|
});
|
|
66
74
|
|
|
@@ -70,7 +78,95 @@ describe('handleConnect admission plumbing', () => {
|
|
|
70
78
|
dynamo: stub.dynamo,
|
|
71
79
|
tables: TABLES,
|
|
72
80
|
connectionId: 'c-full',
|
|
73
|
-
serverConfig: {
|
|
81
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
82
|
+
});
|
|
83
|
+
expect(outcome).toEqual({ admitted: false, statusCode: 429, reason: 'server full' });
|
|
84
|
+
expect(stub.puts).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('handleConnect — IRCv3 WebSocket subprotocol negotiation', () => {
|
|
89
|
+
it('selects binary.ircv3.net, echoes it, and persists a spec-binary mode row', async () => {
|
|
90
|
+
const stub = makeStub([{ Count: 0 }]);
|
|
91
|
+
const outcome = await handleConnect({
|
|
92
|
+
dynamo: stub.dynamo,
|
|
93
|
+
tables: TABLES,
|
|
94
|
+
connectionId: 'c-binary',
|
|
95
|
+
now: 42,
|
|
96
|
+
secWebSocketProtocol: 'binary.ircv3.net',
|
|
97
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
98
|
+
});
|
|
99
|
+
expect(outcome).toEqual({ admitted: true, subprotocol: 'binary.ircv3.net' });
|
|
100
|
+
expect(stub.puts).toBe(1);
|
|
101
|
+
const item = stub.lastPutItem as MarshalledConnection | undefined;
|
|
102
|
+
expect(item?.wsMode).toBe('spec-binary');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('selects text.ircv3.net and persists a spec-text mode row', async () => {
|
|
106
|
+
const stub = makeStub([{ Count: 0 }]);
|
|
107
|
+
const outcome = await handleConnect({
|
|
108
|
+
dynamo: stub.dynamo,
|
|
109
|
+
tables: TABLES,
|
|
110
|
+
connectionId: 'c-text',
|
|
111
|
+
now: 42,
|
|
112
|
+
secWebSocketProtocol: 'text.ircv3.net',
|
|
113
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
114
|
+
});
|
|
115
|
+
expect(outcome).toEqual({ admitted: true, subprotocol: 'text.ircv3.net' });
|
|
116
|
+
const item = stub.lastPutItem as MarshalledConnection | undefined;
|
|
117
|
+
expect(item?.wsMode).toBe('spec-text');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('honours client-preference order when both subprotocols are offered', async () => {
|
|
121
|
+
const stub = makeStub([{ Count: 0 }]);
|
|
122
|
+
const outcome = await handleConnect({
|
|
123
|
+
dynamo: stub.dynamo,
|
|
124
|
+
tables: TABLES,
|
|
125
|
+
connectionId: 'c-pref',
|
|
126
|
+
now: 42,
|
|
127
|
+
secWebSocketProtocol: 'text.ircv3.net, binary.ircv3.net',
|
|
128
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
129
|
+
});
|
|
130
|
+
expect(outcome).toEqual({ admitted: true, subprotocol: 'text.ircv3.net' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('falls back to legacy (null subprotocol, no mode column) when no offer is present', async () => {
|
|
134
|
+
const stub = makeStub([{ Count: 0 }]);
|
|
135
|
+
const outcome = await handleConnect({
|
|
136
|
+
dynamo: stub.dynamo,
|
|
137
|
+
tables: TABLES,
|
|
138
|
+
connectionId: 'c-legacy',
|
|
139
|
+
now: 42,
|
|
140
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
141
|
+
});
|
|
142
|
+
expect(outcome).toEqual({ admitted: true, subprotocol: null });
|
|
143
|
+
const item = stub.lastPutItem as MarshalledConnection | undefined;
|
|
144
|
+
expect(item?.wsMode).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('falls back to legacy when only unsupported subprotocols are offered', async () => {
|
|
148
|
+
const stub = makeStub([{ Count: 0 }]);
|
|
149
|
+
const outcome = await handleConnect({
|
|
150
|
+
dynamo: stub.dynamo,
|
|
151
|
+
tables: TABLES,
|
|
152
|
+
connectionId: 'c-unsupported',
|
|
153
|
+
now: 42,
|
|
154
|
+
secWebSocketProtocol: 'chat, superchat.v2',
|
|
155
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
156
|
+
});
|
|
157
|
+
expect(outcome).toEqual({ admitted: true, subprotocol: null });
|
|
158
|
+
const item = stub.lastPutItem as MarshalledConnection | undefined;
|
|
159
|
+
expect(item?.wsMode).toBeUndefined();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('does not negotiate a subprotocol when the admission gate rejects', async () => {
|
|
163
|
+
const stub = makeStub([{ Count: 5 }]);
|
|
164
|
+
const outcome = await handleConnect({
|
|
165
|
+
dynamo: stub.dynamo,
|
|
166
|
+
tables: TABLES,
|
|
167
|
+
connectionId: 'c-full',
|
|
168
|
+
secWebSocketProtocol: 'binary.ircv3.net',
|
|
169
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
74
170
|
});
|
|
75
171
|
expect(outcome).toEqual({ admitted: false, statusCode: 429, reason: 'server full' });
|
|
76
172
|
expect(stub.puts).toBe(0);
|
|
@@ -12,12 +12,12 @@ import { GoneException } from '@aws-sdk/client-apigatewaymanagementapi';
|
|
|
12
12
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
13
13
|
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
14
14
|
import {
|
|
15
|
-
DEFAULT_SERVER_CONFIG,
|
|
16
15
|
type Nick,
|
|
17
16
|
type ParsedServerConfig,
|
|
18
17
|
StaticMotdProvider,
|
|
19
18
|
createConnection,
|
|
20
19
|
} from '@serverless-ircd/irc-core';
|
|
20
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
21
21
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
22
22
|
import { AwsRuntime, type PostToConnection, cleanupConnection } from '../src/aws-runtime.js';
|
|
23
23
|
import { TABLE_DEFS } from '../src/cdk-table-defs.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: [],
|
|
38
|
-
};
|
|
37
|
+
});
|
|
39
38
|
|
|
40
39
|
interface Fixture {
|
|
41
40
|
tables: TablesConfig;
|
|
@@ -2,20 +2,32 @@
|
|
|
2
2
|
* Vitest `globalSetup` — boots a DynamoDB Local instance once per test run
|
|
3
3
|
* and exposes its endpoint via `process.env.DYNAMO_ENDPOINT`.
|
|
4
4
|
*
|
|
5
|
-
* Strategy
|
|
5
|
+
* Strategy selection:
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* - `DDB_LOCAL_STRATEGY=zip` → only the Java ZIP fallback (skips the
|
|
8
|
+
* testcontainers probe). Canonical CI path on the self-hosted Gitea
|
|
9
|
+
* runner, which has a JRE installed via `setup-java` but no
|
|
10
|
+
* privileged container / `docker.sock` mount for Ryuk.
|
|
11
|
+
* - `DDB_LOCAL_STRATEGY=docker` → only testcontainers.
|
|
12
|
+
* - unset / `auto` (default) → try testcontainers first, then ZIP.
|
|
13
|
+
* Matches developer laptops where either backend may be available.
|
|
14
|
+
*
|
|
15
|
+
* Strategies (in priority order under `auto`):
|
|
16
|
+
*
|
|
17
|
+
* 1. **testcontainers** — preferred. Spins up the canonical
|
|
18
|
+
* `amazon/dynamodb-local` Docker image. Requires Docker with a
|
|
19
|
+
* working Ryuk reaper (privileged runner or `docker.sock` mount).
|
|
20
|
+
* 2. **DynamoDB Local ZIP + Java** — development/CI fallback for hosts
|
|
21
|
+
* with Java (e.g. the CI runner image with a JRE, or dev laptops
|
|
22
|
+
* without Docker Desktop). Downloads the official AWS ZIP once
|
|
23
|
+
* into a per-user cache dir, runs it with `-inMemory` so per-run
|
|
24
|
+
* state is isolated by port.
|
|
13
25
|
* 3. **Skip** — sets `process.env.DDB_AVAILABLE = '0'`; integration
|
|
14
26
|
* tests gate themselves on this via `describe.skipIf`.
|
|
15
27
|
*
|
|
16
28
|
* The skip path keeps the suite green in environments that legitimately
|
|
17
29
|
* can't run DynamoDB Local (no Docker, no Java). The acceptance criterion
|
|
18
|
-
* is "all
|
|
30
|
+
* is "all scenarios pass when DDB is available", not "every dev box
|
|
19
31
|
* runs them".
|
|
20
32
|
*/
|
|
21
33
|
|
|
@@ -51,34 +63,47 @@ interface DdbHandle {
|
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
export default async function globalSetup(): Promise<(() => Promise<void>) | undefined> {
|
|
66
|
+
const strategy = (process.env.DDB_LOCAL_STRATEGY ?? 'auto').toLowerCase();
|
|
67
|
+
// Strategy pin: skip the unused probe to avoid a multi-second timeout
|
|
68
|
+
// on hosts that lack the capability. CI pins `zip`; local dev defaults
|
|
69
|
+
// to `auto` so a Docker Desktop install still gets testcontainers.
|
|
70
|
+
const wantDocker = strategy === 'auto' || strategy === 'docker';
|
|
71
|
+
const wantZip = strategy === 'auto' || strategy === 'zip';
|
|
72
|
+
|
|
54
73
|
// Strategy 1: testcontainers (Docker).
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if (wantDocker) {
|
|
75
|
+
try {
|
|
76
|
+
const handle = await tryTestcontainers();
|
|
77
|
+
if (handle !== null) {
|
|
78
|
+
process.env.DDB_AVAILABLE = '1';
|
|
79
|
+
process.env.DYNAMO_ENDPOINT = handle.endpoint;
|
|
80
|
+
console.warn(`[aws-adapter global-setup] DynamoDB Local via Docker at ${handle.endpoint}`);
|
|
81
|
+
return async () => {
|
|
82
|
+
await handle.stop();
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
} catch (err: unknown) {
|
|
86
|
+
console.warn('[aws-adapter global-setup] testcontainers unavailable:', renderErr(err));
|
|
64
87
|
}
|
|
65
|
-
} catch (err: unknown) {
|
|
66
|
-
console.warn('[aws-adapter global-setup] testcontainers unavailable:', renderErr(err));
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
// Strategy 2: local ZIP + Java.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
if (wantZip) {
|
|
92
|
+
try {
|
|
93
|
+
const handle = await tryZip();
|
|
94
|
+
if (handle !== null) {
|
|
95
|
+
process.env.DDB_AVAILABLE = '1';
|
|
96
|
+
process.env.DYNAMO_ENDPOINT = handle.endpoint;
|
|
97
|
+
console.warn(
|
|
98
|
+
`[aws-adapter global-setup] DynamoDB Local via Java ZIP at ${handle.endpoint}`,
|
|
99
|
+
);
|
|
100
|
+
return async () => {
|
|
101
|
+
await handle.stop();
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
} catch (err: unknown) {
|
|
105
|
+
console.warn('[aws-adapter global-setup] ZIP fallback unavailable:', renderErr(err));
|
|
79
106
|
}
|
|
80
|
-
} catch (err: unknown) {
|
|
81
|
-
console.warn('[aws-adapter global-setup] ZIP fallback unavailable:', renderErr(err));
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
// Strategy 3: skip.
|
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
import { GoneException } from '@aws-sdk/client-apigatewaymanagementapi';
|
|
12
12
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
13
13
|
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
type Nick,
|
|
17
|
-
type ParsedServerConfig,
|
|
18
|
-
createConnection,
|
|
19
|
-
} from '@serverless-ircd/irc-core';
|
|
14
|
+
import { type Nick, type ParsedServerConfig, createConnection } from '@serverless-ircd/irc-core';
|
|
15
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
20
16
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
21
17
|
import { AwsRuntime, type PostToConnection, cleanupConnection } from '../src/aws-runtime.js';
|
|
22
18
|
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
@@ -27,12 +23,11 @@ import type { TablesConfig } from '../src/tables.js';
|
|
|
27
23
|
const available = process.env.DDB_AVAILABLE === '1';
|
|
28
24
|
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
29
25
|
|
|
30
|
-
const SERVER_CONFIG: ParsedServerConfig = {
|
|
31
|
-
...DEFAULT_SERVER_CONFIG,
|
|
26
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
32
27
|
serverName: 'irc.example.com',
|
|
33
28
|
networkName: 'ExampleNet',
|
|
34
29
|
motdLines: [],
|
|
35
|
-
};
|
|
30
|
+
});
|
|
36
31
|
|
|
37
32
|
interface Fixture {
|
|
38
33
|
tables: TablesConfig;
|