serverless-ircd 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +349 -18
- package/README.md +132 -30
- package/apps/aws-stack/README.md +6 -5
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +150 -10
- package/apps/aws-stack/tests/stack.test.ts +145 -4
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +149 -24
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +75 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +96 -9
- package/packages/aws-adapter/src/handlers/default.ts +98 -7
- package/packages/aws-adapter/src/handlers/index.ts +109 -4
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +8 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +174 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +302 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +5 -1
- package/packages/cf-adapter/src/cf-runtime.ts +68 -5
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +278 -85
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +54 -11
- package/packages/cf-adapter/src/index.ts +11 -8
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +12 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
- package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +15 -8
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/wrangler.test.toml +15 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +24 -3
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +17 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +6 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +4 -8
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +65 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +33 -30
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +360 -12
- package/packages/irc-core/src/protocol/numerics.ts +48 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +46 -2
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +57 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +102 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +393 -16
- package/packages/irc-server/src/dispatch.ts +1 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +15 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +104 -0
- package/packages/irc-server/tests/actor.test.ts +1489 -4
- package/packages/irc-server/tests/dispatch.test.ts +37 -17
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +5 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +141 -3
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +10 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +9 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +107 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { RawLine } from '../src/effects';
|
|
3
3
|
import { Effect as EffectValue } from '../src/effects';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
EmptyMotdProvider,
|
|
6
|
+
FakeClock,
|
|
7
|
+
InMemoryNickHistoryStore,
|
|
8
|
+
SequentialIdFactory,
|
|
9
|
+
} from '../src/ports';
|
|
5
10
|
import { createConnection } from '../src/state/connection';
|
|
6
11
|
import {
|
|
7
12
|
type Ctx,
|
|
@@ -97,6 +102,35 @@ describe('Ctx / Reducer types', () => {
|
|
|
97
102
|
const keys = Object.keys(ctx) as Array<keyof Ctx>;
|
|
98
103
|
expect(keys).not.toContain('msg');
|
|
99
104
|
});
|
|
105
|
+
|
|
106
|
+
it('threads an optional history store through buildCtx onto ctx.history', () => {
|
|
107
|
+
const history = new InMemoryNickHistoryStore(new FakeClock(0));
|
|
108
|
+
const serverConfig: ServerConfig = {
|
|
109
|
+
serverName: 'irc.example.com',
|
|
110
|
+
networkName: 'ExampleNet',
|
|
111
|
+
maxChannelsPerUser: 30,
|
|
112
|
+
maxTargetsPerCommand: 10,
|
|
113
|
+
maxListEntries: 50,
|
|
114
|
+
nickLen: 30,
|
|
115
|
+
channelLen: 50,
|
|
116
|
+
topicLen: 390,
|
|
117
|
+
quitMessage: 'Client Quit',
|
|
118
|
+
};
|
|
119
|
+
const ctx = buildCtx({
|
|
120
|
+
serverConfig,
|
|
121
|
+
clock: new FakeClock(1_000),
|
|
122
|
+
ids: new SequentialIdFactory(),
|
|
123
|
+
motd: EmptyMotdProvider,
|
|
124
|
+
connection: createConnection({ id: 'c1', connectedSince: 0 }),
|
|
125
|
+
history,
|
|
126
|
+
});
|
|
127
|
+
expect(ctx.history).toBe(history);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('omits ctx.history when no history store is supplied', () => {
|
|
131
|
+
const ctx = makeCtx();
|
|
132
|
+
expect(ctx.history).toBeUndefined();
|
|
133
|
+
});
|
|
100
134
|
});
|
|
101
135
|
|
|
102
136
|
describe('ServerConfig shape', () => {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
MAX_WS_MESSAGE_BYTES,
|
|
4
|
+
type WsFrameMode,
|
|
5
|
+
frameToLines,
|
|
6
|
+
isWithinWsByteBudget,
|
|
7
|
+
sanitizeForTextMode,
|
|
8
|
+
stripTrailingCrLf,
|
|
9
|
+
wsFrameModeFor,
|
|
10
|
+
} from '../src/ws-framing';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// MAX_WS_MESSAGE_BYTES constant
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
describe('MAX_WS_MESSAGE_BYTES', () => {
|
|
17
|
+
it('is 510 (the IRC line-content limit; WS spec omits the trailing CRLF)', () => {
|
|
18
|
+
expect(MAX_WS_MESSAGE_BYTES).toBe(510);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// wsFrameModeFor — subprotocol → WsFrameMode mapping
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
describe('wsFrameModeFor', () => {
|
|
27
|
+
it('maps text.ircv3.net to spec-text', () => {
|
|
28
|
+
expect(wsFrameModeFor('text.ircv3.net')).toBe('spec-text');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('maps binary.ircv3.net to spec-binary', () => {
|
|
32
|
+
expect(wsFrameModeFor('binary.ircv3.net')).toBe('spec-binary');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('maps undefined (no subprotocol) to legacy', () => {
|
|
36
|
+
expect(wsFrameModeFor(undefined)).toBe('legacy');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// stripTrailingCrLf
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
describe('stripTrailingCrLf', () => {
|
|
45
|
+
it('strips a trailing CR-LF', () => {
|
|
46
|
+
expect(stripTrailingCrLf('NICK bob\r\n')).toBe('NICK bob');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('strips a trailing bare LF', () => {
|
|
50
|
+
expect(stripTrailingCrLf('NICK bob\n')).toBe('NICK bob');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('leaves a string with no trailing terminator unchanged', () => {
|
|
54
|
+
expect(stripTrailingCrLf('NICK bob')).toBe('NICK bob');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('strips only a single trailing terminator', () => {
|
|
58
|
+
expect(stripTrailingCrLf('a\r\n\r\n')).toBe('a\r\n');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns the empty string unchanged', () => {
|
|
62
|
+
expect(stripTrailingCrLf('')).toBe('');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not strip a leading CR-LF', () => {
|
|
66
|
+
expect(stripTrailingCrLf('\r\nNICK bob')).toBe('\r\nNICK bob');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// frameToLines — spec-text / spec-binary modes (one IRC message per frame)
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
describe('frameToLines — spec-text', () => {
|
|
75
|
+
it('strips a trailing CR-LF and returns a single-element array', () => {
|
|
76
|
+
expect(frameToLines('NICK bob\r\n', 'spec-text')).toEqual(['NICK bob']);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('strips a trailing bare LF', () => {
|
|
80
|
+
expect(frameToLines('NICK bob\n', 'spec-text')).toEqual(['NICK bob']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not split on an embedded LF (one WS message === one IRC line)', () => {
|
|
84
|
+
expect(frameToLines('NICK bob\nextra', 'spec-text')).toEqual(['NICK bob\nextra']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns an empty array for an empty frame', () => {
|
|
88
|
+
expect(frameToLines('', 'spec-text')).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns an empty array for a frame that is only a terminator', () => {
|
|
92
|
+
expect(frameToLines('\r\n', 'spec-text')).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns a single bare line unchanged when there is no terminator', () => {
|
|
96
|
+
expect(frameToLines('PING :tok', 'spec-text')).toEqual(['PING :tok']);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('frameToLines — spec-binary', () => {
|
|
101
|
+
it('behaves identically to spec-text (single line, terminator stripped)', () => {
|
|
102
|
+
expect(frameToLines('NICK bob\n', 'spec-binary')).toEqual(['NICK bob']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('does not split on an embedded LF', () => {
|
|
106
|
+
expect(frameToLines('a\nb', 'spec-binary')).toEqual(['a\nb']);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// frameToLines — legacy mode (the pre-spec split-on-/\r?\n/ behaviour)
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
describe('frameToLines — legacy', () => {
|
|
115
|
+
it('splits on CR-LF into separate lines', () => {
|
|
116
|
+
expect(frameToLines('a\r\nb', 'legacy')).toEqual(['a', 'b']);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('splits on bare LF', () => {
|
|
120
|
+
expect(frameToLines('a\nb', 'legacy')).toEqual(['a', 'b']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns an empty array for the empty string', () => {
|
|
124
|
+
expect(frameToLines('', 'legacy')).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns a single bare line with no terminator unchanged', () => {
|
|
128
|
+
expect(frameToLines('PING :tok', 'legacy')).toEqual(['PING :tok']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('emits a trailing empty string for a terminated frame', () => {
|
|
132
|
+
expect(frameToLines('PING :tok\r\n', 'legacy')).toEqual(['PING :tok', '']);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('splits several joined messages keeping a trailing empty string', () => {
|
|
136
|
+
expect(frameToLines('a\r\nb\r\n', 'legacy')).toEqual(['a', 'b', '']);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Exhaustiveness guard: every declared mode is covered by the specs above.
|
|
141
|
+
describe('frameToLines — mode coverage', () => {
|
|
142
|
+
const modes: WsFrameMode[] = ['spec-text', 'spec-binary', 'legacy'];
|
|
143
|
+
for (const mode of modes) {
|
|
144
|
+
it(`accepts the '${mode}' mode without throwing`, () => {
|
|
145
|
+
expect(Array.isArray(frameToLines('x', mode))).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// isWithinWsByteBudget — the IRCv3 WS message byte cap
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
describe('isWithinWsByteBudget', () => {
|
|
155
|
+
it('accepts a frame whose length equals the 510-byte budget', () => {
|
|
156
|
+
expect(isWithinWsByteBudget('a'.repeat(510))).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('rejects a frame whose length is one byte over the budget', () => {
|
|
160
|
+
expect(isWithinWsByteBudget('a'.repeat(511))).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('accepts the empty string', () => {
|
|
164
|
+
expect(isWithinWsByteBudget('')).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('accepts a short frame', () => {
|
|
168
|
+
expect(isWithinWsByteBudget('NICK bob')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// sanitizeForTextMode — replace lone surrogates with U+FFFD
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
describe('sanitizeForTextMode', () => {
|
|
177
|
+
it('replaces a lone high surrogate with U+FFFD', () => {
|
|
178
|
+
expect(sanitizeForTextMode('\uD800')).toBe('\uFFFD');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('replaces a lone low surrogate with U+FFFD', () => {
|
|
182
|
+
expect(sanitizeForTextMode('\uDC00')).toBe('\uFFFD');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('leaves a valid surrogate pair (a supplementary code point) unchanged', () => {
|
|
186
|
+
// U+10000 = \uD800\uDC00
|
|
187
|
+
expect(sanitizeForTextMode('\uD800\uDC00')).toBe('\uD800\uDC00');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('leaves plain ASCII text unchanged', () => {
|
|
191
|
+
expect(sanitizeForTextMode('hello')).toBe('hello');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('replaces a lone surrogate embedded in normal text', () => {
|
|
195
|
+
expect(sanitizeForTextMode('a\uD800b')).toBe('a\uFFFDb');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('replaces two consecutive lone high surrogates with two U+FFFD', () => {
|
|
199
|
+
expect(sanitizeForTextMode('\uD800\uD800')).toBe('\uFFFD\uFFFD');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('preserves a valid pair surrounded by normal text', () => {
|
|
203
|
+
expect(sanitizeForTextMode('x\uD83D\uDE00y')).toBe('x\uD83D\uDE00y');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('replaces a lone high surrogate followed later by a lone low surrogate', () => {
|
|
207
|
+
expect(sanitizeForTextMode('\uD800a\uDC00')).toBe('\uFFFDa\uFFFD');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('returns the empty string unchanged', () => {
|
|
211
|
+
expect(sanitizeForTextMode('')).toBe('');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
WS_SUBPROTO_BINARY,
|
|
4
|
+
WS_SUBPROTO_TEXT,
|
|
5
|
+
type WsSubprotocol,
|
|
6
|
+
parseSecWsProtocolOffers,
|
|
7
|
+
selectSubprotocol,
|
|
8
|
+
} from '../src/ws-subprotocol';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// constants & type
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
describe('WS subprotocol constants', () => {
|
|
15
|
+
it('exposes the IRCv3 text subprotocol name', () => {
|
|
16
|
+
expect(WS_SUBPROTO_TEXT).toBe('text.ircv3.net');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('exposes the IRCv3 binary subprotocol name', () => {
|
|
20
|
+
expect(WS_SUBPROTO_BINARY).toBe('binary.ircv3.net');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('the WsSubprotocol type resolves to the two literal names', () => {
|
|
24
|
+
const a: WsSubprotocol = WS_SUBPROTO_TEXT;
|
|
25
|
+
const b: WsSubprotocol = WS_SUBPROTO_BINARY;
|
|
26
|
+
expect([a, b]).toEqual([WS_SUBPROTO_TEXT, WS_SUBPROTO_BINARY]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// parseSecWsProtocolOffers — header parsing
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
describe('parseSecWsProtocolOffers', () => {
|
|
35
|
+
it('splits a comma-separated list into ordered offers', () => {
|
|
36
|
+
expect(parseSecWsProtocolOffers('text.ircv3.net, binary.ircv3.net')).toEqual([
|
|
37
|
+
'text.ircv3.net',
|
|
38
|
+
'binary.ircv3.net',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('trims whitespace around each item', () => {
|
|
43
|
+
expect(parseSecWsProtocolOffers(' text.ircv3.net , binary.ircv3.net ')).toEqual([
|
|
44
|
+
'text.ircv3.net',
|
|
45
|
+
'binary.ircv3.net',
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves client-preference order', () => {
|
|
50
|
+
expect(parseSecWsProtocolOffers('binary.ircv3.net, text.ircv3.net')).toEqual([
|
|
51
|
+
'binary.ircv3.net',
|
|
52
|
+
'text.ircv3.net',
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns an empty array for null', () => {
|
|
57
|
+
expect(parseSecWsProtocolOffers(null)).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns an empty array for undefined', () => {
|
|
61
|
+
expect(parseSecWsProtocolOffers(undefined)).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns an empty array for an empty string', () => {
|
|
65
|
+
expect(parseSecWsProtocolOffers('')).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('drops empty entries produced by stray commas', () => {
|
|
69
|
+
expect(parseSecWsProtocolOffers('text.ircv3.net,, ,binary.ircv3.net')).toEqual([
|
|
70
|
+
'text.ircv3.net',
|
|
71
|
+
'binary.ircv3.net',
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('preserves duplicate entries in order', () => {
|
|
76
|
+
expect(parseSecWsProtocolOffers('text.ircv3.net, text.ircv3.net')).toEqual([
|
|
77
|
+
'text.ircv3.net',
|
|
78
|
+
'text.ircv3.net',
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// selectSubprotocol — preference-ordered selection
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
describe('selectSubprotocol', () => {
|
|
88
|
+
it('returns the first supported offer in client-preference order (text first)', () => {
|
|
89
|
+
expect(selectSubprotocol(['text.ircv3.net', 'binary.ircv3.net'])).toBe('text.ircv3.net');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('honours client preference when binary is offered first', () => {
|
|
93
|
+
expect(selectSubprotocol(['binary.ircv3.net', 'text.ircv3.net'])).toBe('binary.ircv3.net');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns null when only unsupported protocols are offered', () => {
|
|
97
|
+
expect(selectSubprotocol(['foo', 'bar'])).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns null for an empty offer list', () => {
|
|
101
|
+
expect(selectSubprotocol([])).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('skips unsupported offers and selects the first supported one', () => {
|
|
105
|
+
expect(selectSubprotocol(['foo', 'binary.ircv3.net'])).toBe('binary.ircv3.net');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns the first occurrence for duplicate supported offers', () => {
|
|
109
|
+
expect(selectSubprotocol(['text.ircv3.net', 'text.ircv3.net'])).toBe('text.ircv3.net');
|
|
110
|
+
});
|
|
111
|
+
});
|