serverless-ircd 0.5.0 → 0.7.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 +40 -3
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +474 -0
- package/README.md +219 -22
- package/apps/aws-stack/README.md +33 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +74 -9
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +2 -2
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +59 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +9 -7
- package/apps/local-cli/tests/e2e.test.ts +112 -28
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +2 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +16 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/handlers/default.ts +3 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/serialize.ts +29 -1
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +9 -9
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +90 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/connection-do.ts +3 -0
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/src/caps/capabilities.ts +37 -3
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +34 -0
- package/packages/irc-core/src/commands/isupport.ts +53 -0
- package/packages/irc-core/src/commands/mode.ts +8 -1
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +65 -10
- package/packages/irc-core/src/effects.ts +14 -0
- package/packages/irc-core/src/ports.ts +389 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +15 -0
- package/packages/irc-core/src/protocol/outbound.ts +16 -1
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +112 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/mode.test.ts +72 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +450 -3
- package/packages/irc-server/src/dispatch.ts +108 -16
- package/packages/irc-server/tests/actor.test.ts +1432 -39
- package/packages/irc-server/tests/dispatch.test.ts +263 -2
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
13
|
-
import {
|
|
13
|
+
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
14
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
14
15
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
15
16
|
import {
|
|
16
17
|
loadDynamoAccountStore,
|
|
@@ -126,6 +127,48 @@ describe.skipIf(!available)('Accounts table round-trip', () => {
|
|
|
126
127
|
false,
|
|
127
128
|
);
|
|
128
129
|
});
|
|
130
|
+
|
|
131
|
+
it('silently skips rows with missing or wrong-typed credential fields', async () => {
|
|
132
|
+
// The scan path must tolerate partially-malformed rows (legacy seeds,
|
|
133
|
+
// schema drift) without throwing — it just skips them. Each branch of
|
|
134
|
+
// the per-field `typeof === 'string'` guard is exercised by writing a
|
|
135
|
+
// row where exactly one non-key field has the wrong type. (The
|
|
136
|
+
// `account` field is the partition key, so DynamoDB itself enforces
|
|
137
|
+
// its type at write time and the false branch is not exercisable
|
|
138
|
+
// through the SDK; that branch is a defensive guard against
|
|
139
|
+
// synthetically-shaped Scan results.)
|
|
140
|
+
const client = requireFx().client;
|
|
141
|
+
const table = requireFx().tableName;
|
|
142
|
+
// Well-formed row — must survive.
|
|
143
|
+
await putAccountCredential(client, table, 'good', 's3cret');
|
|
144
|
+
// Row with non-string `algorithm`.
|
|
145
|
+
await client.send(
|
|
146
|
+
new PutCommand({
|
|
147
|
+
TableName: table,
|
|
148
|
+
Item: { account: 'noalgo', algorithm: 7, salt: 's', hash: 'h' },
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
// Row with non-string `salt`.
|
|
152
|
+
await client.send(
|
|
153
|
+
new PutCommand({
|
|
154
|
+
TableName: table,
|
|
155
|
+
Item: { account: 'nosalt', algorithm: 'SCRYPT-SHA-256', salt: false, hash: 'h' },
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
// Row with non-string `hash`.
|
|
159
|
+
await client.send(
|
|
160
|
+
new PutCommand({
|
|
161
|
+
TableName: table,
|
|
162
|
+
Item: { account: 'nohash', algorithm: 'SCRYPT-SHA-256', salt: 's', hash: null },
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
const store = await loadDynamoAccountStore(client, table);
|
|
166
|
+
expect(store).toBeDefined();
|
|
167
|
+
expect(store?.size).toBe(1);
|
|
168
|
+
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'good', password: 's3cret' }).ok).toBe(
|
|
169
|
+
true,
|
|
170
|
+
);
|
|
171
|
+
});
|
|
129
172
|
});
|
|
130
173
|
|
|
131
174
|
describe.skipIf(!available)('resolveAccountStore precedence', () => {
|
|
@@ -139,10 +182,9 @@ describe.skipIf(!available)('resolveAccountStore precedence', () => {
|
|
|
139
182
|
|
|
140
183
|
it('returns a DynamoAccountStore when the table has rows (table wins)', async () => {
|
|
141
184
|
await putAccountCredential(requireFx().client, requireFx().tableName, 'alice', 'table-secret');
|
|
142
|
-
const cfgWithSasl = {
|
|
143
|
-
...DEFAULT_SERVER_CONFIG,
|
|
185
|
+
const cfgWithSasl = makeTestServerConfig({
|
|
144
186
|
saslAccounts: [{ username: 'envuser', password: 'env-secret' }],
|
|
145
|
-
};
|
|
187
|
+
});
|
|
146
188
|
const store = await resolveAccountStore(requireFx().client, requireFx().tableName, cfgWithSasl);
|
|
147
189
|
expect(store).toBeDefined();
|
|
148
190
|
// Table account verifies; config account does NOT (table is authoritative).
|
|
@@ -155,10 +197,9 @@ describe.skipIf(!available)('resolveAccountStore precedence', () => {
|
|
|
155
197
|
});
|
|
156
198
|
|
|
157
199
|
it('falls back to the config-seeded store when the table is empty', async () => {
|
|
158
|
-
const cfgWithSasl = {
|
|
159
|
-
...DEFAULT_SERVER_CONFIG,
|
|
200
|
+
const cfgWithSasl = makeTestServerConfig({
|
|
160
201
|
saslAccounts: [{ username: 'envuser', password: 'env-secret' }],
|
|
161
|
-
};
|
|
202
|
+
});
|
|
162
203
|
const store = await resolveAccountStore(requireFx().client, requireFx().tableName, cfgWithSasl);
|
|
163
204
|
expect(store).toBeDefined();
|
|
164
205
|
expect(
|
|
@@ -170,7 +211,7 @@ describe.skipIf(!available)('resolveAccountStore precedence', () => {
|
|
|
170
211
|
const store = await resolveAccountStore(
|
|
171
212
|
requireFx().client,
|
|
172
213
|
requireFx().tableName,
|
|
173
|
-
|
|
214
|
+
makeTestServerConfig(),
|
|
174
215
|
);
|
|
175
216
|
expect(store).toBeUndefined();
|
|
176
217
|
});
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { DynamoDBDocumentClient, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type ParsedServerConfig,
|
|
5
|
-
type SaslPayload,
|
|
6
|
-
} from '@serverless-ircd/irc-core';
|
|
2
|
+
import type { ParsedServerConfig, SaslPayload } from '@serverless-ircd/irc-core';
|
|
3
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
7
4
|
import { mockClient } from 'aws-sdk-client-mock';
|
|
8
5
|
import { describe, expect, it } from 'vitest';
|
|
9
6
|
import {
|
|
@@ -21,7 +18,7 @@ function plainPayload(username: string, password: string): SaslPayload {
|
|
|
21
18
|
|
|
22
19
|
/** Builds a minimal parsed config with the supplied saslAccounts. */
|
|
23
20
|
function configWith(accounts: Array<{ username: string; password: string }>): ParsedServerConfig {
|
|
24
|
-
return {
|
|
21
|
+
return makeTestServerConfig({ saslAccounts: accounts });
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
// ---------------------------------------------------------------------------
|
|
@@ -184,7 +181,7 @@ describe('resolveAccountStore', () => {
|
|
|
184
181
|
'Accounts',
|
|
185
182
|
fallback === undefined
|
|
186
183
|
? undefined
|
|
187
|
-
: {
|
|
184
|
+
: makeTestServerConfig({ saslAccounts: [{ username: 'envuser', password: 'env-pw' }] }),
|
|
188
185
|
);
|
|
189
186
|
expect(store).toBeDefined();
|
|
190
187
|
expect(store?.verify('PLAIN', plainPayload('alice', 's3cret')).ok).toBe(true);
|
|
@@ -53,8 +53,11 @@ const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Factory entry — appends to a suite's FACTORIES array to opt the scenarios
|
|
56
|
-
* into the AWS matrix.
|
|
57
|
-
*
|
|
56
|
+
* into the AWS matrix. The whole describe block is gated by
|
|
57
|
+
* `describe.skipIf(process.env.DDB_AVAILABLE !== '1')` at the call site
|
|
58
|
+
* (see `aws-integration.test.ts`), so no scenarios run when DynamoDB
|
|
59
|
+
* Local is unavailable. This factory itself assumes DDB is up; its
|
|
60
|
+
* `create()` throws if `DYNAMO_ENDPOINT` was never set.
|
|
58
61
|
*/
|
|
59
62
|
export const awsHarnessFactory: IrcHarnessFactory = Object.freeze({
|
|
60
63
|
name: 'aws+ws',
|
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
* a fresh {@link AwsHarness} which mints uniquely-prefixed DynamoDB
|
|
7
7
|
* tables against the global-setup's endpoint.
|
|
8
8
|
*
|
|
9
|
-
* Gated on `process.env.DDB_AVAILABLE`: the suite is skipped
|
|
10
|
-
* DynamoDB Local cannot be booted (no Docker, no
|
|
11
|
-
* every scenario MUST pass — that is the
|
|
9
|
+
* Gated on `process.env.DDB_AVAILABLE`: the whole suite is skipped via
|
|
10
|
+
* `describe.skipIf` when DynamoDB Local cannot be booted (no Docker, no
|
|
11
|
+
* Java). When available, every scenario MUST pass — that is the
|
|
12
|
+
* acceptance criterion.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { runIrcScenarios } from '@serverless-ircd/irc-test-support';
|
|
16
|
+
import { describe } from 'vitest';
|
|
15
17
|
import { awsHarnessFactory } from './aws-harness.js';
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
describe.skipIf(process.env.DDB_AVAILABLE !== '1')('aws integration scenarios', () => {
|
|
20
|
+
runIrcScenarios([awsHarnessFactory]);
|
|
21
|
+
});
|
|
@@ -399,6 +399,85 @@ describe.skipIf(!available)('AwsRuntime', () => {
|
|
|
399
399
|
});
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
+
// -------------------------------------------------------------------------
|
|
403
|
+
// broadcastWallops — Scan-based fan-out to every +w connection
|
|
404
|
+
// -------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
describe('broadcastWallops', () => {
|
|
407
|
+
it('delivers to every +w connection and skips the except id', async () => {
|
|
408
|
+
const receivedA: string[] = [];
|
|
409
|
+
const receivedB: string[] = [];
|
|
410
|
+
const stateA = createConnection({ id: 'w-a', connectedSince: 0 });
|
|
411
|
+
stateA.userModes.wallops = true;
|
|
412
|
+
const stateB = createConnection({ id: 'w-b', connectedSince: 0 });
|
|
413
|
+
stateB.userModes.wallops = true;
|
|
414
|
+
|
|
415
|
+
const mgmt = new LocalPostToConnection();
|
|
416
|
+
mgmt.register('w-a', (data) => {
|
|
417
|
+
for (const line of data.split('\r\n')) {
|
|
418
|
+
if (line.length > 0) receivedA.push(line);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
mgmt.register('w-b', (data) => {
|
|
422
|
+
for (const line of data.split('\r\n')) {
|
|
423
|
+
if (line.length > 0) receivedB.push(line);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const rtA = makeRuntime('w-a', handlersSink(receivedA, stateA), undefined, mgmt);
|
|
428
|
+
const rtB = makeRuntime('w-b', handlersSink(receivedB, stateB), undefined, mgmt);
|
|
429
|
+
|
|
430
|
+
await rtA.persistConnectionState(stateA);
|
|
431
|
+
await rtB.persistConnectionState(stateB);
|
|
432
|
+
|
|
433
|
+
// Caller 'w-a' is excepted; only 'w-b' should receive the line.
|
|
434
|
+
await rtA.broadcastWallops([{ text: ':oper NOTICE * :hello' }], 'w-a');
|
|
435
|
+
expect(receivedA).toEqual([]);
|
|
436
|
+
expect(receivedB).toEqual([':oper NOTICE * :hello']);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('delivers to the bound connection via in-process handlers when not excepted', async () => {
|
|
440
|
+
// The bound connection's own +w delivery goes through handlers.send,
|
|
441
|
+
// not APIGW. With no `except`, the bound conn is also a recipient.
|
|
442
|
+
const received: string[] = [];
|
|
443
|
+
const state = createConnection({ id: 'self', connectedSince: 0 });
|
|
444
|
+
state.userModes.wallops = true;
|
|
445
|
+
const rt = makeRuntime('self', handlersSink(received, state), undefined, null);
|
|
446
|
+
await rt.persistConnectionState(state);
|
|
447
|
+
await rt.broadcastWallops([{ text: ':oper NOTICE * :hi' }]);
|
|
448
|
+
expect(received).toEqual([':oper NOTICE * :hi']);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// -------------------------------------------------------------------------
|
|
453
|
+
// Nick registry — error propagation
|
|
454
|
+
// -------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
describe('reserveNick error propagation', () => {
|
|
457
|
+
it('rethrows non-conditional errors from the PutCommand', async () => {
|
|
458
|
+
// A non-ConditionalCheckFailedException error must propagate rather
|
|
459
|
+
// than be treated as a nick collision. We force a real DynamoDB
|
|
460
|
+
// error by sending the reserveNick PutCommand against a table that
|
|
461
|
+
// does not exist (deleted mid-flight), which yields a
|
|
462
|
+
// ResourceNotFoundException.
|
|
463
|
+
const rt = makeRuntime('c1', noopHandlers());
|
|
464
|
+
// Drop the Nicks table out from under the runtime.
|
|
465
|
+
await fx.client.send(new DeleteTableCommand({ TableName: fx.tables.Nicks }));
|
|
466
|
+
await expect(rt.reserveNick('Ghost', 'c1')).rejects.toThrow(
|
|
467
|
+
/ResourceNotFoundException|non-existent table/,
|
|
468
|
+
);
|
|
469
|
+
// Recreate the table so afterEach cleanup does not fail on a missing table.
|
|
470
|
+
await fx.client.send(
|
|
471
|
+
new CreateTableCommand({
|
|
472
|
+
TableName: fx.tables.Nicks,
|
|
473
|
+
AttributeDefinitions: [{ AttributeName: 'nickLower', AttributeType: 'S' }],
|
|
474
|
+
KeySchema: [{ AttributeName: 'nickLower', KeyType: 'HASH' }],
|
|
475
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
402
481
|
// -------------------------------------------------------------------------
|
|
403
482
|
// listChannels + getChannelConnections
|
|
404
483
|
// -------------------------------------------------------------------------
|
|
@@ -90,6 +90,15 @@ describe('buildLambdaConfigInput', () => {
|
|
|
90
90
|
);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it('passes an overflowing numeric CREATED_AT through verbatim (not Infinity)', () => {
|
|
94
|
+
// A purely-numeric string that overflows Number.MAX_VALUE renders as
|
|
95
|
+
// `Infinity`, which is not a finite epoch-ms. The parser must fall
|
|
96
|
+
// back to returning the raw string rather than emitting `Infinity`
|
|
97
|
+
// into the config (which would serialize as `null` in JSON).
|
|
98
|
+
const huge = '9'.repeat(400);
|
|
99
|
+
expect(buildLambdaConfigInput({ CREATED_AT: huge }).createdAt).toBe(huge);
|
|
100
|
+
});
|
|
101
|
+
|
|
93
102
|
it('omits serverVersion/createdAt when their env vars are unset', () => {
|
|
94
103
|
const out = buildLambdaConfigInput({});
|
|
95
104
|
expect('serverVersion' in out).toBe(false);
|
|
@@ -154,8 +163,8 @@ describe('loadServerConfigFromLambdaEnv', () => {
|
|
|
154
163
|
expect(typeof cfg.nickLen).toBe('number');
|
|
155
164
|
});
|
|
156
165
|
|
|
157
|
-
it('throws when SERVER_NAME is missing', () => {
|
|
158
|
-
expect(() => loadServerConfigFromLambdaEnv({})).
|
|
166
|
+
it('throws a readable error naming serverName when SERVER_NAME is missing', () => {
|
|
167
|
+
expect(() => loadServerConfigFromLambdaEnv({})).toThrowError(/serverName/u);
|
|
159
168
|
});
|
|
160
169
|
|
|
161
170
|
it('round-trips MOTD lines into ParsedServerConfig', () => {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { type DynamoDBDocumentClient, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
11
|
-
import {
|
|
11
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
12
12
|
import { describe, expect, it } from 'vitest';
|
|
13
13
|
import { handleConnect } from '../src/handlers/connect.js';
|
|
14
14
|
import type { MarshalledConnection } from '../src/serialize.js';
|
|
@@ -66,7 +66,7 @@ describe('handleConnect admission plumbing', () => {
|
|
|
66
66
|
tables: TABLES,
|
|
67
67
|
connectionId: 'c-paged',
|
|
68
68
|
now: 42,
|
|
69
|
-
serverConfig: {
|
|
69
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
70
70
|
});
|
|
71
71
|
expect(outcome).toEqual({ admitted: true, subprotocol: null });
|
|
72
72
|
expect(stub.puts).toBe(1);
|
|
@@ -78,7 +78,7 @@ describe('handleConnect admission plumbing', () => {
|
|
|
78
78
|
dynamo: stub.dynamo,
|
|
79
79
|
tables: TABLES,
|
|
80
80
|
connectionId: 'c-full',
|
|
81
|
-
serverConfig: {
|
|
81
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
82
82
|
});
|
|
83
83
|
expect(outcome).toEqual({ admitted: false, statusCode: 429, reason: 'server full' });
|
|
84
84
|
expect(stub.puts).toBe(0);
|
|
@@ -94,7 +94,7 @@ describe('handleConnect — IRCv3 WebSocket subprotocol negotiation', () => {
|
|
|
94
94
|
connectionId: 'c-binary',
|
|
95
95
|
now: 42,
|
|
96
96
|
secWebSocketProtocol: 'binary.ircv3.net',
|
|
97
|
-
serverConfig: {
|
|
97
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
98
98
|
});
|
|
99
99
|
expect(outcome).toEqual({ admitted: true, subprotocol: 'binary.ircv3.net' });
|
|
100
100
|
expect(stub.puts).toBe(1);
|
|
@@ -110,7 +110,7 @@ describe('handleConnect — IRCv3 WebSocket subprotocol negotiation', () => {
|
|
|
110
110
|
connectionId: 'c-text',
|
|
111
111
|
now: 42,
|
|
112
112
|
secWebSocketProtocol: 'text.ircv3.net',
|
|
113
|
-
serverConfig: {
|
|
113
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
114
114
|
});
|
|
115
115
|
expect(outcome).toEqual({ admitted: true, subprotocol: 'text.ircv3.net' });
|
|
116
116
|
const item = stub.lastPutItem as MarshalledConnection | undefined;
|
|
@@ -125,7 +125,7 @@ describe('handleConnect — IRCv3 WebSocket subprotocol negotiation', () => {
|
|
|
125
125
|
connectionId: 'c-pref',
|
|
126
126
|
now: 42,
|
|
127
127
|
secWebSocketProtocol: 'text.ircv3.net, binary.ircv3.net',
|
|
128
|
-
serverConfig: {
|
|
128
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
129
129
|
});
|
|
130
130
|
expect(outcome).toEqual({ admitted: true, subprotocol: 'text.ircv3.net' });
|
|
131
131
|
});
|
|
@@ -137,7 +137,7 @@ describe('handleConnect — IRCv3 WebSocket subprotocol negotiation', () => {
|
|
|
137
137
|
tables: TABLES,
|
|
138
138
|
connectionId: 'c-legacy',
|
|
139
139
|
now: 42,
|
|
140
|
-
serverConfig: {
|
|
140
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
141
141
|
});
|
|
142
142
|
expect(outcome).toEqual({ admitted: true, subprotocol: null });
|
|
143
143
|
const item = stub.lastPutItem as MarshalledConnection | undefined;
|
|
@@ -152,7 +152,7 @@ describe('handleConnect — IRCv3 WebSocket subprotocol negotiation', () => {
|
|
|
152
152
|
connectionId: 'c-unsupported',
|
|
153
153
|
now: 42,
|
|
154
154
|
secWebSocketProtocol: 'chat, superchat.v2',
|
|
155
|
-
serverConfig: {
|
|
155
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
156
156
|
});
|
|
157
157
|
expect(outcome).toEqual({ admitted: true, subprotocol: null });
|
|
158
158
|
const item = stub.lastPutItem as MarshalledConnection | undefined;
|
|
@@ -166,7 +166,7 @@ describe('handleConnect — IRCv3 WebSocket subprotocol negotiation', () => {
|
|
|
166
166
|
tables: TABLES,
|
|
167
167
|
connectionId: 'c-full',
|
|
168
168
|
secWebSocketProtocol: 'binary.ircv3.net',
|
|
169
|
-
serverConfig: {
|
|
169
|
+
serverConfig: makeTestServerConfig({ maxClients: 5 }),
|
|
170
170
|
});
|
|
171
171
|
expect(outcome).toEqual({ admitted: false, statusCode: 429, reason: 'server full' });
|
|
172
172
|
expect(stub.puts).toBe(0);
|
|
@@ -12,12 +12,12 @@ import { GoneException } from '@aws-sdk/client-apigatewaymanagementapi';
|
|
|
12
12
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
13
13
|
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
14
14
|
import {
|
|
15
|
-
DEFAULT_SERVER_CONFIG,
|
|
16
15
|
type Nick,
|
|
17
16
|
type ParsedServerConfig,
|
|
18
17
|
StaticMotdProvider,
|
|
19
18
|
createConnection,
|
|
20
19
|
} from '@serverless-ircd/irc-core';
|
|
20
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
21
21
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
22
22
|
import { AwsRuntime, type PostToConnection, cleanupConnection } from '../src/aws-runtime.js';
|
|
23
23
|
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
@@ -30,12 +30,11 @@ import type { TablesConfig } from '../src/tables.js';
|
|
|
30
30
|
const available = process.env.DDB_AVAILABLE === '1';
|
|
31
31
|
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
32
32
|
|
|
33
|
-
const SERVER_CONFIG: ParsedServerConfig = {
|
|
34
|
-
...DEFAULT_SERVER_CONFIG,
|
|
33
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
35
34
|
serverName: 'irc.example.com',
|
|
36
35
|
networkName: 'ExampleNet',
|
|
37
36
|
motdLines: [],
|
|
38
|
-
};
|
|
37
|
+
});
|
|
39
38
|
|
|
40
39
|
interface Fixture {
|
|
41
40
|
tables: TablesConfig;
|
|
@@ -2,20 +2,32 @@
|
|
|
2
2
|
* Vitest `globalSetup` — boots a DynamoDB Local instance once per test run
|
|
3
3
|
* and exposes its endpoint via `process.env.DYNAMO_ENDPOINT`.
|
|
4
4
|
*
|
|
5
|
-
* Strategy
|
|
5
|
+
* Strategy selection:
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* - `DDB_LOCAL_STRATEGY=zip` → only the Java ZIP fallback (skips the
|
|
8
|
+
* testcontainers probe). Canonical CI path on the self-hosted Gitea
|
|
9
|
+
* runner, which has a JRE installed via `setup-java` but no
|
|
10
|
+
* privileged container / `docker.sock` mount for Ryuk.
|
|
11
|
+
* - `DDB_LOCAL_STRATEGY=docker` → only testcontainers.
|
|
12
|
+
* - unset / `auto` (default) → try testcontainers first, then ZIP.
|
|
13
|
+
* Matches developer laptops where either backend may be available.
|
|
14
|
+
*
|
|
15
|
+
* Strategies (in priority order under `auto`):
|
|
16
|
+
*
|
|
17
|
+
* 1. **testcontainers** — preferred. Spins up the canonical
|
|
18
|
+
* `amazon/dynamodb-local` Docker image. Requires Docker with a
|
|
19
|
+
* working Ryuk reaper (privileged runner or `docker.sock` mount).
|
|
20
|
+
* 2. **DynamoDB Local ZIP + Java** — development/CI fallback for hosts
|
|
21
|
+
* with Java (e.g. the CI runner image with a JRE, or dev laptops
|
|
22
|
+
* without Docker Desktop). Downloads the official AWS ZIP once
|
|
23
|
+
* into a per-user cache dir, runs it with `-inMemory` so per-run
|
|
24
|
+
* state is isolated by port.
|
|
13
25
|
* 3. **Skip** — sets `process.env.DDB_AVAILABLE = '0'`; integration
|
|
14
26
|
* tests gate themselves on this via `describe.skipIf`.
|
|
15
27
|
*
|
|
16
28
|
* The skip path keeps the suite green in environments that legitimately
|
|
17
29
|
* can't run DynamoDB Local (no Docker, no Java). The acceptance criterion
|
|
18
|
-
* is "all
|
|
30
|
+
* is "all scenarios pass when DDB is available", not "every dev box
|
|
19
31
|
* runs them".
|
|
20
32
|
*/
|
|
21
33
|
|
|
@@ -51,34 +63,47 @@ interface DdbHandle {
|
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
export default async function globalSetup(): Promise<(() => Promise<void>) | undefined> {
|
|
66
|
+
const strategy = (process.env.DDB_LOCAL_STRATEGY ?? 'auto').toLowerCase();
|
|
67
|
+
// Strategy pin: skip the unused probe to avoid a multi-second timeout
|
|
68
|
+
// on hosts that lack the capability. CI pins `zip`; local dev defaults
|
|
69
|
+
// to `auto` so a Docker Desktop install still gets testcontainers.
|
|
70
|
+
const wantDocker = strategy === 'auto' || strategy === 'docker';
|
|
71
|
+
const wantZip = strategy === 'auto' || strategy === 'zip';
|
|
72
|
+
|
|
54
73
|
// Strategy 1: testcontainers (Docker).
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if (wantDocker) {
|
|
75
|
+
try {
|
|
76
|
+
const handle = await tryTestcontainers();
|
|
77
|
+
if (handle !== null) {
|
|
78
|
+
process.env.DDB_AVAILABLE = '1';
|
|
79
|
+
process.env.DYNAMO_ENDPOINT = handle.endpoint;
|
|
80
|
+
console.warn(`[aws-adapter global-setup] DynamoDB Local via Docker at ${handle.endpoint}`);
|
|
81
|
+
return async () => {
|
|
82
|
+
await handle.stop();
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
} catch (err: unknown) {
|
|
86
|
+
console.warn('[aws-adapter global-setup] testcontainers unavailable:', renderErr(err));
|
|
64
87
|
}
|
|
65
|
-
} catch (err: unknown) {
|
|
66
|
-
console.warn('[aws-adapter global-setup] testcontainers unavailable:', renderErr(err));
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
// Strategy 2: local ZIP + Java.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
if (wantZip) {
|
|
92
|
+
try {
|
|
93
|
+
const handle = await tryZip();
|
|
94
|
+
if (handle !== null) {
|
|
95
|
+
process.env.DDB_AVAILABLE = '1';
|
|
96
|
+
process.env.DYNAMO_ENDPOINT = handle.endpoint;
|
|
97
|
+
console.warn(
|
|
98
|
+
`[aws-adapter global-setup] DynamoDB Local via Java ZIP at ${handle.endpoint}`,
|
|
99
|
+
);
|
|
100
|
+
return async () => {
|
|
101
|
+
await handle.stop();
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
} catch (err: unknown) {
|
|
105
|
+
console.warn('[aws-adapter global-setup] ZIP fallback unavailable:', renderErr(err));
|
|
79
106
|
}
|
|
80
|
-
} catch (err: unknown) {
|
|
81
|
-
console.warn('[aws-adapter global-setup] ZIP fallback unavailable:', renderErr(err));
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
// Strategy 3: skip.
|
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
import { GoneException } from '@aws-sdk/client-apigatewaymanagementapi';
|
|
12
12
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
13
13
|
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
type Nick,
|
|
17
|
-
type ParsedServerConfig,
|
|
18
|
-
createConnection,
|
|
19
|
-
} from '@serverless-ircd/irc-core';
|
|
14
|
+
import { type Nick, type ParsedServerConfig, createConnection } from '@serverless-ircd/irc-core';
|
|
15
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
20
16
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
21
17
|
import { AwsRuntime, type PostToConnection, cleanupConnection } from '../src/aws-runtime.js';
|
|
22
18
|
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
@@ -27,12 +23,11 @@ import type { TablesConfig } from '../src/tables.js';
|
|
|
27
23
|
const available = process.env.DDB_AVAILABLE === '1';
|
|
28
24
|
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
29
25
|
|
|
30
|
-
const SERVER_CONFIG: ParsedServerConfig = {
|
|
31
|
-
...DEFAULT_SERVER_CONFIG,
|
|
26
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
32
27
|
serverName: 'irc.example.com',
|
|
33
28
|
networkName: 'ExampleNet',
|
|
34
29
|
motdLines: [],
|
|
35
|
-
};
|
|
30
|
+
});
|
|
36
31
|
|
|
37
32
|
interface Fixture {
|
|
38
33
|
tables: TablesConfig;
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
10
10
|
import {
|
|
11
|
-
DEFAULT_SERVER_CONFIG,
|
|
12
11
|
InMemoryAccountStore,
|
|
13
12
|
type ParsedServerConfig,
|
|
14
13
|
StaticMotdProvider,
|
|
15
14
|
} from '@serverless-ircd/irc-core';
|
|
15
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
16
16
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
17
|
import { AwsRuntime } from '../src/aws-runtime.js';
|
|
18
18
|
import type { PostToConnection } from '../src/aws-runtime.js';
|
|
@@ -30,12 +30,11 @@ import type { TablesConfig } from '../src/tables.js';
|
|
|
30
30
|
const available = process.env.DDB_AVAILABLE === '1';
|
|
31
31
|
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
32
32
|
|
|
33
|
-
const SERVER_CONFIG: ParsedServerConfig = {
|
|
34
|
-
...DEFAULT_SERVER_CONFIG,
|
|
33
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
35
34
|
serverName: 'irc.example.com',
|
|
36
35
|
networkName: 'ExampleNet',
|
|
37
36
|
motdLines: ['Welcome to the test IRC server.'],
|
|
38
|
-
};
|
|
37
|
+
});
|
|
39
38
|
|
|
40
39
|
const MOTD = new StaticMotdProvider(SERVER_CONFIG.motdLines);
|
|
41
40
|
|
|
@@ -685,6 +684,93 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
685
684
|
// Never a 421 for REHASH.
|
|
686
685
|
expect(sink.some((l) => l.includes(' 421 ') && l.includes('REHASH'))).toBe(false);
|
|
687
686
|
});
|
|
687
|
+
|
|
688
|
+
it('refreshChannel updates cached state when a channel becomes empty (no topic)', async () => {
|
|
689
|
+
// After the last member PARTs, the channel snapshot returns 0
|
|
690
|
+
// members but the meta row persists. The next frame that
|
|
691
|
+
// references the channel triggers refreshChannel, which must
|
|
692
|
+
// take the empty-members + existing-cache path without throwing
|
|
693
|
+
// and leave the cache usable for subsequent joins.
|
|
694
|
+
await dispatch(
|
|
695
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'e1' } },
|
|
696
|
+
requireFx().deps,
|
|
697
|
+
);
|
|
698
|
+
requireFx().mgmt.register('e1', () => {});
|
|
699
|
+
await dispatch(
|
|
700
|
+
{
|
|
701
|
+
requestContext: { routeKey: '$default', connectionId: 'e1' },
|
|
702
|
+
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #empty\r\n',
|
|
703
|
+
},
|
|
704
|
+
requireFx().deps,
|
|
705
|
+
);
|
|
706
|
+
// PART drops the member count to 0 while the meta row survives.
|
|
707
|
+
await dispatch(
|
|
708
|
+
{
|
|
709
|
+
requestContext: { routeKey: '$default', connectionId: 'e1' },
|
|
710
|
+
body: 'PART #empty\r\n',
|
|
711
|
+
},
|
|
712
|
+
requireFx().deps,
|
|
713
|
+
);
|
|
714
|
+
// Re-JOIN forces prefetchChannelState → refreshChannel, hitting
|
|
715
|
+
// the empty-members + existing-cache branch. The JOIN must
|
|
716
|
+
// succeed (no exception), proving the cache is still consistent.
|
|
717
|
+
const res = await dispatch(
|
|
718
|
+
{
|
|
719
|
+
requestContext: { routeKey: '$default', connectionId: 'e1' },
|
|
720
|
+
body: 'JOIN #empty\r\n',
|
|
721
|
+
},
|
|
722
|
+
requireFx().deps,
|
|
723
|
+
);
|
|
724
|
+
expect(res.statusCode).toBe(200);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('refreshChannel preserves an existing topic when the snapshot has one', async () => {
|
|
728
|
+
// Same shape as the previous test, but a TOPIC is set before the
|
|
729
|
+
// PART so the snapshot's `topic` field is defined. This exercises
|
|
730
|
+
// the `if (snap.topic !== undefined) existing.topic = snap.topic`
|
|
731
|
+
// assignment in the empty-members path. The JOIN reducer does not
|
|
732
|
+
// emit 332 itself, so we query the topic explicitly via `TOPIC
|
|
733
|
+
// #chan` (no args) after re-JOIN — a 332 RPL_TOPIC reply proves
|
|
734
|
+
// the cache retained the topic through the empty refresh.
|
|
735
|
+
await dispatch(
|
|
736
|
+
{ requestContext: { routeKey: '$connect', connectionId: 't1' } },
|
|
737
|
+
requireFx().deps,
|
|
738
|
+
);
|
|
739
|
+
const sink: string[] = [];
|
|
740
|
+
requireFx().mgmt.register('t1', (data) => {
|
|
741
|
+
for (const line of data.split('\r\n')) {
|
|
742
|
+
if (line.length > 0) sink.push(line);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
await dispatch(
|
|
746
|
+
{
|
|
747
|
+
requestContext: { routeKey: '$default', connectionId: 't1' },
|
|
748
|
+
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #topicchan\r\nTOPIC #topicchan :hello\r\n',
|
|
749
|
+
},
|
|
750
|
+
requireFx().deps,
|
|
751
|
+
);
|
|
752
|
+
const sinkAfterFirstJoin = sink.length;
|
|
753
|
+
await dispatch(
|
|
754
|
+
{
|
|
755
|
+
requestContext: { routeKey: '$default', connectionId: 't1' },
|
|
756
|
+
body: 'PART #topicchan\r\n',
|
|
757
|
+
},
|
|
758
|
+
requireFx().deps,
|
|
759
|
+
);
|
|
760
|
+
// Re-JOIN triggers refreshChannel with snap.topic === 'hello',
|
|
761
|
+
// assigning existing.topic. The subsequent TOPIC query reads from
|
|
762
|
+
// the cache and emits 332 RPL_TOPIC.
|
|
763
|
+
const res = await dispatch(
|
|
764
|
+
{
|
|
765
|
+
requestContext: { routeKey: '$default', connectionId: 't1' },
|
|
766
|
+
body: 'JOIN #topicchan\r\nTOPIC #topicchan\r\n',
|
|
767
|
+
},
|
|
768
|
+
requireFx().deps,
|
|
769
|
+
);
|
|
770
|
+
expect(res.statusCode).toBe(200);
|
|
771
|
+
const newReplies = sink.slice(sinkAfterFirstJoin);
|
|
772
|
+
expect(newReplies.some((l) => l.includes(' 332 ') && l.includes('hello'))).toBe(true);
|
|
773
|
+
});
|
|
688
774
|
});
|
|
689
775
|
|
|
690
776
|
// -------------------------------------------------------------------------
|