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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { statsReducer } from '../../src/commands/stats';
|
|
3
|
+
import { Effect } from '../../src/effects';
|
|
4
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
+
import {
|
|
6
|
+
EmptyMotdProvider,
|
|
7
|
+
FakeClock,
|
|
8
|
+
SequentialIdFactory,
|
|
9
|
+
type ServerStatsSnapshot,
|
|
10
|
+
} from '../../src/ports';
|
|
11
|
+
import { createConnection } from '../../src/state/connection';
|
|
12
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
13
|
+
|
|
14
|
+
const serverConfig: ServerConfig = {
|
|
15
|
+
serverName: 'irc.example.com',
|
|
16
|
+
networkName: 'ExampleNet',
|
|
17
|
+
serverVersion: 'serverless-ircd-0.4.0',
|
|
18
|
+
maxChannelsPerUser: 30,
|
|
19
|
+
maxTargetsPerCommand: 10,
|
|
20
|
+
maxListEntries: 50,
|
|
21
|
+
nickLen: 30,
|
|
22
|
+
channelLen: 50,
|
|
23
|
+
topicLen: 390,
|
|
24
|
+
quitMessage: 'Client Quit',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeCtx(connId = 'c1', nick = 'alice', clock = new FakeClock(5_000)): Ctx {
|
|
28
|
+
const conn = createConnection({ id: connId, connectedSince: 0 });
|
|
29
|
+
conn.nick = nick;
|
|
30
|
+
conn.user = nick;
|
|
31
|
+
conn.host = 'example.com';
|
|
32
|
+
conn.realname = 'Alice';
|
|
33
|
+
conn.registration = 'registered';
|
|
34
|
+
return buildCtx({
|
|
35
|
+
serverConfig,
|
|
36
|
+
clock,
|
|
37
|
+
ids: new SequentialIdFactory(),
|
|
38
|
+
motd: EmptyMotdProvider,
|
|
39
|
+
connection: conn,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const L = (text: string): RawLine => ({ text });
|
|
44
|
+
|
|
45
|
+
const stats = (...params: string[]) =>
|
|
46
|
+
({
|
|
47
|
+
command: 'STATS',
|
|
48
|
+
params,
|
|
49
|
+
tags: {},
|
|
50
|
+
}) as const;
|
|
51
|
+
|
|
52
|
+
const snap = (uptimeStartedAt = 1_000): ServerStatsSnapshot => ({
|
|
53
|
+
users: 0,
|
|
54
|
+
invisible: 0,
|
|
55
|
+
opers: 0,
|
|
56
|
+
unknownConnections: 0,
|
|
57
|
+
channels: 0,
|
|
58
|
+
servers: 1,
|
|
59
|
+
localConns: 0,
|
|
60
|
+
globalConns: 0,
|
|
61
|
+
maxLocalConns: 0,
|
|
62
|
+
maxGlobalConns: 0,
|
|
63
|
+
uptimeStartedAt,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// statsReducer — STATS u (uptime)
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
describe('statsReducer — STATS u (uptime)', () => {
|
|
71
|
+
it('emits 242 RPL_STATSUPTIME with a formatted uptime line', () => {
|
|
72
|
+
const ctx = makeCtx();
|
|
73
|
+
// uptimeStartedAt=0, clock=5_000 → 5 seconds uptime.
|
|
74
|
+
const s = snap(0);
|
|
75
|
+
|
|
76
|
+
const out = statsReducer(s, stats('u'), ctx);
|
|
77
|
+
|
|
78
|
+
expect(out.effects).toContainEqual<EffectType>(
|
|
79
|
+
Effect.send('c1', [L(':irc.example.com 242 alice :Server Up 0 days, 0:00:05')]),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('formats days/hours/minutes/seconds from the uptime delta', () => {
|
|
84
|
+
const ctx = makeCtx(
|
|
85
|
+
'c1',
|
|
86
|
+
'alice',
|
|
87
|
+
new FakeClock(1 * 86_400_000 + 2 * 3_600_000 + 3 * 60_000 + 4_000),
|
|
88
|
+
);
|
|
89
|
+
const s = snap(0);
|
|
90
|
+
|
|
91
|
+
const out = statsReducer(s, stats('u'), ctx);
|
|
92
|
+
|
|
93
|
+
expect(out.effects).toContainEqual<EffectType>(
|
|
94
|
+
Effect.send('c1', [L(':irc.example.com 242 alice :Server Up 1 days, 2:03:04')]),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('terminates the STATS u reply with 219 RPL_ENDOFSTATS carrying the query letter', () => {
|
|
99
|
+
const ctx = makeCtx();
|
|
100
|
+
|
|
101
|
+
const out = statsReducer(snap(0), stats('u'), ctx);
|
|
102
|
+
|
|
103
|
+
expect(out.effects).toContainEqual<EffectType>(
|
|
104
|
+
Effect.send('c1', [L(':irc.example.com 219 alice u :End of /STATS report')]),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('updates the requester lastSeen to ctx.clock.now()', () => {
|
|
109
|
+
const ctx = makeCtx();
|
|
110
|
+
expect(ctx.connection.lastSeen).toBe(0);
|
|
111
|
+
|
|
112
|
+
statsReducer(snap(0), stats('u'), ctx);
|
|
113
|
+
|
|
114
|
+
expect(ctx.connection.lastSeen).toBe(5_000);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// statsReducer — STATS l (link info)
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
describe('statsReducer — STATS l (link info)', () => {
|
|
123
|
+
it('emits 211 RPL_STATSLINKINFO for the local server (single-hop, serverless)', () => {
|
|
124
|
+
const ctx = makeCtx();
|
|
125
|
+
const s = snap(0);
|
|
126
|
+
|
|
127
|
+
const out = statsReducer(s, stats('l'), ctx);
|
|
128
|
+
|
|
129
|
+
// Format: <sendq> <nick|server> <send-bytes> <send-msgs> <rc-bytes>
|
|
130
|
+
// <rc-msgs> <time-open> <time-since-last> <who> <desc>
|
|
131
|
+
// Single-server: one line for the bound server, hops=0.
|
|
132
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
133
|
+
Effect.send('c1', [
|
|
134
|
+
L(
|
|
135
|
+
`:irc.example.com 211 alice irc.example.com 0 0 0 0 0 0 :${ctx.serverConfig.serverVersion ?? ''}`,
|
|
136
|
+
),
|
|
137
|
+
]),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('terminates the STATS l reply with 219 RPL_ENDOFSTATS', () => {
|
|
142
|
+
const ctx = makeCtx();
|
|
143
|
+
|
|
144
|
+
const out = statsReducer(snap(0), stats('l'), ctx);
|
|
145
|
+
|
|
146
|
+
const last = out.effects[out.effects.length - 1];
|
|
147
|
+
expect(last).toEqual<EffectType>(
|
|
148
|
+
Effect.send('c1', [L(':irc.example.com 219 alice l :End of /STATS report')]),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// statsReducer — unknown query letter
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
describe('statsReducer — unknown query letter', () => {
|
|
158
|
+
it('emits only the 219 terminator (charybdis-style) for an unknown letter', () => {
|
|
159
|
+
const ctx = makeCtx();
|
|
160
|
+
|
|
161
|
+
const out = statsReducer(snap(0), stats('z'), ctx);
|
|
162
|
+
|
|
163
|
+
// Charybdis: unknown STATS letters produce no body lines, just 219.
|
|
164
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
165
|
+
Effect.send('c1', [L(':irc.example.com 219 alice z :End of /STATS report')]),
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('passes the unknown query letter through to the 219 terminator', () => {
|
|
170
|
+
const ctx = makeCtx();
|
|
171
|
+
|
|
172
|
+
const out = statsReducer(snap(0), stats('Q'), ctx);
|
|
173
|
+
|
|
174
|
+
// Letter is normalised to lowercase in the terminator.
|
|
175
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
176
|
+
Effect.send('c1', [L(':irc.example.com 219 alice q :End of /STATS report')]),
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// statsReducer — no query letter
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
describe('statsReducer — no query letter', () => {
|
|
186
|
+
it('emits only 219 with an empty query letter when STATS is invoked without args', () => {
|
|
187
|
+
const ctx = makeCtx();
|
|
188
|
+
|
|
189
|
+
const out = statsReducer(snap(0), stats(), ctx);
|
|
190
|
+
|
|
191
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
192
|
+
Effect.send('c1', [L(':irc.example.com 219 alice :End of /STATS report')]),
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// statsReducer — remote server target
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
describe('statsReducer — remote server target', () => {
|
|
202
|
+
it('emits 402 ERR_NOSUCHSERVER when the actor flags a non-local server target', () => {
|
|
203
|
+
const ctx = makeCtx();
|
|
204
|
+
|
|
205
|
+
const out = statsReducer(snap(0), stats('u', 'remote.example.org'), ctx, 'remote.example.org');
|
|
206
|
+
|
|
207
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
208
|
+
Effect.send('c1', [L(':irc.example.com 402 alice remote.example.org :No such server')]),
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('uses "*" as the nick in 402 when the requester has none', () => {
|
|
213
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
214
|
+
conn.registration = 'registered';
|
|
215
|
+
const ctx = buildCtx({
|
|
216
|
+
serverConfig,
|
|
217
|
+
clock: new FakeClock(5_000),
|
|
218
|
+
ids: new SequentialIdFactory(),
|
|
219
|
+
motd: EmptyMotdProvider,
|
|
220
|
+
connection: conn,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const out = statsReducer(snap(0), stats('u', 'remote.example.org'), ctx, 'remote.example.org');
|
|
224
|
+
|
|
225
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
226
|
+
Effect.send('c1', [L(':irc.example.com 402 * remote.example.org :No such server')]),
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// statsReducer — wire-shape details
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
describe('statsReducer — "*" nick fallback', () => {
|
|
236
|
+
it('uses "*" as the nick when the requester has none', () => {
|
|
237
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
238
|
+
conn.registration = 'registered';
|
|
239
|
+
const ctx = buildCtx({
|
|
240
|
+
serverConfig,
|
|
241
|
+
clock: new FakeClock(5_000),
|
|
242
|
+
ids: new SequentialIdFactory(),
|
|
243
|
+
motd: EmptyMotdProvider,
|
|
244
|
+
connection: conn,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const out = statsReducer(snap(0), stats('u'), ctx);
|
|
248
|
+
|
|
249
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
250
|
+
Effect.send('c1', [L(':irc.example.com 242 * :Server Up 0 days, 0:00:05')]),
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('uses "*" as the requester in STATS l when the requester has no nick', () => {
|
|
255
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
256
|
+
conn.registration = 'registered';
|
|
257
|
+
const ctx = buildCtx({
|
|
258
|
+
serverConfig,
|
|
259
|
+
clock: new FakeClock(5_000),
|
|
260
|
+
ids: new SequentialIdFactory(),
|
|
261
|
+
motd: EmptyMotdProvider,
|
|
262
|
+
connection: conn,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const out = statsReducer(snap(0), stats('l'), ctx);
|
|
266
|
+
|
|
267
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
268
|
+
Effect.send('c1', [
|
|
269
|
+
L(':irc.example.com 211 * irc.example.com 0 0 0 0 0 0 :serverless-ircd-0.4.0'),
|
|
270
|
+
]),
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('omits the trailing version when serverConfig.serverVersion is undefined', () => {
|
|
275
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
276
|
+
conn.registration = 'registered';
|
|
277
|
+
const { serverVersion: _omit, ...configNoVersion } = serverConfig;
|
|
278
|
+
void _omit;
|
|
279
|
+
const ctx = buildCtx({
|
|
280
|
+
serverConfig: configNoVersion,
|
|
281
|
+
clock: new FakeClock(5_000),
|
|
282
|
+
ids: new SequentialIdFactory(),
|
|
283
|
+
motd: EmptyMotdProvider,
|
|
284
|
+
connection: conn,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const out = statsReducer(snap(0), stats('l'), ctx);
|
|
288
|
+
|
|
289
|
+
// Trailing colon is empty when no serverVersion is configured.
|
|
290
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
291
|
+
Effect.send('c1', [L(':irc.example.com 211 * irc.example.com 0 0 0 0 0 0 :')]),
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -102,11 +102,7 @@ describe('tagmsgChannelReducer — success', () => {
|
|
|
102
102
|
const conn = makeConn();
|
|
103
103
|
const ctx = makeCtx(conn);
|
|
104
104
|
|
|
105
|
-
const out = tagmsgChannelReducer(
|
|
106
|
-
chan,
|
|
107
|
-
{ command: 'TAGMSG', params: ['#foo'], tags: {} },
|
|
108
|
-
ctx,
|
|
109
|
-
);
|
|
105
|
+
const out = tagmsgChannelReducer(chan, { command: 'TAGMSG', params: ['#foo'], tags: {} }, ctx);
|
|
110
106
|
|
|
111
107
|
expect(out.effects).toEqual<EffectType[]>([
|
|
112
108
|
Effect.broadcast('#foo', [L(':alice!alice@example.com TAGMSG #foo')], 'c1', 'message-tags'),
|
|
@@ -516,11 +512,7 @@ describe('tagmsgChannelReducer — defensive hostmask', () => {
|
|
|
516
512
|
conn.caps.add('message-tags');
|
|
517
513
|
const ctx = makeCtx(conn);
|
|
518
514
|
|
|
519
|
-
const out = tagmsgChannelReducer(
|
|
520
|
-
chan,
|
|
521
|
-
{ command: 'TAGMSG', params: ['#foo'], tags: {} },
|
|
522
|
-
ctx,
|
|
523
|
-
);
|
|
515
|
+
const out = tagmsgChannelReducer(chan, { command: 'TAGMSG', params: ['#foo'], tags: {} }, ctx);
|
|
524
516
|
|
|
525
517
|
const broadcast = out.effects[0];
|
|
526
518
|
expect(broadcast?.tag).toBe('Broadcast');
|
|
@@ -535,11 +527,7 @@ describe('tagmsgChannelReducer — defensive hostmask', () => {
|
|
|
535
527
|
conn.caps.add('message-tags');
|
|
536
528
|
const ctx = makeCtx(conn);
|
|
537
529
|
|
|
538
|
-
const out = tagmsgChannelReducer(
|
|
539
|
-
chan,
|
|
540
|
-
{ command: 'TAGMSG', params: ['#foo'], tags: {} },
|
|
541
|
-
ctx,
|
|
542
|
-
);
|
|
530
|
+
const out = tagmsgChannelReducer(chan, { command: 'TAGMSG', params: ['#foo'], tags: {} }, ctx);
|
|
543
531
|
|
|
544
532
|
const broadcast = out.effects[0];
|
|
545
533
|
expect(broadcast?.tag).toBe('Broadcast');
|
|
@@ -564,11 +552,7 @@ describe('tagmsgUserReducer', () => {
|
|
|
564
552
|
);
|
|
565
553
|
|
|
566
554
|
expect(out.effects).toEqual<EffectType[]>([
|
|
567
|
-
Effect.sendToNick(
|
|
568
|
-
'bob',
|
|
569
|
-
'c1',
|
|
570
|
-
[L('@+typing=active :alice!alice@example.com TAGMSG bob')],
|
|
571
|
-
),
|
|
555
|
+
Effect.sendToNick('bob', 'c1', [L('@+typing=active :alice!alice@example.com TAGMSG bob')]),
|
|
572
556
|
]);
|
|
573
557
|
});
|
|
574
558
|
|
|
@@ -583,11 +567,9 @@ describe('tagmsgUserReducer', () => {
|
|
|
583
567
|
);
|
|
584
568
|
|
|
585
569
|
expect(out.effects).toEqual<EffectType[]>([
|
|
586
|
-
Effect.sendToNick(
|
|
587
|
-
'nobody',
|
|
588
|
-
|
|
589
|
-
[L('@+typing=active :alice!alice@example.com TAGMSG nobody')],
|
|
590
|
-
),
|
|
570
|
+
Effect.sendToNick('nobody', 'c1', [
|
|
571
|
+
L('@+typing=active :alice!alice@example.com TAGMSG nobody'),
|
|
572
|
+
]),
|
|
591
573
|
]);
|
|
592
574
|
});
|
|
593
575
|
|
|
@@ -642,11 +624,7 @@ describe('tagmsgUserReducer', () => {
|
|
|
642
624
|
const conn = makeConn();
|
|
643
625
|
const ctx = makeCtx(conn);
|
|
644
626
|
|
|
645
|
-
const out = tagmsgUserReducer(
|
|
646
|
-
conn,
|
|
647
|
-
{ command: 'TAGMSG', params: ['bob'], tags: {} },
|
|
648
|
-
ctx,
|
|
649
|
-
);
|
|
627
|
+
const out = tagmsgUserReducer(conn, { command: 'TAGMSG', params: ['bob'], tags: {} }, ctx);
|
|
650
628
|
|
|
651
629
|
expect(out.effects).toEqual<EffectType[]>([
|
|
652
630
|
Effect.sendToNick('bob', 'c1', [L(':alice!alice@example.com TAGMSG bob')]),
|
|
@@ -667,11 +645,7 @@ describe('tagmsgUserReducer', () => {
|
|
|
667
645
|
const conn = makeConn();
|
|
668
646
|
const ctx = makeCtx(conn);
|
|
669
647
|
|
|
670
|
-
const out = tagmsgUserReducer(
|
|
671
|
-
conn,
|
|
672
|
-
{ command: 'TAGMSG', params: ['bob'], tags: {} },
|
|
673
|
-
ctx,
|
|
674
|
-
);
|
|
648
|
+
const out = tagmsgUserReducer(conn, { command: 'TAGMSG', params: ['bob'], tags: {} }, ctx);
|
|
675
649
|
|
|
676
650
|
expect(out.state).toBe(conn);
|
|
677
651
|
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { traceReducer } from '../../src/commands/trace';
|
|
3
|
+
import { Effect } from '../../src/effects';
|
|
4
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
+
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
6
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
7
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
8
|
+
|
|
9
|
+
const serverConfig: ServerConfig = {
|
|
10
|
+
serverName: 'irc.example.com',
|
|
11
|
+
networkName: 'ExampleNet',
|
|
12
|
+
maxChannelsPerUser: 30,
|
|
13
|
+
maxTargetsPerCommand: 10,
|
|
14
|
+
maxListEntries: 50,
|
|
15
|
+
nickLen: 30,
|
|
16
|
+
channelLen: 50,
|
|
17
|
+
topicLen: 390,
|
|
18
|
+
quitMessage: 'Client Quit',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function makeCtx(conn: ConnectionState, clock = new FakeClock(5_000)): Ctx {
|
|
22
|
+
return buildCtx({
|
|
23
|
+
serverConfig,
|
|
24
|
+
clock,
|
|
25
|
+
ids: new SequentialIdFactory(),
|
|
26
|
+
motd: EmptyMotdProvider,
|
|
27
|
+
connection: conn,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeConn(id = 'c1', nick = 'alice', oper = false): ConnectionState {
|
|
32
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
33
|
+
s.nick = nick;
|
|
34
|
+
s.user = nick;
|
|
35
|
+
s.host = 'example.com';
|
|
36
|
+
s.realname = 'Alice';
|
|
37
|
+
s.registration = 'registered';
|
|
38
|
+
s.userModes.oper = oper;
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeTarget(
|
|
43
|
+
id: string,
|
|
44
|
+
nick: string,
|
|
45
|
+
opts: { oper?: boolean; invisible?: boolean } = {},
|
|
46
|
+
): ConnectionState {
|
|
47
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
48
|
+
s.nick = nick;
|
|
49
|
+
s.user = nick;
|
|
50
|
+
s.host = 'example.com';
|
|
51
|
+
s.realname = 'Real Name';
|
|
52
|
+
s.registration = 'registered';
|
|
53
|
+
if (opts.oper !== undefined) s.userModes.oper = opts.oper;
|
|
54
|
+
if (opts.invisible !== undefined) s.userModes.invisible = opts.invisible;
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const L = (text: string): RawLine => ({ text });
|
|
59
|
+
|
|
60
|
+
const trace = (target?: string) =>
|
|
61
|
+
({
|
|
62
|
+
command: 'TRACE',
|
|
63
|
+
params: target === undefined ? [] : [target],
|
|
64
|
+
tags: {},
|
|
65
|
+
}) as const;
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// traceReducer — no target
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
describe('traceReducer — no target', () => {
|
|
72
|
+
it('emits the 262 terminator with no middle when there is no target', () => {
|
|
73
|
+
const requester = makeConn();
|
|
74
|
+
const ctx = makeCtx(requester);
|
|
75
|
+
|
|
76
|
+
const out = traceReducer(undefined, trace(), ctx);
|
|
77
|
+
|
|
78
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
79
|
+
Effect.send('c1', [L(':irc.example.com 262 alice :End of TRACE')]),
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('omits any 200/204/205 detail line when there is no target', () => {
|
|
84
|
+
const requester = makeConn();
|
|
85
|
+
const ctx = makeCtx(requester);
|
|
86
|
+
|
|
87
|
+
const out = traceReducer(undefined, trace(), ctx);
|
|
88
|
+
|
|
89
|
+
const text = out.effects
|
|
90
|
+
.map((e) => (e.tag === 'Send' ? e.lines.map((l) => l.text).join('|') : ''))
|
|
91
|
+
.join('|');
|
|
92
|
+
expect(text).not.toContain(' 200 ');
|
|
93
|
+
expect(text).not.toContain(' 204 ');
|
|
94
|
+
expect(text).not.toContain(' 205 ');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('updates the requester lastSeen to ctx.clock.now()', () => {
|
|
98
|
+
const requester = makeConn();
|
|
99
|
+
expect(requester.lastSeen).toBe(0);
|
|
100
|
+
const ctx = makeCtx(requester);
|
|
101
|
+
|
|
102
|
+
traceReducer(undefined, trace(), ctx);
|
|
103
|
+
|
|
104
|
+
expect(requester.lastSeen).toBe(5_000);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('uses "*" as the nick when the requester has none', () => {
|
|
108
|
+
const requester = createConnection({ id: 'c1', connectedSince: 0 });
|
|
109
|
+
const ctx = makeCtx(requester);
|
|
110
|
+
|
|
111
|
+
const out = traceReducer(undefined, trace(), ctx);
|
|
112
|
+
|
|
113
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
114
|
+
Effect.send('c1', [L(':irc.example.com 262 * :End of TRACE')]),
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// traceReducer — non-oper detail suppression
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
describe('traceReducer — non-oper suppression', () => {
|
|
124
|
+
it('emits only the 262 terminator for a non-oper TRACE <nick> (no leak)', () => {
|
|
125
|
+
const requester = makeConn('c1', 'alice', false);
|
|
126
|
+
const ctx = makeCtx(requester);
|
|
127
|
+
const target = makeTarget('c2', 'bob');
|
|
128
|
+
|
|
129
|
+
const out = traceReducer(target, trace('bob'), ctx);
|
|
130
|
+
|
|
131
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
132
|
+
Effect.send('c1', [L(':irc.example.com 262 alice bob :End of TRACE')]),
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not emit a 205 detail line for a non-oper requester', () => {
|
|
137
|
+
const requester = makeConn('c1', 'alice', false);
|
|
138
|
+
const ctx = makeCtx(requester);
|
|
139
|
+
const target = makeTarget('c2', 'bob');
|
|
140
|
+
|
|
141
|
+
const out = traceReducer(target, trace('bob'), ctx);
|
|
142
|
+
|
|
143
|
+
const text = out.effects
|
|
144
|
+
.map((e) => (e.tag === 'Send' ? e.lines.map((l) => l.text).join('|') : ''))
|
|
145
|
+
.join('|');
|
|
146
|
+
expect(text).not.toContain(' 205 ');
|
|
147
|
+
expect(text).not.toContain(' 204 ');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// traceReducer — oper detail
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
describe('traceReducer — oper detail', () => {
|
|
156
|
+
it('emits a 205 RPL_TRACEUSER line for an oper tracing a normal user', () => {
|
|
157
|
+
const requester = makeConn('c1', 'alice', true);
|
|
158
|
+
const ctx = makeCtx(requester);
|
|
159
|
+
const target = makeTarget('c2', 'bob');
|
|
160
|
+
|
|
161
|
+
const out = traceReducer(target, trace('bob'), ctx);
|
|
162
|
+
|
|
163
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
164
|
+
Effect.send('c1', [L(':irc.example.com 205 alice User 0 bob bob example.com')]),
|
|
165
|
+
Effect.send('c1', [L(':irc.example.com 262 alice bob :End of TRACE')]),
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('emits a 204 RPL_TRACEOPERATOR line for an oper tracing another oper', () => {
|
|
170
|
+
const requester = makeConn('c1', 'alice', true);
|
|
171
|
+
const ctx = makeCtx(requester);
|
|
172
|
+
const target = makeTarget('c2', 'bob', { oper: true });
|
|
173
|
+
|
|
174
|
+
const out = traceReducer(target, trace('bob'), ctx);
|
|
175
|
+
|
|
176
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
177
|
+
Effect.send('c1', [L(':irc.example.com 204 alice Oper 0 bob bob example.com')]),
|
|
178
|
+
Effect.send('c1', [L(':irc.example.com 262 alice bob :End of TRACE')]),
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('uses "*" nick fallbacks in 204/205 lines for an oper without a nick', () => {
|
|
183
|
+
const requester = createConnection({ id: 'c1', connectedSince: 0 });
|
|
184
|
+
requester.userModes.oper = true;
|
|
185
|
+
const ctx = makeCtx(requester);
|
|
186
|
+
const target = makeTarget('c2', 'bob');
|
|
187
|
+
|
|
188
|
+
const out = traceReducer(target, trace('bob'), ctx);
|
|
189
|
+
|
|
190
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
191
|
+
Effect.send('c1', [L(':irc.example.com 205 * User 0 bob bob example.com')]),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('uses "?" / "*" fallbacks in 205 line when target lacks user/host/nick', () => {
|
|
196
|
+
const requester = makeConn('c1', 'alice', true);
|
|
197
|
+
const ctx = makeCtx(requester);
|
|
198
|
+
const target = createConnection({ id: 'c2', connectedSince: 0 });
|
|
199
|
+
target.registration = 'registered';
|
|
200
|
+
|
|
201
|
+
const out = traceReducer(target, trace('ghost'), ctx);
|
|
202
|
+
|
|
203
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
204
|
+
Effect.send('c1', [L(':irc.example.com 205 alice User 0 * ? ?')]),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// traceReducer — +i invisible filtering
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
describe('traceReducer — +i invisible filtering', () => {
|
|
214
|
+
it('omits detail for an invisible target a non-oper does not share a channel with', () => {
|
|
215
|
+
const requester = makeConn('c1', 'alice', false);
|
|
216
|
+
const ctx = makeCtx(requester);
|
|
217
|
+
const target = makeTarget('c2', 'bob', { invisible: true });
|
|
218
|
+
|
|
219
|
+
const out = traceReducer(target, trace('bob'), ctx);
|
|
220
|
+
|
|
221
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
222
|
+
Effect.send('c1', [L(':irc.example.com 262 alice bob :End of TRACE')]),
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('shows a 205 line for an invisible target when the requester is an oper (oper bypasses +i)', () => {
|
|
227
|
+
const requester = makeConn('c1', 'alice', true);
|
|
228
|
+
const ctx = makeCtx(requester);
|
|
229
|
+
const target = makeTarget('c2', 'bob', { invisible: true });
|
|
230
|
+
|
|
231
|
+
const out = traceReducer(target, trace('bob'), ctx);
|
|
232
|
+
|
|
233
|
+
expect(out.effects[0]).toEqual<EffectType>(
|
|
234
|
+
Effect.send('c1', [L(':irc.example.com 205 alice User 0 bob bob example.com')]),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// traceReducer — remote server target
|
|
241
|
+
// ============================================================================
|
|
242
|
+
|
|
243
|
+
describe('traceReducer — remote server target', () => {
|
|
244
|
+
it('emits 402 ERR_NOSUCHSERVER when the actor flags a non-local server target', () => {
|
|
245
|
+
const requester = makeConn('c1', 'alice', true);
|
|
246
|
+
const ctx = makeCtx(requester);
|
|
247
|
+
|
|
248
|
+
const out = traceReducer(undefined, trace('remote.example.org'), ctx, 'remote.example.org');
|
|
249
|
+
|
|
250
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
251
|
+
Effect.send('c1', [L(':irc.example.com 402 alice remote.example.org :No such server')]),
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('uses "*" nick in 402 when the requester has none', () => {
|
|
256
|
+
const requester = createConnection({ id: 'c1', connectedSince: 0 });
|
|
257
|
+
const ctx = makeCtx(requester);
|
|
258
|
+
|
|
259
|
+
const out = traceReducer(undefined, trace('remote.example.org'), ctx, 'remote.example.org');
|
|
260
|
+
|
|
261
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
262
|
+
Effect.send('c1', [L(':irc.example.com 402 * remote.example.org :No such server')]),
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// traceReducer — unknown nick (actor could not resolve)
|
|
269
|
+
// ============================================================================
|
|
270
|
+
|
|
271
|
+
describe('traceReducer — unknown nick', () => {
|
|
272
|
+
it('emits 262 with the requested nick when target resolves to undefined', () => {
|
|
273
|
+
const requester = makeConn('c1', 'alice', true);
|
|
274
|
+
const ctx = makeCtx(requester);
|
|
275
|
+
|
|
276
|
+
const out = traceReducer(undefined, trace('ghost'), ctx);
|
|
277
|
+
|
|
278
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
279
|
+
Effect.send('c1', [L(':irc.example.com 262 alice ghost :End of TRACE')]),
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
});
|