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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the NLB TCP+TLS Lambda streaming handler.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the NLB→Lambda byte framing end-to-end against real DynamoDB
|
|
5
|
+
* Local: connection identity derivation, transport buffer persistence
|
|
6
|
+
* across invocations, chunk-boundary reassembly, and the streaming-back
|
|
7
|
+
* response path. The `LocalPostToConnection` double routes cross-connection
|
|
8
|
+
* fanout in-process (no actual NLB or APIGW needed).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_SERVER_CONFIG,
|
|
14
|
+
type ParsedServerConfig,
|
|
15
|
+
StaticMotdProvider,
|
|
16
|
+
} from '@serverless-ircd/irc-core';
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
18
|
+
import { AwsRuntime } from '../src/aws-runtime.js';
|
|
19
|
+
import type { PostToConnection } from '../src/aws-runtime.js';
|
|
20
|
+
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
21
|
+
import { createDynamoDocumentClient } from '../src/dynamo.js';
|
|
22
|
+
import {
|
|
23
|
+
type NlbStreamEvent,
|
|
24
|
+
deriveNlbConnectionId,
|
|
25
|
+
handleNlbStream,
|
|
26
|
+
} from '../src/handlers/nlb-stream.js';
|
|
27
|
+
import { bindMessageStore } from '../src/message-store.js';
|
|
28
|
+
import type { TablesConfig } from '../src/tables.js';
|
|
29
|
+
|
|
30
|
+
const available = process.env.DDB_AVAILABLE === '1';
|
|
31
|
+
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
32
|
+
|
|
33
|
+
const SERVER_CONFIG: ParsedServerConfig = {
|
|
34
|
+
...DEFAULT_SERVER_CONFIG,
|
|
35
|
+
serverName: 'irc.example.com',
|
|
36
|
+
networkName: 'ExampleNet',
|
|
37
|
+
motdLines: ['Welcome to the test IRC server.'],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const MOTD = new StaticMotdProvider(SERVER_CONFIG.motdLines);
|
|
41
|
+
|
|
42
|
+
interface Fixture {
|
|
43
|
+
tables: TablesConfig;
|
|
44
|
+
client: ReturnType<typeof createDynamoDocumentClient>;
|
|
45
|
+
mgmt: LocalPostToConnection;
|
|
46
|
+
cleanup: () => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let fx: Fixture | null;
|
|
50
|
+
|
|
51
|
+
function requireFx(): Fixture {
|
|
52
|
+
if (!fx) throw new Error('test fixture not initialized');
|
|
53
|
+
return fx;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function makeFixture(): Promise<Fixture> {
|
|
57
|
+
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
58
|
+
const prefix = `n${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
59
|
+
const tables = Object.fromEntries(
|
|
60
|
+
Object.keys(TABLE_DEFS).map((name) => [name, `${prefix}${name}`]),
|
|
61
|
+
) as TablesConfig;
|
|
62
|
+
const client = createDynamoDocumentClient({ endpoint });
|
|
63
|
+
for (const [logical, props] of Object.entries(TABLE_DEFS)) {
|
|
64
|
+
const pkName = props.partitionKey?.name;
|
|
65
|
+
const skName = props.sortKey?.name;
|
|
66
|
+
if (pkName === undefined) throw new Error(`table ${logical} missing partitionKey`);
|
|
67
|
+
const attributeDefinitions: Array<{ AttributeName: string; AttributeType: 'S' }> = [
|
|
68
|
+
{ AttributeName: pkName, AttributeType: 'S' },
|
|
69
|
+
];
|
|
70
|
+
const keySchema: Array<{ AttributeName: string; KeyType: 'HASH' | 'RANGE' }> = [
|
|
71
|
+
{ AttributeName: pkName, KeyType: 'HASH' },
|
|
72
|
+
];
|
|
73
|
+
if (skName !== undefined) {
|
|
74
|
+
attributeDefinitions.push({ AttributeName: skName, AttributeType: 'S' });
|
|
75
|
+
keySchema.push({ AttributeName: skName, KeyType: 'RANGE' });
|
|
76
|
+
}
|
|
77
|
+
await client.send(
|
|
78
|
+
new CreateTableCommand({
|
|
79
|
+
TableName: `${prefix}${logical}`,
|
|
80
|
+
AttributeDefinitions: attributeDefinitions,
|
|
81
|
+
KeySchema: keySchema,
|
|
82
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const mgmt = new LocalPostToConnection();
|
|
87
|
+
return {
|
|
88
|
+
tables,
|
|
89
|
+
client,
|
|
90
|
+
mgmt,
|
|
91
|
+
cleanup: async () => {
|
|
92
|
+
for (const logical of Object.keys(TABLE_DEFS)) {
|
|
93
|
+
try {
|
|
94
|
+
await client.send(new DeleteTableCommand({ TableName: `${prefix}${logical}` }));
|
|
95
|
+
} catch {
|
|
96
|
+
// Already gone.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Builds an NLB event from raw client bytes (base64-encodes the body). */
|
|
104
|
+
function nlbEvent(sourceIp: string, sourcePort: string, data: string): NlbStreamEvent {
|
|
105
|
+
return {
|
|
106
|
+
requestContext: { elb: { targetGroupArn: 'arn:aws:elasticloadbalancing:::targetgroup/test' } },
|
|
107
|
+
version: '2.0',
|
|
108
|
+
body: Buffer.from(data, 'utf8').toString('base64'),
|
|
109
|
+
isBase64Encoded: true,
|
|
110
|
+
headers: {
|
|
111
|
+
'x-forwarded-for': sourceIp,
|
|
112
|
+
'x-forwarded-port': sourcePort,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe.skipIf(!available)('deriveNlbConnectionId', () => {
|
|
118
|
+
it('derives a stable id from source IP + port', () => {
|
|
119
|
+
const id = deriveNlbConnectionId('10.0.0.1', '54321');
|
|
120
|
+
expect(id).toBe('nlb-10-0-0-1-54321');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('produces different ids for different ports on the same IP', () => {
|
|
124
|
+
expect(deriveNlbConnectionId('10.0.0.1', '54321')).not.toBe(
|
|
125
|
+
deriveNlbConnectionId('10.0.0.1', '54322'),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('handles IPv6 addresses (colons replaced with dashes)', () => {
|
|
130
|
+
const id = deriveNlbConnectionId('::1', '1234');
|
|
131
|
+
// Two colons → two dashes, then the digit.
|
|
132
|
+
expect(id).toBe('nlb---1-1234');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe.skipIf(!available)('NLB stream handler — byte framing', () => {
|
|
137
|
+
beforeEach(async () => {
|
|
138
|
+
fx = await makeFixture();
|
|
139
|
+
});
|
|
140
|
+
afterEach(async () => {
|
|
141
|
+
if (fx) await fx.cleanup();
|
|
142
|
+
fx = null;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('creates a Connections row on the first chunk (connection establishment)', async () => {
|
|
146
|
+
const f = requireFx();
|
|
147
|
+
const res = await handleNlbStream(nlbEvent('10.0.0.1', '50001', 'PING :tok\r\n'), {
|
|
148
|
+
dynamo: f.client,
|
|
149
|
+
tables: f.tables,
|
|
150
|
+
serverConfig: SERVER_CONFIG,
|
|
151
|
+
motd: MOTD,
|
|
152
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
153
|
+
managementApi: f.mgmt,
|
|
154
|
+
});
|
|
155
|
+
expect(res.statusCode).toBe(200);
|
|
156
|
+
|
|
157
|
+
const probe = new AwsRuntime({
|
|
158
|
+
dynamo: f.client,
|
|
159
|
+
tables: f.tables,
|
|
160
|
+
connId: '__probe__',
|
|
161
|
+
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
162
|
+
managementApi: null,
|
|
163
|
+
});
|
|
164
|
+
const info = await probe.getConnectionInfo(deriveNlbConnectionId('10.0.0.1', '50001'));
|
|
165
|
+
expect(info?.id).toBe(deriveNlbConnectionId('10.0.0.1', '50001'));
|
|
166
|
+
expect(info?.registration).toBe('pre-registration');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('returns a PONG in the response body for a single PING', async () => {
|
|
170
|
+
const f = requireFx();
|
|
171
|
+
// First chunk establishes the connection.
|
|
172
|
+
await handleNlbStream(nlbEvent('10.0.0.2', '50002', ''), {
|
|
173
|
+
dynamo: f.client,
|
|
174
|
+
tables: f.tables,
|
|
175
|
+
serverConfig: SERVER_CONFIG,
|
|
176
|
+
motd: MOTD,
|
|
177
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
178
|
+
managementApi: f.mgmt,
|
|
179
|
+
});
|
|
180
|
+
const res = await handleNlbStream(nlbEvent('10.0.0.2', '50002', 'PING :tok\r\n'), {
|
|
181
|
+
dynamo: f.client,
|
|
182
|
+
tables: f.tables,
|
|
183
|
+
serverConfig: SERVER_CONFIG,
|
|
184
|
+
motd: MOTD,
|
|
185
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
186
|
+
managementApi: f.mgmt,
|
|
187
|
+
});
|
|
188
|
+
expect(res.statusCode).toBe(200);
|
|
189
|
+
expect(res.isBase64Encoded).toBe(true);
|
|
190
|
+
const decoded = Buffer.from(res.body, 'base64').toString('utf8');
|
|
191
|
+
expect(decoded).toContain('PONG :tok');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('completes a full registration sequence (NICK + USER) and returns the welcome block', async () => {
|
|
195
|
+
const f = requireFx();
|
|
196
|
+
const connId = deriveNlbConnectionId('10.0.0.3', '50003');
|
|
197
|
+
// First chunk establishes the connection.
|
|
198
|
+
await handleNlbStream(nlbEvent('10.0.0.3', '50003', ''), {
|
|
199
|
+
dynamo: f.client,
|
|
200
|
+
tables: f.tables,
|
|
201
|
+
serverConfig: SERVER_CONFIG,
|
|
202
|
+
motd: MOTD,
|
|
203
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
204
|
+
managementApi: f.mgmt,
|
|
205
|
+
});
|
|
206
|
+
const res = await handleNlbStream(
|
|
207
|
+
nlbEvent('10.0.0.3', '50003', 'NICK alice\r\nUSER alice 0 * :Alice\r\n'),
|
|
208
|
+
{
|
|
209
|
+
dynamo: f.client,
|
|
210
|
+
tables: f.tables,
|
|
211
|
+
serverConfig: SERVER_CONFIG,
|
|
212
|
+
motd: MOTD,
|
|
213
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
214
|
+
managementApi: f.mgmt,
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
expect(res.statusCode).toBe(200);
|
|
218
|
+
const decoded = Buffer.from(res.body, 'base64').toString('utf8');
|
|
219
|
+
expect(decoded).toContain('001');
|
|
220
|
+
expect(decoded).toContain('alice');
|
|
221
|
+
|
|
222
|
+
const probe = new AwsRuntime({
|
|
223
|
+
dynamo: f.client,
|
|
224
|
+
tables: f.tables,
|
|
225
|
+
connId: '__probe__',
|
|
226
|
+
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
227
|
+
managementApi: null,
|
|
228
|
+
});
|
|
229
|
+
const info = await probe.getConnectionInfo(connId);
|
|
230
|
+
expect(info?.nick).toBe('alice');
|
|
231
|
+
expect(info?.registration).toBe('registered');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe.skipIf(!available)('NLB stream handler — chunk boundaries', () => {
|
|
236
|
+
beforeEach(async () => {
|
|
237
|
+
fx = await makeFixture();
|
|
238
|
+
});
|
|
239
|
+
afterEach(async () => {
|
|
240
|
+
if (fx) await fx.cleanup();
|
|
241
|
+
fx = null;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('buffers a partial line across two invocations and completes it on the second', async () => {
|
|
245
|
+
const f = requireFx();
|
|
246
|
+
// Establish + register.
|
|
247
|
+
await handleNlbStream(nlbEvent('10.0.0.10', '50010', ''), {
|
|
248
|
+
dynamo: f.client,
|
|
249
|
+
tables: f.tables,
|
|
250
|
+
serverConfig: SERVER_CONFIG,
|
|
251
|
+
motd: MOTD,
|
|
252
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
253
|
+
managementApi: f.mgmt,
|
|
254
|
+
});
|
|
255
|
+
await handleNlbStream(nlbEvent('10.0.0.10', '50010', 'NICK bob\r\nUSER bob 0 * :Bob\r\n'), {
|
|
256
|
+
dynamo: f.client,
|
|
257
|
+
tables: f.tables,
|
|
258
|
+
serverConfig: SERVER_CONFIG,
|
|
259
|
+
motd: MOTD,
|
|
260
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
261
|
+
managementApi: f.mgmt,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// First chunk: partial PING with no terminator.
|
|
265
|
+
const res1 = await handleNlbStream(nlbEvent('10.0.0.10', '50010', 'PING :to'), {
|
|
266
|
+
dynamo: f.client,
|
|
267
|
+
tables: f.tables,
|
|
268
|
+
serverConfig: SERVER_CONFIG,
|
|
269
|
+
motd: MOTD,
|
|
270
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
271
|
+
managementApi: f.mgmt,
|
|
272
|
+
});
|
|
273
|
+
expect(res1.statusCode).toBe(200);
|
|
274
|
+
// No PONG yet — the line is incomplete.
|
|
275
|
+
const decoded1 = Buffer.from(res1.body, 'base64').toString('utf8');
|
|
276
|
+
expect(decoded1).not.toContain('PONG');
|
|
277
|
+
|
|
278
|
+
// Second chunk: the rest of the line + terminator.
|
|
279
|
+
const res2 = await handleNlbStream(nlbEvent('10.0.0.10', '50010', 'k\r\n'), {
|
|
280
|
+
dynamo: f.client,
|
|
281
|
+
tables: f.tables,
|
|
282
|
+
serverConfig: SERVER_CONFIG,
|
|
283
|
+
motd: MOTD,
|
|
284
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
285
|
+
managementApi: f.mgmt,
|
|
286
|
+
});
|
|
287
|
+
expect(res2.statusCode).toBe(200);
|
|
288
|
+
const decoded2 = Buffer.from(res2.body, 'base64').toString('utf8');
|
|
289
|
+
expect(decoded2).toContain('PONG :tok');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('reassembles a line split across three chunks', async () => {
|
|
293
|
+
const f = requireFx();
|
|
294
|
+
await handleNlbStream(nlbEvent('10.0.0.11', '50011', ''), {
|
|
295
|
+
dynamo: f.client,
|
|
296
|
+
tables: f.tables,
|
|
297
|
+
serverConfig: SERVER_CONFIG,
|
|
298
|
+
motd: MOTD,
|
|
299
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
300
|
+
managementApi: f.mgmt,
|
|
301
|
+
});
|
|
302
|
+
await handleNlbStream(
|
|
303
|
+
nlbEvent('10.0.0.11', '50011', 'NICK carol\r\nUSER carol 0 * :Carol\r\n'),
|
|
304
|
+
{
|
|
305
|
+
dynamo: f.client,
|
|
306
|
+
tables: f.tables,
|
|
307
|
+
serverConfig: SERVER_CONFIG,
|
|
308
|
+
motd: MOTD,
|
|
309
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
310
|
+
managementApi: f.mgmt,
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const commonOpts = {
|
|
315
|
+
dynamo: f.client,
|
|
316
|
+
tables: f.tables,
|
|
317
|
+
serverConfig: SERVER_CONFIG,
|
|
318
|
+
motd: MOTD,
|
|
319
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
320
|
+
managementApi: f.mgmt,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const r1 = await handleNlbStream(nlbEvent('10.0.0.11', '50011', 'PI'), commonOpts);
|
|
324
|
+
const d1 = Buffer.from(r1.body, 'base64').toString('utf8');
|
|
325
|
+
expect(d1).not.toContain('PONG');
|
|
326
|
+
|
|
327
|
+
const r2 = await handleNlbStream(nlbEvent('10.0.0.11', '50011', 'NG :hello'), commonOpts);
|
|
328
|
+
const d2 = Buffer.from(r2.body, 'base64').toString('utf8');
|
|
329
|
+
expect(d2).not.toContain('PONG');
|
|
330
|
+
|
|
331
|
+
const r3 = await handleNlbStream(nlbEvent('10.0.0.11', '50011', '\r\n'), commonOpts);
|
|
332
|
+
const d3 = Buffer.from(r3.body, 'base64').toString('utf8');
|
|
333
|
+
expect(d3).toContain('PONG :hello');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('persists the transport buffer in DynamoDB between invocations', async () => {
|
|
337
|
+
const f = requireFx();
|
|
338
|
+
await handleNlbStream(nlbEvent('10.0.0.12', '50012', ''), {
|
|
339
|
+
dynamo: f.client,
|
|
340
|
+
tables: f.tables,
|
|
341
|
+
serverConfig: SERVER_CONFIG,
|
|
342
|
+
motd: MOTD,
|
|
343
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
344
|
+
managementApi: f.mgmt,
|
|
345
|
+
});
|
|
346
|
+
// Feed a partial line.
|
|
347
|
+
await handleNlbStream(nlbEvent('10.0.0.12', '50012', 'PING :partial'), {
|
|
348
|
+
dynamo: f.client,
|
|
349
|
+
tables: f.tables,
|
|
350
|
+
serverConfig: SERVER_CONFIG,
|
|
351
|
+
motd: MOTD,
|
|
352
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
353
|
+
managementApi: f.mgmt,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// The Connections row must carry the buffered partial line.
|
|
357
|
+
const { GetCommand } = await import('@aws-sdk/lib-dynamodb');
|
|
358
|
+
const row = await f.client.send(
|
|
359
|
+
new GetCommand({
|
|
360
|
+
TableName: f.tables.Connections,
|
|
361
|
+
Key: { connectionId: deriveNlbConnectionId('10.0.0.12', '50012') },
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
expect(row.Item?.transportBuffer).toBe('PING :partial');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe.skipIf(!available)('NLB stream handler — streaming back', () => {
|
|
369
|
+
beforeEach(async () => {
|
|
370
|
+
fx = await makeFixture();
|
|
371
|
+
});
|
|
372
|
+
afterEach(async () => {
|
|
373
|
+
if (fx) await fx.cleanup();
|
|
374
|
+
fx = null;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('returns base64-encoded response bytes for the NLB client', async () => {
|
|
378
|
+
const f = requireFx();
|
|
379
|
+
await handleNlbStream(nlbEvent('10.0.0.20', '50020', ''), {
|
|
380
|
+
dynamo: f.client,
|
|
381
|
+
tables: f.tables,
|
|
382
|
+
serverConfig: SERVER_CONFIG,
|
|
383
|
+
motd: MOTD,
|
|
384
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
385
|
+
managementApi: f.mgmt,
|
|
386
|
+
});
|
|
387
|
+
const res = await handleNlbStream(nlbEvent('10.0.0.20', '50020', 'PING :xyz\r\n'), {
|
|
388
|
+
dynamo: f.client,
|
|
389
|
+
tables: f.tables,
|
|
390
|
+
serverConfig: SERVER_CONFIG,
|
|
391
|
+
motd: MOTD,
|
|
392
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
393
|
+
managementApi: f.mgmt,
|
|
394
|
+
});
|
|
395
|
+
expect(res.statusCode).toBe(200);
|
|
396
|
+
expect(res.isBase64Encoded).toBe(true);
|
|
397
|
+
// The body must be valid base64 that decodes to the PONG line.
|
|
398
|
+
const decoded = Buffer.from(res.body, 'base64').toString('utf8');
|
|
399
|
+
expect(decoded).toMatch(/PONG :xyz\r?\n/);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('fans out a PRIVMSG to a second NLB connection via the management API', async () => {
|
|
403
|
+
const f = requireFx();
|
|
404
|
+
// Connection A registers.
|
|
405
|
+
await handleNlbStream(nlbEvent('10.0.0.30', '50030', ''), {
|
|
406
|
+
dynamo: f.client,
|
|
407
|
+
tables: f.tables,
|
|
408
|
+
serverConfig: SERVER_CONFIG,
|
|
409
|
+
motd: MOTD,
|
|
410
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
411
|
+
managementApi: f.mgmt,
|
|
412
|
+
});
|
|
413
|
+
await handleNlbStream(
|
|
414
|
+
nlbEvent('10.0.0.30', '50030', 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #room\r\n'),
|
|
415
|
+
{
|
|
416
|
+
dynamo: f.client,
|
|
417
|
+
tables: f.tables,
|
|
418
|
+
serverConfig: SERVER_CONFIG,
|
|
419
|
+
motd: MOTD,
|
|
420
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
421
|
+
managementApi: f.mgmt,
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
// Connection B registers + joins the same channel.
|
|
425
|
+
const bSink: string[] = [];
|
|
426
|
+
const bConnId = deriveNlbConnectionId('10.0.0.31', '50031');
|
|
427
|
+
f.mgmt.register(bConnId, (data) => {
|
|
428
|
+
for (const line of data.split('\r\n')) {
|
|
429
|
+
if (line.length > 0) bSink.push(line);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
await handleNlbStream(nlbEvent('10.0.0.31', '50031', ''), {
|
|
433
|
+
dynamo: f.client,
|
|
434
|
+
tables: f.tables,
|
|
435
|
+
serverConfig: SERVER_CONFIG,
|
|
436
|
+
motd: MOTD,
|
|
437
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
438
|
+
managementApi: f.mgmt,
|
|
439
|
+
});
|
|
440
|
+
await handleNlbStream(
|
|
441
|
+
nlbEvent('10.0.0.31', '50031', 'NICK bob\r\nUSER bob 0 * :Bob\r\nJOIN #room\r\n'),
|
|
442
|
+
{
|
|
443
|
+
dynamo: f.client,
|
|
444
|
+
tables: f.tables,
|
|
445
|
+
serverConfig: SERVER_CONFIG,
|
|
446
|
+
motd: MOTD,
|
|
447
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
448
|
+
managementApi: f.mgmt,
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
// Alice sends a PRIVMSG; Bob's sink should receive it via management API.
|
|
452
|
+
await handleNlbStream(nlbEvent('10.0.0.30', '50030', 'PRIVMSG #room :hello from alice\r\n'), {
|
|
453
|
+
dynamo: f.client,
|
|
454
|
+
tables: f.tables,
|
|
455
|
+
serverConfig: SERVER_CONFIG,
|
|
456
|
+
motd: MOTD,
|
|
457
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
458
|
+
managementApi: f.mgmt,
|
|
459
|
+
});
|
|
460
|
+
expect(bSink.some((l) => l.includes('PRIVMSG #room :hello from alice'))).toBe(true);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Test double for `ApiGatewayManagementApi`. Routes `postToConnection`
|
|
466
|
+
* calls to a registered sink for `ConnectionId`.
|
|
467
|
+
*/
|
|
468
|
+
class LocalPostToConnection implements PostToConnection {
|
|
469
|
+
private readonly sinks = new Map<string, (data: string) => void>();
|
|
470
|
+
register(connId: string, sink: (data: string) => void): void {
|
|
471
|
+
this.sinks.set(connId, sink);
|
|
472
|
+
}
|
|
473
|
+
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<unknown> {
|
|
474
|
+
const sink = this.sinks.get(input.ConnectionId);
|
|
475
|
+
if (sink !== undefined) sink(input.Data);
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
@@ -52,6 +52,11 @@ interface Fixture {
|
|
|
52
52
|
|
|
53
53
|
let fx: Fixture | null;
|
|
54
54
|
|
|
55
|
+
function requireFx(): Fixture {
|
|
56
|
+
if (!fx) throw new Error('test fixture not initialized');
|
|
57
|
+
return fx;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
async function makeFixture(): Promise<Fixture> {
|
|
56
61
|
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
57
62
|
const prefix = `p${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
@@ -100,9 +105,9 @@ async function makeFixture(): Promise<Fixture> {
|
|
|
100
105
|
|
|
101
106
|
function params(overrides: Partial<PingCheckParams> = {}): PingCheckParams {
|
|
102
107
|
return {
|
|
103
|
-
dynamo:
|
|
104
|
-
tables:
|
|
105
|
-
managementApi:
|
|
108
|
+
dynamo: requireFx().client,
|
|
109
|
+
tables: requireFx().tables,
|
|
110
|
+
managementApi: requireFx().mgmt,
|
|
106
111
|
...overrides,
|
|
107
112
|
};
|
|
108
113
|
}
|
|
@@ -120,18 +125,18 @@ async function seedConnection(opts: {
|
|
|
120
125
|
if (opts.nick !== undefined) {
|
|
121
126
|
state.nick = opts.nick as Nick;
|
|
122
127
|
state.user = opts.nick;
|
|
123
|
-
await
|
|
128
|
+
await requireFx().client.send(
|
|
124
129
|
new PutCommand({
|
|
125
|
-
TableName:
|
|
130
|
+
TableName: requireFx().tables.Nicks,
|
|
126
131
|
Item: marshalNick(opts.nick as Nick, opts.connId, opts.idleSince),
|
|
127
132
|
}),
|
|
128
133
|
);
|
|
129
134
|
}
|
|
130
135
|
for (const chan of opts.channels ?? []) {
|
|
131
136
|
state.joinedChannels.add(chan);
|
|
132
|
-
await
|
|
137
|
+
await requireFx().client.send(
|
|
133
138
|
new PutCommand({
|
|
134
|
-
TableName:
|
|
139
|
+
TableName: requireFx().tables.ChannelMembers,
|
|
135
140
|
Item: marshalChannelMember(chan.toLowerCase(), {
|
|
136
141
|
conn: opts.connId,
|
|
137
142
|
nick: (opts.nick ?? 'x') as Nick,
|
|
@@ -141,26 +146,26 @@ async function seedConnection(opts: {
|
|
|
141
146
|
}),
|
|
142
147
|
);
|
|
143
148
|
}
|
|
144
|
-
await
|
|
149
|
+
await requireFx().client.send(
|
|
145
150
|
new PutCommand({
|
|
146
|
-
TableName:
|
|
151
|
+
TableName: requireFx().tables.Connections,
|
|
147
152
|
Item: marshalConnection(state, opts.idleSince),
|
|
148
153
|
}),
|
|
149
154
|
);
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
async function connectionExists(connId: string): Promise<boolean> {
|
|
153
|
-
const res = await
|
|
154
|
-
new GetCommand({ TableName:
|
|
158
|
+
const res = await requireFx().client.send(
|
|
159
|
+
new GetCommand({ TableName: requireFx().tables.Connections, Key: { connectionId: connId } }),
|
|
155
160
|
);
|
|
156
161
|
return res.Item !== undefined;
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
async function membersOf(channelKey: string): Promise<{ connectionId: string }[]> {
|
|
160
165
|
const { QueryCommand } = await import('@aws-sdk/lib-dynamodb');
|
|
161
|
-
const res = await
|
|
166
|
+
const res = await requireFx().client.send(
|
|
162
167
|
new QueryCommand({
|
|
163
|
-
TableName:
|
|
168
|
+
TableName: requireFx().tables.ChannelMembers,
|
|
164
169
|
KeyConditionExpression: 'channelName = :k',
|
|
165
170
|
ExpressionAttributeValues: { ':k': channelKey },
|
|
166
171
|
}),
|
|
@@ -169,8 +174,8 @@ async function membersOf(channelKey: string): Promise<{ connectionId: string }[]
|
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
async function nickExists(nick: string): Promise<boolean> {
|
|
172
|
-
const res = await
|
|
173
|
-
new GetCommand({ TableName:
|
|
177
|
+
const res = await requireFx().client.send(
|
|
178
|
+
new GetCommand({ TableName: requireFx().tables.Nicks, Key: { nickLower: nick.toLowerCase() } }),
|
|
174
179
|
);
|
|
175
180
|
return res.Item !== undefined;
|
|
176
181
|
}
|
|
@@ -200,10 +205,10 @@ describe.skipIf(!available)('idle / PING checker', () => {
|
|
|
200
205
|
expect(result.scanned).toBe(1);
|
|
201
206
|
expect(result.pinged).toBe(1);
|
|
202
207
|
expect(result.disconnected).toBe(0);
|
|
203
|
-
expect(
|
|
204
|
-
expect(
|
|
208
|
+
expect(requireFx().mgmt.calls.size).toBe(1);
|
|
209
|
+
expect(requireFx().mgmt.calls.has('idle')).toBe(true);
|
|
205
210
|
// PING line format: `PING :<token>` (token is opaque, server-chosen).
|
|
206
|
-
const line =
|
|
211
|
+
const line = requireFx().mgmt.calls.get('idle')?.join('\r\n');
|
|
207
212
|
expect(line).toMatch(/^PING :.+\r?\n?$/);
|
|
208
213
|
});
|
|
209
214
|
|
|
@@ -221,7 +226,7 @@ describe.skipIf(!available)('idle / PING checker', () => {
|
|
|
221
226
|
expect(result.disconnected).toBe(1);
|
|
222
227
|
expect(await connectionExists('ghost')).toBe(false);
|
|
223
228
|
// No PING is sent in the disconnect window — past it.
|
|
224
|
-
expect(
|
|
229
|
+
expect(requireFx().mgmt.calls.size).toBe(0);
|
|
225
230
|
});
|
|
226
231
|
|
|
227
232
|
// -------------------------------------------------------------------------
|
|
@@ -239,7 +244,7 @@ describe.skipIf(!available)('idle / PING checker', () => {
|
|
|
239
244
|
expect(result.scanned).toBe(1);
|
|
240
245
|
expect(result.pinged).toBe(0);
|
|
241
246
|
expect(result.disconnected).toBe(0);
|
|
242
|
-
expect(
|
|
247
|
+
expect(requireFx().mgmt.calls.size).toBe(0);
|
|
243
248
|
expect(await connectionExists('live')).toBe(true);
|
|
244
249
|
});
|
|
245
250
|
|
|
@@ -329,7 +334,10 @@ describe.skipIf(!available)('idle / PING checker', () => {
|
|
|
329
334
|
expect(await connectionExists('live')).toBe(true);
|
|
330
335
|
expect(await connectionExists('pingMe')).toBe(true);
|
|
331
336
|
expect(await connectionExists('reap')).toBe(false);
|
|
332
|
-
expect(
|
|
337
|
+
expect(fx).toBeDefined();
|
|
338
|
+
if (fx) {
|
|
339
|
+
expect([...fx.mgmt.calls.keys()]).toEqual(['pingMe']);
|
|
340
|
+
}
|
|
333
341
|
});
|
|
334
342
|
|
|
335
343
|
it('uses Date.now() when `now` is omitted (real-clock default path)', async () => {
|
|
@@ -377,9 +385,9 @@ describe.skipIf(!available)('idle / PING checker', () => {
|
|
|
377
385
|
});
|
|
378
386
|
|
|
379
387
|
it('skips a row whose idleSince is not a number (defensive)', async () => {
|
|
380
|
-
await
|
|
388
|
+
await requireFx().client.send(
|
|
381
389
|
new PutCommand({
|
|
382
|
-
TableName:
|
|
390
|
+
TableName: requireFx().tables.Connections,
|
|
383
391
|
Item: {
|
|
384
392
|
connectionId: 'corrupt',
|
|
385
393
|
registration: 'registered',
|
|
@@ -408,7 +416,7 @@ describe.skipIf(!available)('idle / PING checker', () => {
|
|
|
408
416
|
// the connection as disconnected.
|
|
409
417
|
const now = 100_000;
|
|
410
418
|
await seedConnection({ connId: 'halfopen', idleSince: 0 }); // idleFor = 100s
|
|
411
|
-
|
|
419
|
+
requireFx().mgmt.failNextWithGone('halfopen');
|
|
412
420
|
|
|
413
421
|
const result = await handlePingCheck(
|
|
414
422
|
params({ now, pingIntervalMs: 60_000, pongTimeoutMs: 60_000 }),
|
|
@@ -431,7 +439,7 @@ describe.skipIf(!available)('idle / PING checker', () => {
|
|
|
431
439
|
it('rethrows non-Gone errors from the PING send (no silent swallow)', async () => {
|
|
432
440
|
const now = 100_000;
|
|
433
441
|
await seedConnection({ connId: 'idle', idleSince: 0 });
|
|
434
|
-
|
|
442
|
+
requireFx().mgmt.throwNext('idle', new Error('InternalServerError: boom'));
|
|
435
443
|
|
|
436
444
|
await expect(
|
|
437
445
|
handlePingCheck(params({ now, pingIntervalMs: 60_000, pongTimeoutMs: 60_000 })),
|
|
@@ -548,7 +556,7 @@ class RecordingPostToConnection {
|
|
|
548
556
|
throwNext(connId: string, err: Error): void {
|
|
549
557
|
this.throwNextErr.set(connId, err);
|
|
550
558
|
}
|
|
551
|
-
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<
|
|
559
|
+
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<unknown> {
|
|
552
560
|
const throwErr = this.throwNextErr.get(input.ConnectionId);
|
|
553
561
|
if (throwErr !== undefined) {
|
|
554
562
|
this.throwNextErr.delete(input.ConnectionId);
|
|
@@ -566,6 +574,3 @@ class RecordingPostToConnection {
|
|
|
566
574
|
return {};
|
|
567
575
|
}
|
|
568
576
|
}
|
|
569
|
-
|
|
570
|
-
// Keep the import live for type usage in `params`.
|
|
571
|
-
export type { DynamoDBDocumentClient };
|