serverless-ircd 0.1.0 → 0.3.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 +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +436 -0
- package/README.md +127 -84
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +5 -1
- package/apps/cf-worker/wrangler.toml +66 -17
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +57 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +267 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +126 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- 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 +93 -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 +86 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1186 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +11 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +19 -14
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/aws-runtime.ts +871 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
- package/packages/aws-adapter/src/config-loader.ts +126 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +330 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
- package/packages/aws-adapter/src/handlers/index.ts +293 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +74 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +171 -0
- package/packages/aws-adapter/tests/account-store.test.ts +280 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +213 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +473 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/cf-runtime.ts +42 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +135 -0
- package/packages/cf-adapter/src/connection-do.ts +119 -0
- package/packages/cf-adapter/src/env.ts +27 -1
- package/packages/cf-adapter/src/index.ts +2 -1
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
- package/packages/cf-adapter/tests/worker/main.ts +2 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +10 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
- package/packages/irc-core/package.json +13 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +9 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +63 -10
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +79 -21
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/config.ts +270 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +7 -0
- package/packages/irc-core/src/ports.ts +719 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +88 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/account-store.test.ts +88 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- package/packages/irc-core/tests/commands/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +380 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/config.test.ts +721 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +186 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +160 -0
- package/packages/irc-server/tests/actor.test.ts +690 -15
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +204 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +6 -5
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/seed-aws-accounts.ts +80 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* --target <url> Upstream ws:// or wss:// URL (required).
|
|
15
15
|
* --listen-port <n> TCP port to listen on (default 6667).
|
|
16
16
|
* --listen-host <addr> Interface to bind (default 127.0.0.1).
|
|
17
|
+
* --log-level <level> debug | info | warn | error (default info,
|
|
18
|
+
* or $LOG_LEVEL if set).
|
|
17
19
|
* -h, --help Show this help and exit.
|
|
18
20
|
*
|
|
19
21
|
* Point your IRC client at 127.0.0.1:<listen-port> and it will appear as
|
|
@@ -21,15 +23,22 @@
|
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
import { startForwarder } from './forwarder.js';
|
|
26
|
+
import { ConsoleLogger, LogLevel, type Logger, logLevelLabel, parseLogLevel } from './logger.js';
|
|
24
27
|
|
|
25
28
|
interface CliArgs {
|
|
26
29
|
target: string | undefined;
|
|
27
30
|
listenPort: number;
|
|
28
31
|
listenHost: string;
|
|
32
|
+
logLevel: LogLevel | undefined;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
function parseArgs(argv: string[]): CliArgs {
|
|
32
|
-
const out: CliArgs = {
|
|
36
|
+
const out: CliArgs = {
|
|
37
|
+
target: undefined,
|
|
38
|
+
listenPort: 6667,
|
|
39
|
+
listenHost: '127.0.0.1',
|
|
40
|
+
logLevel: undefined,
|
|
41
|
+
};
|
|
33
42
|
for (let i = 0; i < argv.length; i++) {
|
|
34
43
|
const a = argv[i];
|
|
35
44
|
if (a === undefined) continue;
|
|
@@ -43,6 +52,9 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
43
52
|
} else if (a === '--listen-host' && next !== undefined) {
|
|
44
53
|
out.listenHost = next;
|
|
45
54
|
i++;
|
|
55
|
+
} else if (a === '--log-level' && next !== undefined) {
|
|
56
|
+
out.logLevel = parseLogLevel(next);
|
|
57
|
+
i++;
|
|
46
58
|
} else if (a === '--help' || a === '-h') {
|
|
47
59
|
process.stdout.write(
|
|
48
60
|
[
|
|
@@ -52,6 +64,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
52
64
|
' --target <url> Upstream ws:// or wss:// URL (required).',
|
|
53
65
|
' --listen-port <n> TCP port to listen on (default 6667).',
|
|
54
66
|
' --listen-host <addr> Interface to bind (default 127.0.0.1).',
|
|
67
|
+
' --log-level <level> debug | info | warn | error (default info, or $LOG_LEVEL).',
|
|
55
68
|
' -h, --help Show this help and exit.',
|
|
56
69
|
'',
|
|
57
70
|
].join('\n'),
|
|
@@ -68,6 +81,14 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
68
81
|
return out;
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
/** Resolves the effective log level: CLI flag, else $LOG_LEVEL, else info. */
|
|
85
|
+
function resolveLogLevel(cli: LogLevel | undefined): LogLevel {
|
|
86
|
+
if (cli !== undefined) return cli;
|
|
87
|
+
const env = process.env.LOG_LEVEL;
|
|
88
|
+
if (env !== undefined && env.trim() !== '') return parseLogLevel(env);
|
|
89
|
+
return LogLevel.Info;
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
async function main(): Promise<void> {
|
|
72
93
|
const args = parseArgs(process.argv.slice(2));
|
|
73
94
|
if (args.target === undefined) {
|
|
@@ -75,30 +96,28 @@ async function main(): Promise<void> {
|
|
|
75
96
|
process.exit(2);
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
const logLevel = resolveLogLevel(args.logLevel);
|
|
100
|
+
const logger: Logger = new ConsoleLogger(undefined, logLevel);
|
|
101
|
+
|
|
78
102
|
const forwarder = await startForwarder({
|
|
79
103
|
listenPort: args.listenPort,
|
|
80
104
|
listenHost: args.listenHost,
|
|
81
105
|
targetUrl: args.target,
|
|
106
|
+
logLevel,
|
|
107
|
+
});
|
|
108
|
+
logger.info('tcp-ws-forwarder listening', {
|
|
109
|
+
listen: `${forwarder.listenHost}:${forwarder.listenPort}`,
|
|
110
|
+
target: forwarder.targetUrl,
|
|
111
|
+
level: logLevelLabel(logLevel),
|
|
82
112
|
});
|
|
83
|
-
process.stdout.write(
|
|
84
|
-
`${JSON.stringify({
|
|
85
|
-
ts: new Date().toISOString(),
|
|
86
|
-
level: 'info',
|
|
87
|
-
msg: 'tcp-ws-forwarder listening',
|
|
88
|
-
listen: `${forwarder.listenHost}:${forwarder.listenPort}`,
|
|
89
|
-
target: forwarder.targetUrl,
|
|
90
|
-
})}\n`,
|
|
91
|
-
);
|
|
92
113
|
|
|
93
114
|
const shutdown = (signal: string): void => {
|
|
94
|
-
|
|
95
|
-
`${JSON.stringify({ ts: new Date().toISOString(), level: 'info', msg: 'shutdown', signal })}\n`,
|
|
96
|
-
);
|
|
115
|
+
logger.info('shutdown', { signal });
|
|
97
116
|
forwarder
|
|
98
117
|
.close()
|
|
99
118
|
.then(() => process.exit(0))
|
|
100
119
|
.catch((err) => {
|
|
101
|
-
|
|
120
|
+
logger.error('shutdown error', { err });
|
|
102
121
|
process.exit(1);
|
|
103
122
|
});
|
|
104
123
|
};
|
|
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
6
6
|
import type { RawData, WebSocket } from 'ws';
|
|
7
7
|
import { WebSocketServer as WsServer } from 'ws';
|
|
8
8
|
import { startForwarder } from '../src/forwarder';
|
|
9
|
+
import { LogLevel } from '../src/logger';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Integration tests for the TCP↔WS forwarder against real sockets.
|
|
@@ -616,3 +617,88 @@ describe('forwarder — error handling', () => {
|
|
|
616
617
|
errSpy.mockRestore();
|
|
617
618
|
});
|
|
618
619
|
});
|
|
620
|
+
|
|
621
|
+
describe('forwarder — logging', () => {
|
|
622
|
+
/** Collects the `msg` field from every JSON line a console sink received. */
|
|
623
|
+
function msgsFrom(spy: { mock: { calls: unknown[][] } }): string[] {
|
|
624
|
+
return spy.mock.calls.map((c) => JSON.parse(String(c[0])).msg as string);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
it('at logLevel debug, logs bridge.accepted (info) + tcp.line/ws.open/ws.frame (debug)', async () => {
|
|
628
|
+
const target = await startTarget();
|
|
629
|
+
openServers.push(target);
|
|
630
|
+
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined);
|
|
631
|
+
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => undefined);
|
|
632
|
+
|
|
633
|
+
const fwd = await startForwarder({
|
|
634
|
+
listenPort: 0,
|
|
635
|
+
targetUrl: `ws://127.0.0.1:${target.port}`,
|
|
636
|
+
logLevel: LogLevel.Debug,
|
|
637
|
+
});
|
|
638
|
+
openServers.push(fwd);
|
|
639
|
+
|
|
640
|
+
const client = new TcpClient(fwd.listenPort);
|
|
641
|
+
openClients.push(client);
|
|
642
|
+
await client.opened();
|
|
643
|
+
client.send('PING :tok');
|
|
644
|
+
|
|
645
|
+
const ws = await target.waitForConnection();
|
|
646
|
+
ws.send(':srv PONG :tok');
|
|
647
|
+
await new Promise<void>((res) => ws.once('message', () => res())); // wait TCP→WS
|
|
648
|
+
await client.waitFor((l) => l.includes('PONG')); // wait WS→TCP
|
|
649
|
+
|
|
650
|
+
const debug = msgsFrom(debugSpy);
|
|
651
|
+
const info = msgsFrom(infoSpy);
|
|
652
|
+
expect(info).toContain('bridge.accepted');
|
|
653
|
+
expect(debug).toContain('tcp.line');
|
|
654
|
+
expect(debug).toContain('ws.open');
|
|
655
|
+
expect(debug).toContain('ws.frame');
|
|
656
|
+
infoSpy.mockRestore();
|
|
657
|
+
debugSpy.mockRestore();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('at the default level (info), suppresses debug trace logs but keeps lifecycle', async () => {
|
|
661
|
+
const target = await startTarget();
|
|
662
|
+
openServers.push(target);
|
|
663
|
+
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined);
|
|
664
|
+
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => undefined);
|
|
665
|
+
|
|
666
|
+
const fwd = await startForwarder({ listenPort: 0, targetUrl: `ws://127.0.0.1:${target.port}` });
|
|
667
|
+
openServers.push(fwd);
|
|
668
|
+
|
|
669
|
+
const client = new TcpClient(fwd.listenPort);
|
|
670
|
+
openClients.push(client);
|
|
671
|
+
await client.opened();
|
|
672
|
+
client.send('PING :tok');
|
|
673
|
+
const ws = await target.waitForConnection();
|
|
674
|
+
await new Promise<void>((res) => ws.once('message', () => res()));
|
|
675
|
+
|
|
676
|
+
expect(debugSpy).not.toHaveBeenCalled();
|
|
677
|
+
expect(msgsFrom(infoSpy)).toContain('bridge.accepted');
|
|
678
|
+
infoSpy.mockRestore();
|
|
679
|
+
debugSpy.mockRestore();
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('logs bridge.closed with the tcp-close reason when the client disconnects', async () => {
|
|
683
|
+
const target = await startTarget();
|
|
684
|
+
openServers.push(target);
|
|
685
|
+
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined);
|
|
686
|
+
const fwd = await startForwarder({ listenPort: 0, targetUrl: `ws://127.0.0.1:${target.port}` });
|
|
687
|
+
openServers.push(fwd);
|
|
688
|
+
|
|
689
|
+
const client = new TcpClient(fwd.listenPort);
|
|
690
|
+
openClients.push(client);
|
|
691
|
+
await client.opened();
|
|
692
|
+
await target.waitForConnection();
|
|
693
|
+
await client.close();
|
|
694
|
+
await new Promise((r) => setImmediate(r)); // let the close event propagate
|
|
695
|
+
|
|
696
|
+
const closed = msgsFrom(infoSpy).filter((m) => m === 'bridge.closed');
|
|
697
|
+
expect(closed).toHaveLength(1);
|
|
698
|
+
const rec = JSON.parse(
|
|
699
|
+
String(infoSpy.mock.calls.find((c) => JSON.parse(String(c[0])).msg === 'bridge.closed')?.[0]),
|
|
700
|
+
);
|
|
701
|
+
expect(rec.reason).toBe('tcp-close');
|
|
702
|
+
infoSpy.mockRestore();
|
|
703
|
+
});
|
|
704
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ConsoleLogger, LogLevel, parseLogLevel } from '../src/logger';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Unit tests for the forwarder's self-contained structured logger.
|
|
6
|
+
*
|
|
7
|
+
* The forwarder intentionally has no `@serverless-ircd/*` dependency, so it
|
|
8
|
+
* ships its own tiny logger that mirrors the irc-core conventions: a numeric
|
|
9
|
+
* `LogLevel` enum, one JSON line per record, injectable sinks (so tests never
|
|
10
|
+
* touch the real `console`), and `minLevel` filtering.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Sink that captures every emitted line per level into mutable arrays. */
|
|
14
|
+
function capturingSinks() {
|
|
15
|
+
const lines = {
|
|
16
|
+
debug: [] as string[],
|
|
17
|
+
info: [] as string[],
|
|
18
|
+
warn: [] as string[],
|
|
19
|
+
error: [] as string[],
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
lines,
|
|
23
|
+
sinks: {
|
|
24
|
+
debug: (l: string) => lines.debug.push(l),
|
|
25
|
+
info: (l: string) => lines.info.push(l),
|
|
26
|
+
warn: (l: string) => lines.warn.push(l),
|
|
27
|
+
error: (l: string) => lines.error.push(l),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('parseLogLevel', () => {
|
|
33
|
+
it('maps each canonical lower-case label to its level', () => {
|
|
34
|
+
expect(parseLogLevel('debug')).toBe(LogLevel.Debug);
|
|
35
|
+
expect(parseLogLevel('info')).toBe(LogLevel.Info);
|
|
36
|
+
expect(parseLogLevel('warn')).toBe(LogLevel.Warn);
|
|
37
|
+
expect(parseLogLevel('error')).toBe(LogLevel.Error);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('is case-insensitive', () => {
|
|
41
|
+
expect(parseLogLevel('DEBUG')).toBe(LogLevel.Debug);
|
|
42
|
+
expect(parseLogLevel('Warn')).toBe(LogLevel.Warn);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('trims surrounding whitespace', () => {
|
|
46
|
+
expect(parseLogLevel(' info ')).toBe(LogLevel.Info);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('throws for an unknown label', () => {
|
|
50
|
+
expect(() => parseLogLevel('verbose')).toThrow();
|
|
51
|
+
expect(() => parseLogLevel('')).toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('ConsoleLogger — level filtering', () => {
|
|
56
|
+
it('emits records at or above the configured min level', () => {
|
|
57
|
+
const { lines, sinks } = capturingSinks();
|
|
58
|
+
const log = new ConsoleLogger(sinks, LogLevel.Info);
|
|
59
|
+
log.debug('skip me');
|
|
60
|
+
log.info('keep me');
|
|
61
|
+
log.warn('keep too');
|
|
62
|
+
log.error('keep three');
|
|
63
|
+
expect(lines.debug).toHaveLength(0);
|
|
64
|
+
expect(lines.info).toHaveLength(1);
|
|
65
|
+
expect(lines.warn).toHaveLength(1);
|
|
66
|
+
expect(lines.error).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('defaults to debug (lowest) so nothing is suppressed', () => {
|
|
70
|
+
const { lines, sinks } = capturingSinks();
|
|
71
|
+
const log = new ConsoleLogger(sinks);
|
|
72
|
+
log.debug('d');
|
|
73
|
+
log.info('i');
|
|
74
|
+
expect(lines.debug).toHaveLength(1);
|
|
75
|
+
expect(lines.info).toHaveLength(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('suppresses everything at a level above error (error-only)', () => {
|
|
79
|
+
// error-only means minLevel = Error: warn and below are dropped.
|
|
80
|
+
const { lines, sinks } = capturingSinks();
|
|
81
|
+
const log = new ConsoleLogger(sinks, LogLevel.Error);
|
|
82
|
+
log.warn('dropped');
|
|
83
|
+
log.info('dropped');
|
|
84
|
+
log.error('kept');
|
|
85
|
+
expect(lines.warn).toHaveLength(0);
|
|
86
|
+
expect(lines.info).toHaveLength(0);
|
|
87
|
+
expect(lines.error).toHaveLength(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('ConsoleLogger — output shape', () => {
|
|
92
|
+
it('emits one JSON line with ts, level, msg, and merged fields', () => {
|
|
93
|
+
const { lines, sinks } = capturingSinks();
|
|
94
|
+
const log = new ConsoleLogger(sinks, LogLevel.Info);
|
|
95
|
+
log.info('listening', { listen: '127.0.0.1:6667', target: 'wss://x/' });
|
|
96
|
+
const rec = JSON.parse(lines.info[0] as string);
|
|
97
|
+
expect(rec.level).toBe('info');
|
|
98
|
+
expect(rec.msg).toBe('listening');
|
|
99
|
+
expect(rec.listen).toBe('127.0.0.1:6667');
|
|
100
|
+
expect(rec.target).toBe('wss://x/');
|
|
101
|
+
expect(typeof rec.ts).toBe('string');
|
|
102
|
+
expect(new Date(rec.ts).toString()).not.toBe('Invalid Date');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('serializes an Error field into { name, message, stack }', () => {
|
|
106
|
+
const { lines, sinks } = capturingSinks();
|
|
107
|
+
const log = new ConsoleLogger(sinks, LogLevel.Error);
|
|
108
|
+
const err = new Error('boom');
|
|
109
|
+
log.error('forwarder tcp socket error', { err });
|
|
110
|
+
const rec = JSON.parse(lines.error[0] as string);
|
|
111
|
+
expect(rec.msg).toBe('forwarder tcp socket error');
|
|
112
|
+
expect(rec.err).toMatchObject({ name: 'Error', message: 'boom' });
|
|
113
|
+
expect(typeof rec.err.stack).toBe('string');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('passes a non-Error field through unchanged', () => {
|
|
117
|
+
const { lines, sinks } = capturingSinks();
|
|
118
|
+
const log = new ConsoleLogger(sinks, LogLevel.Error);
|
|
119
|
+
log.error('upstream failure', { err: 'a bare string failure' });
|
|
120
|
+
const rec = JSON.parse(lines.error[0] as string);
|
|
121
|
+
expect(rec.err).toBe('a bare string failure');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('routes each level to its matching sink', () => {
|
|
125
|
+
const { lines, sinks } = capturingSinks();
|
|
126
|
+
const log = new ConsoleLogger(sinks, LogLevel.Debug);
|
|
127
|
+
log.debug('d');
|
|
128
|
+
log.info('i');
|
|
129
|
+
log.warn('w');
|
|
130
|
+
log.error('e');
|
|
131
|
+
expect(JSON.parse(lines.debug[0] as string).msg).toBe('d');
|
|
132
|
+
expect(JSON.parse(lines.info[0] as string).msg).toBe('i');
|
|
133
|
+
expect(JSON.parse(lines.warn[0] as string).msg).toBe('w');
|
|
134
|
+
expect(JSON.parse(lines.error[0] as string).msg).toBe('e');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-tree parametrized IRC scenario suite (folded in from the former
|
|
3
|
-
* `@serverless-ircd/integration-tests` workspace package).
|
|
4
|
-
*
|
|
5
|
-
* Each runtime (in-memory now; CF via `../cf-harness.ts`; AWS in Phase 4)
|
|
6
|
-
* ships one {@link IrcHarnessFactory}; the scenario tests run against
|
|
7
|
-
* every registered factory.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export type {
|
|
11
|
-
ClientHarness,
|
|
12
|
-
IrcHarness,
|
|
13
|
-
IrcHarnessFactory,
|
|
14
|
-
SpawnClientOptions,
|
|
15
|
-
} from './harness.js';
|
|
16
|
-
export { inMemoryHarnessFactory, HARNESS_SERVER_CONFIG } from './in-memory-harness.js';
|
|
17
|
-
export { runIrcScenarios } from './scenarios.js';
|
|
File without changes
|
|
File without changes
|