serverless-ircd 0.2.0 → 0.4.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 +258 -18
- package/README.md +72 -30
- package/apps/aws-stack/README.md +2 -2
- 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 +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- 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/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 +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- 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/config-loader.test.ts +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- 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 +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +40 -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 +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- 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.test.ts +40 -0
- 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/worker/main.ts +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- 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 +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- 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/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- 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/tagmsg.ts +205 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +84 -3
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- 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/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- 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/tagmsg.test.ts +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -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/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- 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 +182 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.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 +78 -13
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- 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 +9 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { tagmsgChannelReducer, tagmsgUserReducer } from '../../src/commands/tagmsg';
|
|
3
|
+
import { Effect } from '../../src/effects';
|
|
4
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
+
import {
|
|
6
|
+
EmptyMotdProvider,
|
|
7
|
+
FakeClock,
|
|
8
|
+
InMemoryMessageStore,
|
|
9
|
+
type MessageStore,
|
|
10
|
+
SequentialIdFactory,
|
|
11
|
+
type StoredMessage,
|
|
12
|
+
} from '../../src/ports';
|
|
13
|
+
import { type ChannelState, createChannel } from '../../src/state/channel';
|
|
14
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
15
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
16
|
+
|
|
17
|
+
const serverConfig: ServerConfig = {
|
|
18
|
+
serverName: 'irc.example.com',
|
|
19
|
+
networkName: 'ExampleNet',
|
|
20
|
+
maxChannelsPerUser: 30,
|
|
21
|
+
maxTargetsPerCommand: 10,
|
|
22
|
+
maxListEntries: 50,
|
|
23
|
+
nickLen: 30,
|
|
24
|
+
channelLen: 50,
|
|
25
|
+
topicLen: 390,
|
|
26
|
+
quitMessage: 'Client Quit',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function makeCtx(
|
|
30
|
+
conn: ConnectionState,
|
|
31
|
+
clock = new FakeClock(1_000),
|
|
32
|
+
messages?: MessageStore,
|
|
33
|
+
): Ctx {
|
|
34
|
+
return buildCtx({
|
|
35
|
+
serverConfig,
|
|
36
|
+
clock,
|
|
37
|
+
ids: new SequentialIdFactory(),
|
|
38
|
+
motd: EmptyMotdProvider,
|
|
39
|
+
connection: conn,
|
|
40
|
+
...(messages !== undefined ? { messages } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeConn(id = 'c1', nick = 'alice'): ConnectionState {
|
|
45
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
46
|
+
s.nick = nick;
|
|
47
|
+
s.user = nick;
|
|
48
|
+
s.host = 'example.com';
|
|
49
|
+
s.realname = nick.charAt(0).toUpperCase() + nick.slice(1);
|
|
50
|
+
s.registration = 'registered';
|
|
51
|
+
s.caps.add('message-tags');
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeChan(name = '#foo'): ChannelState {
|
|
56
|
+
return createChannel({ name, nameLower: name.toLowerCase(), createdAt: 0 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function addMember(
|
|
60
|
+
chan: ChannelState,
|
|
61
|
+
connId: string,
|
|
62
|
+
nick: string,
|
|
63
|
+
op = false,
|
|
64
|
+
voice = false,
|
|
65
|
+
): void {
|
|
66
|
+
chan.members.set(connId, { conn: connId, nick, op, voice });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const L = (text: string): RawLine => ({ text });
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// tagmsgChannelReducer — success path
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
describe('tagmsgChannelReducer — success', () => {
|
|
76
|
+
it('broadcasts TAGMSG to channel members (excluding sender) with the tag prefix', () => {
|
|
77
|
+
const chan = makeChan('#foo');
|
|
78
|
+
addMember(chan, 'c1', 'alice');
|
|
79
|
+
addMember(chan, 'c2', 'bob');
|
|
80
|
+
const conn = makeConn();
|
|
81
|
+
const ctx = makeCtx(conn);
|
|
82
|
+
|
|
83
|
+
const out = tagmsgChannelReducer(
|
|
84
|
+
chan,
|
|
85
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
86
|
+
ctx,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
90
|
+
Effect.broadcast(
|
|
91
|
+
'#foo',
|
|
92
|
+
[L('@+typing=active :alice!alice@example.com TAGMSG #foo')],
|
|
93
|
+
'c1',
|
|
94
|
+
'message-tags',
|
|
95
|
+
),
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('broadcasts without an @ prefix when no tags are present', () => {
|
|
100
|
+
const chan = makeChan('#foo');
|
|
101
|
+
addMember(chan, 'c1', 'alice');
|
|
102
|
+
const conn = makeConn();
|
|
103
|
+
const ctx = makeCtx(conn);
|
|
104
|
+
|
|
105
|
+
const out = tagmsgChannelReducer(chan, { command: 'TAGMSG', params: ['#foo'], tags: {} }, ctx);
|
|
106
|
+
|
|
107
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
108
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com TAGMSG #foo')], 'c1', 'message-tags'),
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('serializes multiple tags correctly', () => {
|
|
113
|
+
const chan = makeChan('#foo');
|
|
114
|
+
addMember(chan, 'c1', 'alice');
|
|
115
|
+
const conn = makeConn();
|
|
116
|
+
const ctx = makeCtx(conn);
|
|
117
|
+
|
|
118
|
+
const out = tagmsgChannelReducer(
|
|
119
|
+
chan,
|
|
120
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active', 'vendor/foo': 'bar' } },
|
|
121
|
+
ctx,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
125
|
+
Effect.broadcast(
|
|
126
|
+
'#foo',
|
|
127
|
+
[L('@+typing=active;vendor/foo=bar :alice!alice@example.com TAGMSG #foo')],
|
|
128
|
+
'c1',
|
|
129
|
+
'message-tags',
|
|
130
|
+
),
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('serializes a valueless tag as a bare key (no =suffix)', () => {
|
|
135
|
+
const chan = makeChan('#foo');
|
|
136
|
+
addMember(chan, 'c1', 'alice');
|
|
137
|
+
const conn = makeConn();
|
|
138
|
+
const ctx = makeCtx(conn);
|
|
139
|
+
|
|
140
|
+
const out = tagmsgChannelReducer(
|
|
141
|
+
chan,
|
|
142
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+ack': '' } },
|
|
143
|
+
ctx,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
147
|
+
Effect.broadcast(
|
|
148
|
+
'#foo',
|
|
149
|
+
[L('@+ack :alice!alice@example.com TAGMSG #foo')],
|
|
150
|
+
'c1',
|
|
151
|
+
'message-tags',
|
|
152
|
+
),
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('updates the connection lastSeen to ctx.clock.now()', () => {
|
|
157
|
+
const chan = makeChan('#foo');
|
|
158
|
+
addMember(chan, 'c1', 'alice');
|
|
159
|
+
const conn = makeConn();
|
|
160
|
+
const clock = new FakeClock(8_800);
|
|
161
|
+
const ctx = makeCtx(conn, clock);
|
|
162
|
+
|
|
163
|
+
tagmsgChannelReducer(chan, { command: 'TAGMSG', params: ['#foo'], tags: {} }, ctx);
|
|
164
|
+
|
|
165
|
+
expect(conn.lastSeen).toBe(8_800);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns the same state reference (no mutation to channel state)', () => {
|
|
169
|
+
const chan = makeChan('#foo');
|
|
170
|
+
addMember(chan, 'c1', 'alice');
|
|
171
|
+
const conn = makeConn();
|
|
172
|
+
const ctx = makeCtx(conn);
|
|
173
|
+
|
|
174
|
+
const out = tagmsgChannelReducer(
|
|
175
|
+
chan,
|
|
176
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
177
|
+
ctx,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(out.state).toBe(chan);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// tagmsgChannelReducer — cap gate
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
describe('tagmsgChannelReducer — cap gate', () => {
|
|
189
|
+
it('emits 421 ERR_UNKNOWNCOMMAND when message-tags cap is not negotiated', () => {
|
|
190
|
+
const chan = makeChan('#foo');
|
|
191
|
+
addMember(chan, 'c1', 'alice');
|
|
192
|
+
const conn = makeConn();
|
|
193
|
+
conn.caps.delete('message-tags');
|
|
194
|
+
const ctx = makeCtx(conn);
|
|
195
|
+
|
|
196
|
+
const out = tagmsgChannelReducer(
|
|
197
|
+
chan,
|
|
198
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
199
|
+
ctx,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
203
|
+
Effect.send('c1', [L(':irc.example.com 421 alice TAGMSG :Unknown command')]),
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('uses * as the nick in 421 when the connection has no nick', () => {
|
|
208
|
+
const chan = makeChan('#foo');
|
|
209
|
+
addMember(chan, 'c2', 'bob');
|
|
210
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
211
|
+
const ctx = makeCtx(conn);
|
|
212
|
+
|
|
213
|
+
const out = tagmsgChannelReducer(
|
|
214
|
+
chan,
|
|
215
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
216
|
+
ctx,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
220
|
+
Effect.send('c1', [L(':irc.example.com 421 * TAGMSG :Unknown command')]),
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// tagmsgChannelReducer — NOTICE-style silent rejections
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
describe('tagmsgChannelReducer — silent rejections', () => {
|
|
230
|
+
it('MUST NOT produce any numeric reply when target is missing', () => {
|
|
231
|
+
const chan = makeChan('#foo');
|
|
232
|
+
addMember(chan, 'c1', 'alice');
|
|
233
|
+
const conn = makeConn();
|
|
234
|
+
const ctx = makeCtx(conn);
|
|
235
|
+
|
|
236
|
+
const out = tagmsgChannelReducer(
|
|
237
|
+
chan,
|
|
238
|
+
{ command: 'TAGMSG', params: [], tags: { '+typing': 'active' } },
|
|
239
|
+
ctx,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(out.effects).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('MUST NOT produce any numeric reply when rejected by +n (no external)', () => {
|
|
246
|
+
const chan = makeChan('#foo');
|
|
247
|
+
chan.modes.noExternal = true;
|
|
248
|
+
addMember(chan, 'c2', 'bob');
|
|
249
|
+
const conn = makeConn();
|
|
250
|
+
const ctx = makeCtx(conn);
|
|
251
|
+
|
|
252
|
+
const out = tagmsgChannelReducer(
|
|
253
|
+
chan,
|
|
254
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
255
|
+
ctx,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
expect(out.effects).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('MUST NOT produce any numeric reply when rejected by +m (moderated)', () => {
|
|
262
|
+
const chan = makeChan('#foo');
|
|
263
|
+
chan.modes.moderated = true;
|
|
264
|
+
addMember(chan, 'c1', 'alice', false, false);
|
|
265
|
+
const conn = makeConn();
|
|
266
|
+
const ctx = makeCtx(conn);
|
|
267
|
+
|
|
268
|
+
const out = tagmsgChannelReducer(
|
|
269
|
+
chan,
|
|
270
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
271
|
+
ctx,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(out.effects).toEqual([]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('MUST NOT produce any numeric reply when +m rejects a non-member (no +n)', () => {
|
|
278
|
+
const chan = makeChan('#foo');
|
|
279
|
+
chan.modes.moderated = true;
|
|
280
|
+
addMember(chan, 'c2', 'bob', true, false);
|
|
281
|
+
const conn = makeConn();
|
|
282
|
+
const ctx = makeCtx(conn);
|
|
283
|
+
|
|
284
|
+
const out = tagmsgChannelReducer(
|
|
285
|
+
chan,
|
|
286
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
287
|
+
ctx,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(out.effects).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('still broadcasts when the sender is an op of a +m channel', () => {
|
|
294
|
+
const chan = makeChan('#foo');
|
|
295
|
+
chan.modes.moderated = true;
|
|
296
|
+
addMember(chan, 'c1', 'alice', true, false);
|
|
297
|
+
const conn = makeConn();
|
|
298
|
+
const ctx = makeCtx(conn);
|
|
299
|
+
|
|
300
|
+
const out = tagmsgChannelReducer(
|
|
301
|
+
chan,
|
|
302
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
303
|
+
ctx,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
expect(out.effects).toHaveLength(1);
|
|
307
|
+
expect(out.effects[0]?.tag).toBe('Broadcast');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('still broadcasts when the sender is voiced on a +m channel', () => {
|
|
311
|
+
const chan = makeChan('#foo');
|
|
312
|
+
chan.modes.moderated = true;
|
|
313
|
+
addMember(chan, 'c1', 'alice', false, true);
|
|
314
|
+
const conn = makeConn();
|
|
315
|
+
const ctx = makeCtx(conn);
|
|
316
|
+
|
|
317
|
+
const out = tagmsgChannelReducer(
|
|
318
|
+
chan,
|
|
319
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
320
|
+
ctx,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(out.effects).toHaveLength(1);
|
|
324
|
+
expect(out.effects[0]?.tag).toBe('Broadcast');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('MUST NOT produce any numeric reply when banned (+b)', () => {
|
|
328
|
+
const chan = makeChan('#foo');
|
|
329
|
+
chan.banMasks.add('*!*@example.com');
|
|
330
|
+
addMember(chan, 'c1', 'alice');
|
|
331
|
+
const conn = makeConn();
|
|
332
|
+
const ctx = makeCtx(conn);
|
|
333
|
+
|
|
334
|
+
const out = tagmsgChannelReducer(
|
|
335
|
+
chan,
|
|
336
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
337
|
+
ctx,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(out.effects).toEqual([]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('honors the ? wildcard in ban masks', () => {
|
|
344
|
+
const chan = makeChan('#foo');
|
|
345
|
+
chan.banMasks.add('alice!?????@example.com');
|
|
346
|
+
addMember(chan, 'c1', 'alice');
|
|
347
|
+
const conn = makeConn();
|
|
348
|
+
const ctx = makeCtx(conn);
|
|
349
|
+
|
|
350
|
+
const out = tagmsgChannelReducer(
|
|
351
|
+
chan,
|
|
352
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
353
|
+
ctx,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(out.effects).toEqual([]);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('proceeds with the broadcast when the ban list is non-empty but no mask matches', () => {
|
|
360
|
+
const chan = makeChan('#foo');
|
|
361
|
+
chan.banMasks.add('*!*@baddomain.example');
|
|
362
|
+
chan.banMasks.add('evil!*@*');
|
|
363
|
+
addMember(chan, 'c1', 'alice');
|
|
364
|
+
const conn = makeConn();
|
|
365
|
+
const ctx = makeCtx(conn);
|
|
366
|
+
|
|
367
|
+
const out = tagmsgChannelReducer(
|
|
368
|
+
chan,
|
|
369
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
370
|
+
ctx,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(out.effects).toHaveLength(1);
|
|
374
|
+
expect(out.effects[0]?.tag).toBe('Broadcast');
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// tagmsgChannelReducer — echo-message
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
describe('tagmsgChannelReducer — echo-message', () => {
|
|
383
|
+
it('echoes the TAGMSG back to the sender when echo-message cap is present', () => {
|
|
384
|
+
const chan = makeChan('#foo');
|
|
385
|
+
addMember(chan, 'c1', 'alice');
|
|
386
|
+
const conn = makeConn();
|
|
387
|
+
conn.caps.add('echo-message');
|
|
388
|
+
const ctx = makeCtx(conn);
|
|
389
|
+
|
|
390
|
+
const out = tagmsgChannelReducer(
|
|
391
|
+
chan,
|
|
392
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
393
|
+
ctx,
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const line = L('@+typing=active :alice!alice@example.com TAGMSG #foo');
|
|
397
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
398
|
+
Effect.broadcast('#foo', [line], 'c1', 'message-tags'),
|
|
399
|
+
Effect.send('c1', [line]),
|
|
400
|
+
]);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('does NOT echo when echo-message cap is absent', () => {
|
|
404
|
+
const chan = makeChan('#foo');
|
|
405
|
+
addMember(chan, 'c1', 'alice');
|
|
406
|
+
const conn = makeConn();
|
|
407
|
+
const ctx = makeCtx(conn);
|
|
408
|
+
|
|
409
|
+
const out = tagmsgChannelReducer(
|
|
410
|
+
chan,
|
|
411
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
412
|
+
ctx,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
expect(out.effects).toHaveLength(1);
|
|
416
|
+
expect(out.effects[0]?.tag).toBe('Broadcast');
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// tagmsgChannelReducer — chathistory recording
|
|
422
|
+
// ============================================================================
|
|
423
|
+
|
|
424
|
+
describe('tagmsgChannelReducer — chathistory recording', () => {
|
|
425
|
+
it('records the TAGMSG into ctx.messages with a fresh msgid and time', () => {
|
|
426
|
+
const chan = makeChan('#foo');
|
|
427
|
+
addMember(chan, 'c1', 'alice');
|
|
428
|
+
const store = new InMemoryMessageStore();
|
|
429
|
+
const conn = makeConn();
|
|
430
|
+
const clock = new FakeClock(7_700);
|
|
431
|
+
const ctx = makeCtx(conn, clock, store);
|
|
432
|
+
|
|
433
|
+
tagmsgChannelReducer(
|
|
434
|
+
chan,
|
|
435
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
436
|
+
ctx,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const recorded = store.query({ chan: '#foo', direction: 'latest', limit: 10 });
|
|
440
|
+
expect(recorded).toHaveLength(1);
|
|
441
|
+
const entry = recorded[0] as StoredMessage | undefined;
|
|
442
|
+
expect(entry).toBeDefined();
|
|
443
|
+
expect(entry?.msgid).toBe('nonce-0');
|
|
444
|
+
expect(entry?.time).toBe(7_700);
|
|
445
|
+
expect(entry?.chan).toBe('#foo');
|
|
446
|
+
expect(entry?.command).toBe('TAGMSG');
|
|
447
|
+
expect(entry?.nick).toBe('alice');
|
|
448
|
+
expect(entry?.user).toBe('alice');
|
|
449
|
+
expect(entry?.host).toBe('example.com');
|
|
450
|
+
expect(entry?.text).toBe('');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('does NOT crash when no MessageStore is bound', () => {
|
|
454
|
+
const chan = makeChan('#foo');
|
|
455
|
+
addMember(chan, 'c1', 'alice');
|
|
456
|
+
const conn = makeConn();
|
|
457
|
+
const ctx = makeCtx(conn);
|
|
458
|
+
|
|
459
|
+
const out = tagmsgChannelReducer(
|
|
460
|
+
chan,
|
|
461
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
462
|
+
ctx,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(out.effects[0]?.tag).toBe('Broadcast');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('does NOT record when rejected by +n', () => {
|
|
469
|
+
const chan = makeChan('#foo');
|
|
470
|
+
chan.modes.noExternal = true;
|
|
471
|
+
const store = new InMemoryMessageStore();
|
|
472
|
+
const ctx = makeCtx(makeConn(), new FakeClock(1_000), store);
|
|
473
|
+
|
|
474
|
+
tagmsgChannelReducer(
|
|
475
|
+
chan,
|
|
476
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
477
|
+
ctx,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
expect(store.query({ chan: '#foo', direction: 'latest', limit: 10 })).toEqual([]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('does NOT record when rejected by +b', () => {
|
|
484
|
+
const chan = makeChan('#foo');
|
|
485
|
+
chan.banMasks.add('*!*@example.com');
|
|
486
|
+
addMember(chan, 'c1', 'alice');
|
|
487
|
+
const store = new InMemoryMessageStore();
|
|
488
|
+
const ctx = makeCtx(makeConn(), new FakeClock(1_000), store);
|
|
489
|
+
|
|
490
|
+
tagmsgChannelReducer(
|
|
491
|
+
chan,
|
|
492
|
+
{ command: 'TAGMSG', params: ['#foo'], tags: { '+typing': 'active' } },
|
|
493
|
+
ctx,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(store.query({ chan: '#foo', direction: 'latest', limit: 10 })).toEqual([]);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// tagmsgChannelReducer — defensive hostmask fallbacks
|
|
502
|
+
// ============================================================================
|
|
503
|
+
|
|
504
|
+
describe('tagmsgChannelReducer — defensive hostmask', () => {
|
|
505
|
+
it('falls back to the bare nick when user/host are absent', () => {
|
|
506
|
+
const chan = makeChan('#foo');
|
|
507
|
+
addMember(chan, 'c2', 'bob');
|
|
508
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
509
|
+
conn.nick = 'alice';
|
|
510
|
+
conn.realname = 'Alice';
|
|
511
|
+
conn.registration = 'registered';
|
|
512
|
+
conn.caps.add('message-tags');
|
|
513
|
+
const ctx = makeCtx(conn);
|
|
514
|
+
|
|
515
|
+
const out = tagmsgChannelReducer(chan, { command: 'TAGMSG', params: ['#foo'], tags: {} }, ctx);
|
|
516
|
+
|
|
517
|
+
const broadcast = out.effects[0];
|
|
518
|
+
expect(broadcast?.tag).toBe('Broadcast');
|
|
519
|
+
const lines = (broadcast as { lines: RawLine[] } | undefined)?.lines;
|
|
520
|
+
expect(lines?.[0]?.text).toBe(':alice TAGMSG #foo');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('falls back to ? when the connection has no nick at all', () => {
|
|
524
|
+
const chan = makeChan('#foo');
|
|
525
|
+
addMember(chan, 'c2', 'bob');
|
|
526
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
527
|
+
conn.caps.add('message-tags');
|
|
528
|
+
const ctx = makeCtx(conn);
|
|
529
|
+
|
|
530
|
+
const out = tagmsgChannelReducer(chan, { command: 'TAGMSG', params: ['#foo'], tags: {} }, ctx);
|
|
531
|
+
|
|
532
|
+
const broadcast = out.effects[0];
|
|
533
|
+
expect(broadcast?.tag).toBe('Broadcast');
|
|
534
|
+
const lines = (broadcast as { lines: RawLine[] } | undefined)?.lines;
|
|
535
|
+
expect(lines?.[0]?.text).toBe(':? TAGMSG #foo');
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// ============================================================================
|
|
540
|
+
// tagmsgUserReducer
|
|
541
|
+
// ============================================================================
|
|
542
|
+
|
|
543
|
+
describe('tagmsgUserReducer', () => {
|
|
544
|
+
it('routes a private TAGMSG via SendToNick', () => {
|
|
545
|
+
const conn = makeConn();
|
|
546
|
+
const ctx = makeCtx(conn);
|
|
547
|
+
|
|
548
|
+
const out = tagmsgUserReducer(
|
|
549
|
+
conn,
|
|
550
|
+
{ command: 'TAGMSG', params: ['bob'], tags: { '+typing': 'active' } },
|
|
551
|
+
ctx,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
555
|
+
Effect.sendToNick('bob', 'c1', [L('@+typing=active :alice!alice@example.com TAGMSG bob')]),
|
|
556
|
+
]);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('has no notFoundLines for an offline target (silent like NOTICE)', () => {
|
|
560
|
+
const conn = makeConn();
|
|
561
|
+
const ctx = makeCtx(conn);
|
|
562
|
+
|
|
563
|
+
const out = tagmsgUserReducer(
|
|
564
|
+
conn,
|
|
565
|
+
{ command: 'TAGMSG', params: ['nobody'], tags: { '+typing': 'active' } },
|
|
566
|
+
ctx,
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
570
|
+
Effect.sendToNick('nobody', 'c1', [
|
|
571
|
+
L('@+typing=active :alice!alice@example.com TAGMSG nobody'),
|
|
572
|
+
]),
|
|
573
|
+
]);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('emits 421 ERR_UNKNOWNCOMMAND when message-tags cap is not negotiated', () => {
|
|
577
|
+
const conn = makeConn();
|
|
578
|
+
conn.caps.delete('message-tags');
|
|
579
|
+
const ctx = makeCtx(conn);
|
|
580
|
+
|
|
581
|
+
const out = tagmsgUserReducer(
|
|
582
|
+
conn,
|
|
583
|
+
{ command: 'TAGMSG', params: ['bob'], tags: { '+typing': 'active' } },
|
|
584
|
+
ctx,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
588
|
+
Effect.send('c1', [L(':irc.example.com 421 alice TAGMSG :Unknown command')]),
|
|
589
|
+
]);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('MUST NOT produce any numeric reply when target is missing', () => {
|
|
593
|
+
const conn = makeConn();
|
|
594
|
+
const ctx = makeCtx(conn);
|
|
595
|
+
|
|
596
|
+
const out = tagmsgUserReducer(
|
|
597
|
+
conn,
|
|
598
|
+
{ command: 'TAGMSG', params: [], tags: { '+typing': 'active' } },
|
|
599
|
+
ctx,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
expect(out.effects).toEqual([]);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('echoes back to the sender when echo-message cap is present', () => {
|
|
606
|
+
const conn = makeConn();
|
|
607
|
+
conn.caps.add('echo-message');
|
|
608
|
+
const ctx = makeCtx(conn);
|
|
609
|
+
|
|
610
|
+
const out = tagmsgUserReducer(
|
|
611
|
+
conn,
|
|
612
|
+
{ command: 'TAGMSG', params: ['bob'], tags: { '+typing': 'active' } },
|
|
613
|
+
ctx,
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const line = L('@+typing=active :alice!alice@example.com TAGMSG bob');
|
|
617
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
618
|
+
Effect.sendToNick('bob', 'c1', [line]),
|
|
619
|
+
Effect.send('c1', [line]),
|
|
620
|
+
]);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('works without tags (no @ prefix)', () => {
|
|
624
|
+
const conn = makeConn();
|
|
625
|
+
const ctx = makeCtx(conn);
|
|
626
|
+
|
|
627
|
+
const out = tagmsgUserReducer(conn, { command: 'TAGMSG', params: ['bob'], tags: {} }, ctx);
|
|
628
|
+
|
|
629
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
630
|
+
Effect.sendToNick('bob', 'c1', [L(':alice!alice@example.com TAGMSG bob')]),
|
|
631
|
+
]);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('updates lastSeen to ctx.clock.now()', () => {
|
|
635
|
+
const conn = makeConn();
|
|
636
|
+
const clock = new FakeClock(12_000);
|
|
637
|
+
const ctx = makeCtx(conn, clock);
|
|
638
|
+
|
|
639
|
+
tagmsgUserReducer(conn, { command: 'TAGMSG', params: ['bob'], tags: {} }, ctx);
|
|
640
|
+
|
|
641
|
+
expect(conn.lastSeen).toBe(12_000);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('returns the same state reference', () => {
|
|
645
|
+
const conn = makeConn();
|
|
646
|
+
const ctx = makeCtx(conn);
|
|
647
|
+
|
|
648
|
+
const out = tagmsgUserReducer(conn, { command: 'TAGMSG', params: ['bob'], tags: {} }, ctx);
|
|
649
|
+
|
|
650
|
+
expect(out.state).toBe(conn);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('does NOT record a user-target TAGMSG', () => {
|
|
654
|
+
const store = new InMemoryMessageStore();
|
|
655
|
+
const conn = makeConn();
|
|
656
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), store);
|
|
657
|
+
|
|
658
|
+
tagmsgUserReducer(conn, { command: 'TAGMSG', params: ['bob'], tags: {} }, ctx);
|
|
659
|
+
|
|
660
|
+
expect(store.targets(0, 100_000)).toEqual([]);
|
|
661
|
+
});
|
|
662
|
+
});
|