From f115ea924145f73a5a07165ef717ff921482789f Mon Sep 17 00:00:00 2001
From: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
Date: Wed, 17 Apr 2024 22:01:04 +0200
Subject: [PATCH] Fixed formatting.
---
pnpm-lock.yaml | 91 +
src/.eslintrc.cjs | 237 +-
src/biome.json | 12 +
src/ep.json | 230 +-
src/locales/af.json | 5 +-
src/locales/ast.json | 5 +-
src/locales/awa.json | 5 +-
src/locales/azb.json | 8 +-
src/locales/bcc.json | 6 +-
src/locales/be-tarask.json | 7 +-
src/locales/bg.json | 6 +-
src/locales/bgn.json | 4 +-
src/locales/br.json | 8 +-
src/locales/bs.json | 7 +-
src/locales/dsb.json | 4 +-
src/locales/dty.json | 7 +-
src/locales/en.json | 322 +-
src/locales/et.json | 5 +-
src/locales/ff.json | 4 +-
src/locales/fo.json | 4 +-
src/locales/fy.json | 4 +-
src/locales/gl.json | 6 +-
src/locales/gu.json | 7 +-
src/locales/he.json | 8 +-
src/locales/hi.json | 4 +-
src/locales/hr.json | 6 +-
src/locales/hrx.json | 4 +-
src/locales/hsb.json | 4 +-
src/locales/hy.json | 5 +-
src/locales/ia.json | 4 +-
src/locales/id.json | 7 +-
src/locales/is.json | 5 +-
src/locales/kab.json | 4 +-
src/locales/km.json | 6 +-
src/locales/kn.json | 5 +-
src/locales/krc.json | 5 +-
src/locales/ksh.json | 4 +-
src/locales/lb.json | 7 +-
src/locales/lki.json | 6 +-
src/locales/lrc.json | 5 +-
src/locales/lv.json | 8 +-
src/locales/map-bms.json | 5 +-
src/locales/mg.json | 4 +-
src/locales/mk.json | 6 +-
src/locales/mn.json | 6 +-
src/locales/mnw.json | 5 +-
src/locales/mr.json | 6 +-
src/locales/ms.json | 6 +-
src/locales/my.json | 5 +-
src/locales/nah.json | 6 +-
src/locales/nap.json | 7 +-
src/locales/nds.json | 5 +-
src/locales/nn.json | 4 +-
src/locales/oc.json | 5 +-
src/locales/olo.json | 6 +-
src/locales/os.json | 4 +-
src/locales/pa.json | 8 +-
src/locales/pms.json | 4 +-
src/locales/ps.json | 4 +-
src/locales/sc.json | 4 +-
src/locales/sd.json | 7 +-
src/locales/sh.json | 5 +-
src/locales/shn.json | 6 +-
src/locales/skr-arab.json | 4 +-
src/locales/sms.json | 4 +-
src/locales/sq.json | 7 +-
src/locales/sro.json | 5 +-
src/locales/sw.json | 6 +-
src/locales/ta.json | 6 +-
src/locales/tcy.json | 5 +-
src/locales/th.json | 7 +-
src/locales/vec.json | 4 +-
src/node/db/API.ts | 769 +-
src/node/db/AuthorManager.ts | 325 +-
src/node/db/DB.ts | 54 +-
src/node/db/GroupManager.ts | 194 +-
src/node/db/Pad.ts | 1607 +-
src/node/db/PadManager.ts | 219 +-
src/node/db/ReadOnlyManager.ts | 48 +-
src/node/db/SecurityManager.ts | 210 +-
src/node/db/SessionManager.ts | 338 +-
src/node/db/SessionStore.ts | 199 +-
src/node/eejs/index.ts | 143 +-
src/node/handler/APIHandler.ts | 272 +-
src/node/handler/ExportHandler.ts | 166 +-
src/node/handler/ImportHandler.ts | 392 +-
src/node/handler/PadMessageHandler.ts | 2111 +-
src/node/handler/SocketIORouter.ts | 118 +-
src/node/hooks/express.ts | 463 +-
src/node/hooks/express/admin.ts | 36 +-
src/node/hooks/express/adminplugins.ts | 204 +-
src/node/hooks/express/adminsettings.ts | 385 +-
src/node/hooks/express/apicalls.ts | 79 +-
src/node/hooks/express/errorhandling.ts | 36 +-
src/node/hooks/express/importexport.ts | 185 +-
src/node/hooks/express/openapi.ts | 1349 +-
src/node/hooks/express/padurlsanitize.ts | 61 +-
src/node/hooks/express/socketio.ts | 270 +-
src/node/hooks/express/specialpages.ts | 222 +-
src/node/hooks/express/static.ts | 149 +-
src/node/hooks/express/tests.ts | 160 +-
src/node/hooks/express/webaccess.ts | 408 +-
src/node/hooks/i18n.ts | 245 +-
src/node/padaccess.ts | 42 +-
src/node/security/OAuth2Provider.ts | 576 +-
src/node/security/OAuth2User.ts | 8 +-
src/node/security/OIDCAdapter.ts | 179 +-
src/node/security/SecretRotator.ts | 505 +-
src/node/security/crypto.ts | 7 +-
src/node/server.ts | 482 +-
src/node/stats.ts | 8 +-
src/node/types/ArgsExpressType.ts | 8 +-
src/node/types/AsyncQueueTask.ts | 8 +-
src/node/types/ChangeSet.ts | 4 +-
src/node/types/DeriveModel.ts | 10 +-
src/node/types/ErrorCaused.ts | 21 +-
src/node/types/I18nPluginDefs.ts | 8 +-
src/node/types/LegacyParams.ts | 14 +-
src/node/types/MapType.ts | 8 +-
src/node/types/PackageInfo.ts | 35 +-
src/node/types/PadSearchQuery.ts | 23 +-
src/node/types/PadType.ts | 78 +-
src/node/types/PartType.ts | 14 +-
src/node/types/Plugin.ts | 13 +-
src/node/types/PromiseWithStd.ts | 12 +-
src/node/types/QueryType.ts | 8 +-
src/node/types/RunCMDOptions.ts | 20 +-
src/node/types/SecretRotatorType.ts | 4 +-
src/node/types/SettingsUser.ts | 10 +-
src/node/types/SocketAcknowledge.ts | 4 +-
src/node/types/SocketClientRequest.ts | 48 +-
src/node/types/SocketModule.ts | 4 +-
src/node/types/SwaggerUIResource.ts | 56 +-
src/node/types/UserSettingsObject.ts | 8 +-
src/node/types/WebAccessTypes.ts | 16 +-
src/node/utils/Abiword.ts | 147 +-
src/node/utils/AbsolutePaths.ts | 145 +-
src/node/utils/Cli.ts | 29 +-
src/node/utils/ExportEtherpad.ts | 99 +-
src/node/utils/ExportHelper.ts | 133 +-
src/node/utils/ExportHtml.ts | 934 +-
src/node/utils/ExportTxt.ts | 407 +-
src/node/utils/ImportEtherpad.ts | 210 +-
src/node/utils/ImportHtml.ts | 129 +-
src/node/utils/LibreOffice.ts | 184 +-
src/node/utils/Minify.js | 512 +-
src/node/utils/MinifyWorker.js | 43 +-
src/node/utils/NodeVersion.ts | 45 +-
src/node/utils/Settings.ts | 1019 +-
src/node/utils/SettingsTree.ts | 178 +-
src/node/utils/Stream.ts | 266 +-
src/node/utils/UpdateCheck.ts | 89 +-
src/node/utils/caching_middleware.ts | 286 +-
src/node/utils/checkValidRev.ts | 42 +-
src/node/utils/customError.ts | 23 +-
src/node/utils/padDiff.ts | 948 +-
src/node/utils/path_exists.ts | 21 +-
src/node/utils/promises.ts | 112 +-
src/node/utils/randomstring.ts | 6 +-
src/node/utils/run_cmd.ts | 230 +-
src/node/utils/sanitizePathname.ts | 36 +-
src/node/utils/tar.json | 198 +-
src/node/utils/toolbar.ts | 492 +-
src/package.json | 264 +-
src/playwright.config.ts | 132 +-
src/static/font/config.json | 1608 +-
src/static/js/AttributeManager.js | 590 +-
src/static/js/AttributeMap.js | 127 +-
src/static/js/AttributePool.js | 347 +-
src/static/js/Changeset.js | 3307 +-
src/static/js/ChangesetUtils.js | 48 +-
src/static/js/ChatMessage.js | 169 +-
src/static/js/ace.js | 529 +-
src/static/js/ace2_common.js | 41 +-
src/static/js/ace2_inner.js | 7465 +--
src/static/js/attributes.js | 63 +-
src/static/js/basic_error_handler.js | 74 +-
src/static/js/broadcast.js | 892 +-
src/static/js/broadcast_revisions.js | 163 +-
src/static/js/broadcast_slider.js | 586 +-
src/static/js/caretPosition.js | 288 +-
src/static/js/changesettracker.js | 346 +-
src/static/js/chat.js | 498 +-
src/static/js/collab_client.js | 868 +-
src/static/js/colorutils.js | 108 +-
src/static/js/contentcollector.js | 1297 +-
src/static/js/cssmanager.js | 82 +-
src/static/js/domline.js | 473 +-
src/static/js/index.js | 67 +-
src/static/js/l10n.js | 27 +-
src/static/js/linestylefilter.js | 453 +-
src/static/js/pad.js | 1316 +-
src/static/js/pad_automatic_reconnect.js | 247 +-
src/static/js/pad_connectionstatus.js | 118 +-
src/static/js/pad_cookie.js | 95 +-
src/static/js/pad_editbar.js | 846 +-
src/static/js/pad_editor.js | 366 +-
src/static/js/pad_impexp.js | 292 +-
src/static/js/pad_modals.js | 50 +-
src/static/js/pad_savedrevs.js | 29 +-
src/static/js/pad_userlist.js | 1100 +-
src/static/js/pad_utils.js | 822 +-
src/static/js/pluginfw/LinkInstaller.ts | 469 +-
src/static/js/pluginfw/client_plugins.js | 95 +-
src/static/js/pluginfw/hooks.js | 495 +-
src/static/js/pluginfw/installer.ts | 367 +-
src/static/js/pluginfw/plugin_defs.js | 2 -
src/static/js/pluginfw/plugins.js | 310 +-
src/static/js/pluginfw/shared.js | 145 +-
src/static/js/pluginfw/tsort.js | 152 +-
src/static/js/rjquery.js | 3 +-
src/static/js/scroll.js | 576 +-
src/static/js/security.js | 4 +-
src/static/js/skin_variants.js | 106 +-
src/static/js/skiplist.js | 572 +-
src/static/js/socketio.js | 62 +-
src/static/js/timeslider.js | 225 +-
src/static/js/underscore.js | 4 +-
src/static/js/undomodule.js | 500 +-
src/static/js/vendors/browser.js | 588 +-
src/static/js/vendors/farbtastic.js | 993 +-
src/static/js/vendors/gritter.js | 358 +-
src/static/js/vendors/html10n.js | 2072 +-
src/static/js/vendors/jquery.js | 10146 ++--
src/static/js/vendors/nice-select.js | 369 +-
src/static/skins/colibris/index.js | 8 +-
src/static/skins/colibris/pad.js | 12 +-
src/static/skins/colibris/timeslider.js | 5 +-
src/static/skins/no-skin/index.js | 8 +-
src/static/skins/no-skin/pad.js | 8 +-
src/static/skins/no-skin/timeslider.js | 8 +-
src/tests/backend/common.ts | 441 +-
src/tests/backend/fuzzImportTest.ts | 99 +-
src/tests/backend/specs/ExportEtherpad.ts | 116 +-
src/tests/backend/specs/ImportEtherpad.ts | 442 +-
src/tests/backend/specs/Pad.ts | 282 +-
src/tests/backend/specs/SecretRotator.ts | 1066 +-
src/tests/backend/specs/SessionStore.ts | 503 +-
src/tests/backend/specs/Stream.ts | 695 +-
src/tests/backend/specs/api/api.ts | 75 +-
.../backend/specs/api/characterEncoding.ts | 152 +-
src/tests/backend/specs/api/chat.ts | 224 +-
src/tests/backend/specs/api/importexport.ts | 517 +-
.../backend/specs/api/importexportGetPost.ts | 1510 +-
src/tests/backend/specs/api/instance.ts | 99 +-
src/tests/backend/specs/api/pad.ts | 1449 +-
.../backend/specs/api/restoreRevision.ts | 152 +-
.../backend/specs/api/sessionsAndGroups.ts | 779 +-
src/tests/backend/specs/caching_middleware.ts | 183 +-
src/tests/backend/specs/chat.ts | 319 +-
src/tests/backend/specs/contentcollector.ts | 678 +-
src/tests/backend/specs/crypto.ts | 11 +-
src/tests/backend/specs/export.ts | 41 +-
src/tests/backend/specs/favicon.ts | 196 +-
src/tests/backend/specs/health.ts | 96 +-
src/tests/backend/specs/hooks.ts | 2645 +-
src/tests/backend/specs/lowerCasePadIds.ts | 173 +-
src/tests/backend/specs/messages.ts | 475 +-
src/tests/backend/specs/pad_utils.ts | 70 +-
src/tests/backend/specs/pads-with-spaces.ts | 36 +-
src/tests/backend/specs/promises.ts | 167 +-
src/tests/backend/specs/regression-db.ts | 48 +-
src/tests/backend/specs/sanitizePathname.ts | 192 +-
src/tests/backend/specs/settings.ts | 176 +-
src/tests/backend/specs/socketio.ts | 950 +-
src/tests/backend/specs/specialpages.ts | 52 +-
src/tests/backend/specs/webaccess.ts | 1113 +-
src/tests/container/loadSettings.js | 36 +-
src/tests/container/specs/api/pad.js | 42 +-
.../admin-spec/adminsettings.spec.ts | 101 +-
.../admin-spec/admintroubleshooting.spec.ts | 63 +-
.../admin-spec/adminupdateplugins.spec.ts | 125 +-
src/tests/frontend-new/helper/adminhelper.ts | 58 +-
src/tests/frontend-new/helper/padHelper.ts | 234 +-
.../frontend-new/helper/settingsHelper.ts | 57 +-
src/tests/frontend-new/helper/timeslider.ts | 13 +-
src/tests/frontend-new/specs/alphabet.spec.ts | 53 +-
src/tests/frontend-new/specs/bold.spec.ts | 76 +-
.../specs/change_user_color.spec.ts | 199 +-
.../specs/change_user_name.spec.ts | 72 +-
src/tests/frontend-new/specs/chat.spec.ts | 211 +-
.../specs/clear_authorship_color.spec.ts | 174 +-
.../frontend-new/specs/collab_client.spec.ts | 187 +-
src/tests/frontend-new/specs/delete.spec.ts | 39 +-
.../frontend-new/specs/embed_value.spec.ts | 228 +-
src/tests/frontend-new/specs/enter.spec.ts | 126 +-
.../frontend-new/specs/font_type.spec.ts | 74 +-
.../frontend-new/specs/indentation.spec.ts | 489 +-
.../frontend-new/specs/inner_height.spec.ts | 116 +-
src/tests/frontend-new/specs/italic.spec.ts | 133 +-
src/tests/frontend-new/specs/language.spec.ts | 174 +-
.../frontend-new/specs/ordered_list.spec.ts | 188 +-
src/tests/frontend-new/specs/redo.spec.ts | 127 +-
.../frontend-new/specs/strikethrough.spec.ts | 62 +-
.../frontend-new/specs/timeslider.spec.ts | 57 +-
.../specs/timeslider_follow.spec.ts | 160 +-
src/tests/frontend-new/specs/undo.spec.ts | 108 +-
.../frontend-new/specs/unordered_list.spec.ts | 278 +-
.../specs/urls_become_clickable.spec.ts | 98 +-
src/tests/frontend/cypress/cypress.config.js | 14 +-
.../frontend/cypress/integration/test.js | 45 +-
src/tests/frontend/easysync-helper.js | 383 +-
src/tests/frontend/helper.js | 628 +-
src/tests/frontend/helper/methods.js | 179 +-
src/tests/frontend/helper/multipleUsers.js | 156 +-
src/tests/frontend/helper/ui.js | 81 +-
src/tests/frontend/lib/expect.js | 2619 +-
src/tests/frontend/lib/mocha.js | 41672 +++++++++-------
src/tests/frontend/lib/sendkeys.js | 842 +-
src/tests/frontend/lib/underscore.js | 2429 +-
src/tests/frontend/runner.js | 629 +-
src/tests/frontend/specs/AttributeMap.js | 341 +-
src/tests/frontend/specs/attributes.js | 701 +-
.../frontend/specs/authorship_of_editions.js | 182 +-
src/tests/frontend/specs/chat_hooks.js | 282 +-
.../frontend/specs/chat_load_messages.js | 145 +-
src/tests/frontend/specs/drag_and_drop.js | 286 +-
.../frontend/specs/easysync-assembler.js | 402 +-
src/tests/frontend/specs/easysync-compose.js | 95 +-
src/tests/frontend/specs/easysync-follow.js | 166 +-
.../frontend/specs/easysync-inverseRandom.js | 107 +-
.../frontend/specs/easysync-mutations.js | 680 +-
src/tests/frontend/specs/easysync-other.js | 341 +-
.../frontend/specs/easysync-subAttribution.js | 102 +-
src/tests/frontend/specs/helper.js | 937 +-
src/tests/frontend/specs/importexport.js | 1184 +-
src/tests/frontend/specs/importindents.js | 315 +-
...ultiple_authors_clear_authorship_colors.js | 66 +-
src/tests/frontend/specs/pad_modal.js | 172 +-
src/tests/frontend/specs/responsiveness.js | 130 +-
src/tests/frontend/specs/scrollTo.js | 70 +-
.../specs/select_formatting_buttons.js | 260 +-
src/tests/frontend/specs/skiplist.js | 88 +-
src/tests/frontend/specs/timeslider_labels.js | 96 +-
.../specs/timeslider_numeric_padID.js | 52 +-
.../frontend/specs/timeslider_revisions.js | 256 +-
src/tests/frontend/specs/xxauto_reconnect.js | 90 +-
src/tests/frontend/travis/remote_runner.js | 220 +-
src/tests/ratelimit/send_changesets.js | 30 +-
src/tsconfig.json | 32 +-
340 files changed, 77690 insertions(+), 66928 deletions(-)
create mode 100644 src/biome.json
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0d845c88a..4a625498a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -278,6 +278,9 @@ importers:
specifier: ^0.9.2
version: 0.9.2
devDependencies:
+ '@biomejs/biome':
+ specifier: 1.7.0
+ version: 1.7.0
'@playwright/test':
specifier: ^1.43.1
version: 1.43.1
@@ -721,6 +724,94 @@ packages:
to-fast-properties: 2.0.0
dev: true
+ /@biomejs/biome@1.7.0:
+ resolution: {integrity: sha512-mejiRhnAq6UrXtYvjWJUKdstcT58n0/FfKemFf3d2Ou0HxOdS88HQmWtQ/UgyZvOEPD572YbFTb6IheyROpqkw==}
+ engines: {node: '>=14.21.3'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@biomejs/cli-darwin-arm64': 1.7.0
+ '@biomejs/cli-darwin-x64': 1.7.0
+ '@biomejs/cli-linux-arm64': 1.7.0
+ '@biomejs/cli-linux-arm64-musl': 1.7.0
+ '@biomejs/cli-linux-x64': 1.7.0
+ '@biomejs/cli-linux-x64-musl': 1.7.0
+ '@biomejs/cli-win32-arm64': 1.7.0
+ '@biomejs/cli-win32-x64': 1.7.0
+ dev: true
+
+ /@biomejs/cli-darwin-arm64@1.7.0:
+ resolution: {integrity: sha512-12TaeaKHU4SAZt0fQJ2bYk1jUb4foope7LmgDE5p3c0uMxd3mFkg1k7G721T+K6UHYULcSOQDsNNM8DhYi8Irg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@biomejs/cli-darwin-x64@1.7.0:
+ resolution: {integrity: sha512-6Qq1BSIB0cpp0cQNqO/+EiUV7FE3jMpF6w7+AgIBXp0oJxUWb2Ff0RDZdO9bfzkimXD58j0vGpNHMGnCcjDV2Q==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@biomejs/cli-linux-arm64-musl@1.7.0:
+ resolution: {integrity: sha512-pwIY80nU7SAxrVVZ6HD9ah1pruwh9ZqlSR0Nvbg4ZJqQa0POhiB+RJx7+/1Ml2mTZdrl8kb/YiwQpD16uwb5wg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@biomejs/cli-linux-arm64@1.7.0:
+ resolution: {integrity: sha512-GwSci7xBJ2j1CrdDXDUVXnUtrvypEz/xmiYPpFeVdlX5p95eXx+7FekPPbJfhGGw5WKSsKZ+V8AAlbN+kUwJWw==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@biomejs/cli-linux-x64-musl@1.7.0:
+ resolution: {integrity: sha512-KzCA0mW4LSbCd7XZWaEJvTOTTBjfJoVEXkfq1fsXxww1HB+ww5PGMbhbIcbYCsj2CTJUifeD5hOkyuBVppU1xQ==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@biomejs/cli-linux-x64@1.7.0:
+ resolution: {integrity: sha512-1y+odKQsyHcw0JCGRuqhbx7Y6jxOVSh4lGIVDdJxW1b55yD22DY1kcMEfhUte6f95OIc2uqfkwtiI6xQAiZJdw==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@biomejs/cli-win32-arm64@1.7.0:
+ resolution: {integrity: sha512-AvLDUYZBpOUFgS/mni4VruIoVV3uSGbKSkZQBPXsHgL0w4KttLll3NBrVanmWxOHsom6C6ocHLyfAY8HUc8TXg==}
+ engines: {node: '>=14.21.3'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@biomejs/cli-win32-x64@1.7.0:
+ resolution: {integrity: sha512-Pylm00BAAuLVb40IH9PC17432BTsY8K4pSUvhvgR1eaalnMaD6ug9SYJTTzKDbT6r24MPAGCTiSZERyhGkGzFQ==}
+ engines: {node: '>=14.21.3'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@docsearch/css@3.6.0:
resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==}
dev: true
diff --git a/src/.eslintrc.cjs b/src/.eslintrc.cjs
index 03d432ede..db21428d1 100644
--- a/src/.eslintrc.cjs
+++ b/src/.eslintrc.cjs
@@ -1,139 +1,108 @@
-'use strict';
+"use strict";
// This is a workaround for https://github.com/eslint/eslint/issues/3458
-require('eslint-config-etherpad/patch/modern-module-resolution');
+require("eslint-config-etherpad/patch/modern-module-resolution");
module.exports = {
- ignorePatterns: [
- '/static/js/vendors/browser.js',
- '/static/js/vendors/farbtastic.js',
- '/static/js/vendors/gritter.js',
- '/static/js/vendors/html10n.js',
- '/static/js/vendors/jquery.js',
- '/static/js/vendors/nice-select.js',
- '/tests/frontend/lib/',
- ],
- overrides: [
- {
- files: [
- '**/.eslintrc.*',
- ],
- extends: 'etherpad/node',
- },
- {
- files: [
- '**/*',
- ],
- excludedFiles: [
- '**/.eslintrc.*',
- 'tests/frontend/**/*',
- ],
- extends: 'etherpad/node',
- },
- {
- files: [
- 'static/**/*',
- 'tests/frontend/helper.js',
- 'tests/frontend/helper/**/*',
- ],
- excludedFiles: [
- '**/.eslintrc.*',
- ],
- extends: 'etherpad/browser',
- env: {
- 'shared-node-browser': true,
- },
- overrides: [
- {
- files: [
- 'tests/frontend/helper/**/*',
- ],
- globals: {
- helper: 'readonly',
- },
- },
- ],
- },
- {
- files: [
- 'tests/**/*',
- ],
- excludedFiles: [
- '**/.eslintrc.*',
- 'tests/frontend/cypress/**/*',
- 'tests/frontend/helper.js',
- 'tests/frontend/helper/**/*',
- 'tests/frontend/travis/**/*',
- 'tests/ratelimit/**/*',
- ],
- extends: 'etherpad/tests',
- rules: {
- 'mocha/no-exports': 'off',
- 'mocha/no-top-level-hooks': 'off',
- },
- },
- {
- files: [
- 'tests/backend/**/*',
- ],
- excludedFiles: [
- '**/.eslintrc.*',
- ],
- extends: 'etherpad/tests/backend',
- overrides: [
- {
- files: [
- 'tests/backend/**/*',
- ],
- excludedFiles: [
- 'tests/backend/specs/**/*',
- ],
- rules: {
- 'mocha/no-exports': 'off',
- 'mocha/no-top-level-hooks': 'off',
- },
- },
- ],
- },
- {
- files: [
- 'tests/frontend/**/*',
- ],
- excludedFiles: [
- '**/.eslintrc.*',
- 'tests/frontend/cypress/**/*',
- 'tests/frontend/helper.js',
- 'tests/frontend/helper/**/*',
- 'tests/frontend/travis/**/*',
- ],
- extends: 'etherpad/tests/frontend',
- overrides: [
- {
- files: [
- 'tests/frontend/**/*',
- ],
- excludedFiles: [
- 'tests/frontend/specs/**/*',
- ],
- rules: {
- 'mocha/no-exports': 'off',
- 'mocha/no-top-level-hooks': 'off',
- },
- },
- ],
- },
- {
- files: [
- 'tests/frontend/cypress/**/*',
- ],
- extends: 'etherpad/tests/cypress',
- },
- {
- files: [
- 'tests/frontend/travis/**/*',
- ],
- extends: 'etherpad/node',
- },
- ],
- root: true,
+ ignorePatterns: [
+ "/static/js/vendors/browser.js",
+ "/static/js/vendors/farbtastic.js",
+ "/static/js/vendors/gritter.js",
+ "/static/js/vendors/html10n.js",
+ "/static/js/vendors/jquery.js",
+ "/static/js/vendors/nice-select.js",
+ "/tests/frontend/lib/",
+ ],
+ overrides: [
+ {
+ files: ["**/.eslintrc.*"],
+ extends: "etherpad/node",
+ },
+ {
+ files: ["**/*"],
+ excludedFiles: ["**/.eslintrc.*", "tests/frontend/**/*"],
+ extends: "etherpad/node",
+ },
+ {
+ files: [
+ "static/**/*",
+ "tests/frontend/helper.js",
+ "tests/frontend/helper/**/*",
+ ],
+ excludedFiles: ["**/.eslintrc.*"],
+ extends: "etherpad/browser",
+ env: {
+ "shared-node-browser": true,
+ },
+ overrides: [
+ {
+ files: ["tests/frontend/helper/**/*"],
+ globals: {
+ helper: "readonly",
+ },
+ },
+ ],
+ },
+ {
+ files: ["tests/**/*"],
+ excludedFiles: [
+ "**/.eslintrc.*",
+ "tests/frontend/cypress/**/*",
+ "tests/frontend/helper.js",
+ "tests/frontend/helper/**/*",
+ "tests/frontend/travis/**/*",
+ "tests/ratelimit/**/*",
+ ],
+ extends: "etherpad/tests",
+ rules: {
+ "mocha/no-exports": "off",
+ "mocha/no-top-level-hooks": "off",
+ },
+ },
+ {
+ files: ["tests/backend/**/*"],
+ excludedFiles: ["**/.eslintrc.*"],
+ extends: "etherpad/tests/backend",
+ overrides: [
+ {
+ files: ["tests/backend/**/*"],
+ excludedFiles: ["tests/backend/specs/**/*"],
+ rules: {
+ "mocha/no-exports": "off",
+ "mocha/no-top-level-hooks": "off",
+ },
+ },
+ ],
+ },
+ {
+ files: ["tests/frontend/**/*"],
+ excludedFiles: [
+ "**/.eslintrc.*",
+ "tests/frontend/cypress/**/*",
+ "tests/frontend/helper.js",
+ "tests/frontend/helper/**/*",
+ "tests/frontend/travis/**/*",
+ ],
+ extends: "etherpad/tests/frontend",
+ overrides: [
+ {
+ files: ["tests/frontend/**/*"],
+ excludedFiles: ["tests/frontend/specs/**/*"],
+ rules: {
+ "mocha/no-exports": "off",
+ "mocha/no-top-level-hooks": "off",
+ },
+ },
+ ],
+ },
+ {
+ files: ["tests/frontend/cypress/**/*"],
+ extends: "etherpad/tests/cypress",
+ },
+ {
+ files: ["tests/frontend/travis/**/*"],
+ extends: "etherpad/node",
+ },
+ ],
+ root: true,
};
diff --git a/src/biome.json b/src/biome.json
new file mode 100644
index 000000000..9c8fb4a31
--- /dev/null
+++ b/src/biome.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
+ "organizeImports": {
+ "enabled": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ }
+}
diff --git a/src/ep.json b/src/ep.json
index 95cb4135e..090bca081 100644
--- a/src/ep.json
+++ b/src/ep.json
@@ -1,117 +1,117 @@
{
- "parts": [
- {
- "name": "DB",
- "hooks": {
- "shutdown": "ep_etherpad-lite/node/db/DB"
- }
- },
- {
- "name": "Minify",
- "hooks": {
- "shutdown": "ep_etherpad-lite/node/utils/Minify"
- }
- },
- {
- "name": "express",
- "hooks": {
- "createServer": "ep_etherpad-lite/node/hooks/express",
- "restartServer": "ep_etherpad-lite/node/hooks/express",
- "shutdown": "ep_etherpad-lite/node/hooks/express"
- }
- },
- {
- "name": "static",
- "hooks": {
- "expressPreSession": "ep_etherpad-lite/node/hooks/express/static"
- }
- },
- {
- "name": "stats",
- "hooks": {
- "shutdown": "ep_etherpad-lite/node/stats"
- }
- },
- {
- "name": "i18n",
- "hooks": {
- "expressPreSession": "ep_etherpad-lite/node/hooks/i18n"
- }
- },
- {
- "name": "specialpages",
- "hooks": {
- "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
- "expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages"
- }
- },
- {
- "name": "oauth2",
- "hooks": {
- "expressCreateServer": "ep_etherpad-lite/node/security/OAuth2Provider"
- }
- },
- {
- "name": "padurlsanitize",
- "hooks": {
- "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize"
- }
- },
- {
- "name": "apicalls",
- "hooks": {
- "expressPreSession": "ep_etherpad-lite/node/hooks/express/apicalls"
- }
- },
- {
- "name": "importexport",
- "hooks": {
- "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport"
- }
- },
- {
- "name": "errorhandling",
- "hooks": {
- "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
- }
- },
- {
- "name": "socketio",
- "hooks": {
- "expressCloseServer": "ep_etherpad-lite/node/hooks/express/socketio",
- "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio",
- "socketio": "ep_etherpad-lite/node/handler/PadMessageHandler"
- }
- },
- {
- "name": "tests",
- "hooks": {
- "expressPreSession": "ep_etherpad-lite/node/hooks/express/tests"
- }
- },
- {
- "name": "admin",
- "hooks": {
- "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin"
- }
- },
- {
- "name": "adminplugins",
- "hooks": {
- "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins"
- }
- },
- {
- "name": "adminsettings",
- "hooks": {
- "socketio": "ep_etherpad-lite/node/hooks/express/adminsettings"
- }
- },
- {
- "name": "openapi",
- "hooks": {
- "expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
- }
- }
- ]
+ "parts": [
+ {
+ "name": "DB",
+ "hooks": {
+ "shutdown": "ep_etherpad-lite/node/db/DB"
+ }
+ },
+ {
+ "name": "Minify",
+ "hooks": {
+ "shutdown": "ep_etherpad-lite/node/utils/Minify"
+ }
+ },
+ {
+ "name": "express",
+ "hooks": {
+ "createServer": "ep_etherpad-lite/node/hooks/express",
+ "restartServer": "ep_etherpad-lite/node/hooks/express",
+ "shutdown": "ep_etherpad-lite/node/hooks/express"
+ }
+ },
+ {
+ "name": "static",
+ "hooks": {
+ "expressPreSession": "ep_etherpad-lite/node/hooks/express/static"
+ }
+ },
+ {
+ "name": "stats",
+ "hooks": {
+ "shutdown": "ep_etherpad-lite/node/stats"
+ }
+ },
+ {
+ "name": "i18n",
+ "hooks": {
+ "expressPreSession": "ep_etherpad-lite/node/hooks/i18n"
+ }
+ },
+ {
+ "name": "specialpages",
+ "hooks": {
+ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages",
+ "expressPreSession": "ep_etherpad-lite/node/hooks/express/specialpages"
+ }
+ },
+ {
+ "name": "oauth2",
+ "hooks": {
+ "expressCreateServer": "ep_etherpad-lite/node/security/OAuth2Provider"
+ }
+ },
+ {
+ "name": "padurlsanitize",
+ "hooks": {
+ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize"
+ }
+ },
+ {
+ "name": "apicalls",
+ "hooks": {
+ "expressPreSession": "ep_etherpad-lite/node/hooks/express/apicalls"
+ }
+ },
+ {
+ "name": "importexport",
+ "hooks": {
+ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/importexport"
+ }
+ },
+ {
+ "name": "errorhandling",
+ "hooks": {
+ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
+ }
+ },
+ {
+ "name": "socketio",
+ "hooks": {
+ "expressCloseServer": "ep_etherpad-lite/node/hooks/express/socketio",
+ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/socketio",
+ "socketio": "ep_etherpad-lite/node/handler/PadMessageHandler"
+ }
+ },
+ {
+ "name": "tests",
+ "hooks": {
+ "expressPreSession": "ep_etherpad-lite/node/hooks/express/tests"
+ }
+ },
+ {
+ "name": "admin",
+ "hooks": {
+ "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin"
+ }
+ },
+ {
+ "name": "adminplugins",
+ "hooks": {
+ "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins"
+ }
+ },
+ {
+ "name": "adminsettings",
+ "hooks": {
+ "socketio": "ep_etherpad-lite/node/hooks/express/adminsettings"
+ }
+ },
+ {
+ "name": "openapi",
+ "hooks": {
+ "expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
+ }
+ }
+ ]
}
diff --git a/src/locales/af.json b/src/locales/af.json
index 5b41aee10..4a8378e0b 100644
--- a/src/locales/af.json
+++ b/src/locales/af.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Fwolff",
- "Naudefj"
- ]
+ "authors": ["Fwolff", "Naudefj"]
},
"index.newPad": "Nuwe pad",
"index.createOpenPad": "of skep/open 'n pad met die naam:",
diff --git a/src/locales/ast.json b/src/locales/ast.json
index b46964bf4..052d320da 100644
--- a/src/locales/ast.json
+++ b/src/locales/ast.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Xuacu",
- "YoaR"
- ]
+ "authors": ["Xuacu", "YoaR"]
},
"index.newPad": "Nuevu bloc",
"index.createOpenPad": "o crear/abrir un bloc col nome:",
diff --git a/src/locales/awa.json b/src/locales/awa.json
index 947cde330..ce79873f2 100644
--- a/src/locales/awa.json
+++ b/src/locales/awa.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "1AnuraagPandey",
- "बडा काजी"
- ]
+ "authors": ["1AnuraagPandey", "बडा काजी"]
},
"index.newPad": "नयाँ प्याड",
"pad.toolbar.bold.title": "मोट (Ctrl-B)",
diff --git a/src/locales/azb.json b/src/locales/azb.json
index 812d38f66..2e67a265c 100644
--- a/src/locales/azb.json
+++ b/src/locales/azb.json
@@ -1,12 +1,6 @@
{
"@metadata": {
- "authors": [
- "Alp Er Tunqa",
- "Amir a57",
- "Ilğım",
- "Koroğlu",
- "Mousa"
- ]
+ "authors": ["Alp Er Tunqa", "Amir a57", "Ilğım", "Koroğlu", "Mousa"]
},
"index.newPad": "یئنی یادداشت دفترچه سی",
"index.createOpenPad": "یا دا ایجاد /بیر پد آدلا برابر آچماق:",
diff --git a/src/locales/bcc.json b/src/locales/bcc.json
index d56dbea35..eb231a4e3 100644
--- a/src/locales/bcc.json
+++ b/src/locales/bcc.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Baloch Afghanistan",
- "Moshtank",
- "Sultanselim baloch"
- ]
+ "authors": ["Baloch Afghanistan", "Moshtank", "Sultanselim baloch"]
},
"admin.page-title": "کارمسترءِ کُرسی - اترپَد",
"admin_plugins": "گݔشانکانءِ کار ءُ بار",
diff --git a/src/locales/be-tarask.json b/src/locales/be-tarask.json
index b785fb9cc..519251c67 100644
--- a/src/locales/be-tarask.json
+++ b/src/locales/be-tarask.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Jim-by",
- "Red Winged Duck",
- "Renessaince",
- "Wizardist"
- ]
+ "authors": ["Jim-by", "Red Winged Duck", "Renessaince", "Wizardist"]
},
"admin.page-title": "Адміністрацыйная панэль — Etherpad",
"admin_plugins": "Кіраўнік плагінаў",
diff --git a/src/locales/bg.json b/src/locales/bg.json
index f643ecfb8..dd22ff89c 100644
--- a/src/locales/bg.json
+++ b/src/locales/bg.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "StanProg",
- "Vlad5250",
- "Vodnokon4e"
- ]
+ "authors": ["StanProg", "Vlad5250", "Vodnokon4e"]
},
"index.newPad": "Нов пад",
"index.createOpenPad": "или създаване/отваряне на пад с име:",
diff --git a/src/locales/bgn.json b/src/locales/bgn.json
index 0bb468cf1..eefdd3b1f 100644
--- a/src/locales/bgn.json
+++ b/src/locales/bgn.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Baloch Afghanistan"
- ]
+ "authors": ["Baloch Afghanistan"]
},
"index.newPad": "یاداشتی نوکین کتابچه",
"index.createOpenPad": "یا جوڑ\t کورتین/پاچ کورتین یک کتابچه ئی یاداشتی بی نام:",
diff --git a/src/locales/br.json b/src/locales/br.json
index d3c33202d..ab0c4dbe1 100644
--- a/src/locales/br.json
+++ b/src/locales/br.json
@@ -1,12 +1,6 @@
{
"@metadata": {
- "authors": [
- "Fohanno",
- "Fulup",
- "Gwenn-Ael",
- "Huñvreüs",
- "Y-M D"
- ]
+ "authors": ["Fohanno", "Fulup", "Gwenn-Ael", "Huñvreüs", "Y-M D"]
},
"index.newPad": "Pad nevez",
"index.createOpenPad": "pe krouiñ/digeriñ ur Pad gant an anv :",
diff --git a/src/locales/bs.json b/src/locales/bs.json
index 55f9fb966..5197acd4c 100644
--- a/src/locales/bs.json
+++ b/src/locales/bs.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Edinwiki",
- "Semina x",
- "Srdjan m",
- "Srđan"
- ]
+ "authors": ["Edinwiki", "Semina x", "Srdjan m", "Srđan"]
},
"index.newPad": "Novi Pad",
"index.createOpenPad": "ili napravite/otvorite Pad sa imenom:",
diff --git a/src/locales/dsb.json b/src/locales/dsb.json
index cedac2f88..7930257c7 100644
--- a/src/locales/dsb.json
+++ b/src/locales/dsb.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Michawiki"
- ]
+ "authors": ["Michawiki"]
},
"admin.page-title": "Administratorowa delka – Etherpad",
"admin_plugins": "Zastojnik tykacow",
diff --git a/src/locales/dty.json b/src/locales/dty.json
index b0dd53568..259a65b76 100644
--- a/src/locales/dty.json
+++ b/src/locales/dty.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Nirajan pant",
- "बडा काजी",
- "रमेश सिंह बोहरा",
- "राम प्रसाद जोशी"
- ]
+ "authors": ["Nirajan pant", "बडा काजी", "रमेश सिंह बोहरा", "राम प्रसाद जोशी"]
},
"index.newPad": "नौलो प्याड",
"index.createOpenPad": "नाउँ सहितको नौलो प्याड सिर्जना गद्य्या / खोल्ल्या :",
diff --git a/src/locales/en.json b/src/locales/en.json
index 5737fab00..11800f628 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1,187 +1,187 @@
{
- "admin.page-title": "Admin Dashboard - Etherpad",
- "admin_plugins": "Plugin manager",
- "admin_plugins.available": "Available plugins",
- "admin_plugins.available_not-found": "No plugins found.",
- "admin_plugins.available_fetching": "Fetching…",
- "admin_plugins.available_install.value": "Install",
- "admin_plugins.available_search.placeholder": "Search for plugins to install",
- "admin_plugins.description": "Description",
- "admin_plugins.installed": "Installed plugins",
- "admin_plugins.installed_fetching": "Fetching installed plugins…",
- "admin_plugins.installed_nothing": "You haven't installed any plugins yet.",
- "admin_plugins.installed_uninstall.value": "Uninstall",
- "admin_plugins.last-update": "Last update",
- "admin_plugins.name": "Name",
- "admin_plugins.page-title": "Plugin manager - Etherpad",
- "admin_plugins.version": "Version",
- "admin_plugins_info": "Troubleshooting information",
- "admin_plugins_info.hooks": "Installed hooks",
- "admin_plugins_info.hooks_client": "Client-side hooks",
- "admin_plugins_info.hooks_server": "Server-side hooks",
- "admin_plugins_info.parts": "Installed parts",
- "admin_plugins_info.plugins": "Installed plugins",
- "admin_plugins_info.page-title": "Plugin information - Etherpad",
- "admin_plugins_info.version": "Etherpad version",
- "admin_plugins_info.version_latest": "Latest available version",
- "admin_plugins_info.version_number": "Version number",
- "admin_settings": "Settings",
- "admin_settings.current": "Current configuration",
- "admin_settings.current_example-devel": "Example development settings template",
- "admin_settings.current_example-prod": "Example production settings template",
- "admin_settings.current_restart.value": "Restart Etherpad",
- "admin_settings.current_save.value": "Save Settings",
- "admin_settings.page-title": "Settings - Etherpad",
+ "admin.page-title": "Admin Dashboard - Etherpad",
+ "admin_plugins": "Plugin manager",
+ "admin_plugins.available": "Available plugins",
+ "admin_plugins.available_not-found": "No plugins found.",
+ "admin_plugins.available_fetching": "Fetching…",
+ "admin_plugins.available_install.value": "Install",
+ "admin_plugins.available_search.placeholder": "Search for plugins to install",
+ "admin_plugins.description": "Description",
+ "admin_plugins.installed": "Installed plugins",
+ "admin_plugins.installed_fetching": "Fetching installed plugins…",
+ "admin_plugins.installed_nothing": "You haven't installed any plugins yet.",
+ "admin_plugins.installed_uninstall.value": "Uninstall",
+ "admin_plugins.last-update": "Last update",
+ "admin_plugins.name": "Name",
+ "admin_plugins.page-title": "Plugin manager - Etherpad",
+ "admin_plugins.version": "Version",
+ "admin_plugins_info": "Troubleshooting information",
+ "admin_plugins_info.hooks": "Installed hooks",
+ "admin_plugins_info.hooks_client": "Client-side hooks",
+ "admin_plugins_info.hooks_server": "Server-side hooks",
+ "admin_plugins_info.parts": "Installed parts",
+ "admin_plugins_info.plugins": "Installed plugins",
+ "admin_plugins_info.page-title": "Plugin information - Etherpad",
+ "admin_plugins_info.version": "Etherpad version",
+ "admin_plugins_info.version_latest": "Latest available version",
+ "admin_plugins_info.version_number": "Version number",
+ "admin_settings": "Settings",
+ "admin_settings.current": "Current configuration",
+ "admin_settings.current_example-devel": "Example development settings template",
+ "admin_settings.current_example-prod": "Example production settings template",
+ "admin_settings.current_restart.value": "Restart Etherpad",
+ "admin_settings.current_save.value": "Save Settings",
+ "admin_settings.page-title": "Settings - Etherpad",
- "index.newPad": "New Pad",
- "index.createOpenPad": "or create/open a Pad with the name:",
- "index.openPad": "open an existing Pad with the name:",
+ "index.newPad": "New Pad",
+ "index.createOpenPad": "or create/open a Pad with the name:",
+ "index.openPad": "open an existing Pad with the name:",
- "pad.toolbar.bold.title": "Bold (Ctrl+B)",
- "pad.toolbar.italic.title": "Italic (Ctrl+I)",
- "pad.toolbar.underline.title": "Underline (Ctrl+U)",
- "pad.toolbar.strikethrough.title": "Strikethrough (Ctrl+5)",
- "pad.toolbar.ol.title": "Ordered list (Ctrl+Shift+N)",
- "pad.toolbar.ul.title": "Unordered List (Ctrl+Shift+L)",
- "pad.toolbar.indent.title": "Indent (TAB)",
- "pad.toolbar.unindent.title": "Outdent (Shift+TAB)",
- "pad.toolbar.undo.title": "Undo (Ctrl+Z)",
- "pad.toolbar.redo.title": "Redo (Ctrl+Y)",
- "pad.toolbar.clearAuthorship.title": "Clear Authorship Colors (Ctrl+Shift+C)",
- "pad.toolbar.import_export.title": "Import/Export from/to different file formats",
- "pad.toolbar.timeslider.title": "Timeslider",
- "pad.toolbar.savedRevision.title": "Save Revision",
- "pad.toolbar.settings.title": "Settings",
- "pad.toolbar.embed.title": "Share and Embed this pad",
- "pad.toolbar.showusers.title": "Show the users on this pad",
+ "pad.toolbar.bold.title": "Bold (Ctrl+B)",
+ "pad.toolbar.italic.title": "Italic (Ctrl+I)",
+ "pad.toolbar.underline.title": "Underline (Ctrl+U)",
+ "pad.toolbar.strikethrough.title": "Strikethrough (Ctrl+5)",
+ "pad.toolbar.ol.title": "Ordered list (Ctrl+Shift+N)",
+ "pad.toolbar.ul.title": "Unordered List (Ctrl+Shift+L)",
+ "pad.toolbar.indent.title": "Indent (TAB)",
+ "pad.toolbar.unindent.title": "Outdent (Shift+TAB)",
+ "pad.toolbar.undo.title": "Undo (Ctrl+Z)",
+ "pad.toolbar.redo.title": "Redo (Ctrl+Y)",
+ "pad.toolbar.clearAuthorship.title": "Clear Authorship Colors (Ctrl+Shift+C)",
+ "pad.toolbar.import_export.title": "Import/Export from/to different file formats",
+ "pad.toolbar.timeslider.title": "Timeslider",
+ "pad.toolbar.savedRevision.title": "Save Revision",
+ "pad.toolbar.settings.title": "Settings",
+ "pad.toolbar.embed.title": "Share and Embed this pad",
+ "pad.toolbar.showusers.title": "Show the users on this pad",
- "pad.colorpicker.save": "Save",
- "pad.colorpicker.cancel": "Cancel",
+ "pad.colorpicker.save": "Save",
+ "pad.colorpicker.cancel": "Cancel",
- "pad.loading": "Loading...",
- "pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
- "pad.permissionDenied": "You do not have permission to access this pad",
+ "pad.loading": "Loading...",
+ "pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
+ "pad.permissionDenied": "You do not have permission to access this pad",
- "pad.settings.padSettings": "Pad Settings",
- "pad.settings.myView": "My View",
- "pad.settings.stickychat": "Chat always on screen",
- "pad.settings.chatandusers": "Show Chat and Users",
- "pad.settings.colorcheck": "Authorship colors",
- "pad.settings.linenocheck": "Line numbers",
- "pad.settings.rtlcheck": "Read content from right to left?",
- "pad.settings.fontType": "Font type:",
- "pad.settings.fontType.normal": "Normal",
- "pad.settings.language": "Language:",
- "pad.settings.about": "About",
- "pad.settings.poweredBy": "Powered by",
+ "pad.settings.padSettings": "Pad Settings",
+ "pad.settings.myView": "My View",
+ "pad.settings.stickychat": "Chat always on screen",
+ "pad.settings.chatandusers": "Show Chat and Users",
+ "pad.settings.colorcheck": "Authorship colors",
+ "pad.settings.linenocheck": "Line numbers",
+ "pad.settings.rtlcheck": "Read content from right to left?",
+ "pad.settings.fontType": "Font type:",
+ "pad.settings.fontType.normal": "Normal",
+ "pad.settings.language": "Language:",
+ "pad.settings.about": "About",
+ "pad.settings.poweredBy": "Powered by",
- "pad.importExport.import_export": "Import/Export",
- "pad.importExport.import": "Upload any text file or document",
- "pad.importExport.importSuccessful": "Successful!",
- "pad.importExport.export": "Export current pad as:",
- "pad.importExport.exportetherpad": "Etherpad",
- "pad.importExport.exporthtml": "HTML",
- "pad.importExport.exportplain": "Plain text",
- "pad.importExport.exportword": "Microsoft Word",
- "pad.importExport.exportpdf": "PDF",
- "pad.importExport.exportopen": "ODF (Open Document Format)",
- "pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please install AbiWord or LibreOffice.",
+ "pad.importExport.import_export": "Import/Export",
+ "pad.importExport.import": "Upload any text file or document",
+ "pad.importExport.importSuccessful": "Successful!",
+ "pad.importExport.export": "Export current pad as:",
+ "pad.importExport.exportetherpad": "Etherpad",
+ "pad.importExport.exporthtml": "HTML",
+ "pad.importExport.exportplain": "Plain text",
+ "pad.importExport.exportword": "Microsoft Word",
+ "pad.importExport.exportpdf": "PDF",
+ "pad.importExport.exportopen": "ODF (Open Document Format)",
+ "pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please install AbiWord or LibreOffice.",
- "pad.modals.connected": "Connected.",
- "pad.modals.reconnecting": "Reconnecting to your pad…",
- "pad.modals.forcereconnect": "Force reconnect",
- "pad.modals.reconnecttimer": "Trying to reconnect in",
- "pad.modals.cancel": "Cancel",
+ "pad.modals.connected": "Connected.",
+ "pad.modals.reconnecting": "Reconnecting to your pad…",
+ "pad.modals.forcereconnect": "Force reconnect",
+ "pad.modals.reconnecttimer": "Trying to reconnect in",
+ "pad.modals.cancel": "Cancel",
- "pad.modals.userdup": "Opened in another window",
- "pad.modals.userdup.explanation": "This pad seems to be opened in more than one browser window on this computer.",
- "pad.modals.userdup.advice": "Reconnect to use this window instead.",
+ "pad.modals.userdup": "Opened in another window",
+ "pad.modals.userdup.explanation": "This pad seems to be opened in more than one browser window on this computer.",
+ "pad.modals.userdup.advice": "Reconnect to use this window instead.",
- "pad.modals.unauth": "Not authorized",
- "pad.modals.unauth.explanation": "Your permissions have changed while viewing this page. Try to reconnect.",
+ "pad.modals.unauth": "Not authorized",
+ "pad.modals.unauth.explanation": "Your permissions have changed while viewing this page. Try to reconnect.",
- "pad.modals.looping.explanation": "There are communication problems with the synchronization server.",
- "pad.modals.looping.cause": "Perhaps you connected through an incompatible firewall or proxy.",
+ "pad.modals.looping.explanation": "There are communication problems with the synchronization server.",
+ "pad.modals.looping.cause": "Perhaps you connected through an incompatible firewall or proxy.",
- "pad.modals.initsocketfail": "Server is unreachable.",
- "pad.modals.initsocketfail.explanation": "Couldn't connect to the synchronization server.",
- "pad.modals.initsocketfail.cause": "This is probably due to a problem with your browser or your internet connection.",
+ "pad.modals.initsocketfail": "Server is unreachable.",
+ "pad.modals.initsocketfail.explanation": "Couldn't connect to the synchronization server.",
+ "pad.modals.initsocketfail.cause": "This is probably due to a problem with your browser or your internet connection.",
- "pad.modals.slowcommit.explanation": "The server is not responding.",
- "pad.modals.slowcommit.cause": "This could be due to problems with network connectivity.",
+ "pad.modals.slowcommit.explanation": "The server is not responding.",
+ "pad.modals.slowcommit.cause": "This could be due to problems with network connectivity.",
- "pad.modals.badChangeset.explanation": "An edit you have made was classified illegal by the synchronization server.",
- "pad.modals.badChangeset.cause": "This could be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator, if you feel this is an error. Try to reconnect in order to continue editing.",
+ "pad.modals.badChangeset.explanation": "An edit you have made was classified illegal by the synchronization server.",
+ "pad.modals.badChangeset.cause": "This could be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator, if you feel this is an error. Try to reconnect in order to continue editing.",
- "pad.modals.corruptPad.explanation": "The pad you are trying to access is corrupt.",
- "pad.modals.corruptPad.cause": "This may be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator.",
+ "pad.modals.corruptPad.explanation": "The pad you are trying to access is corrupt.",
+ "pad.modals.corruptPad.cause": "This may be due to a wrong server configuration or some other unexpected behavior. Please contact the service administrator.",
- "pad.modals.deleted": "Deleted.",
- "pad.modals.deleted.explanation": "This pad has been removed.",
+ "pad.modals.deleted": "Deleted.",
+ "pad.modals.deleted.explanation": "This pad has been removed.",
- "pad.modals.rateLimited": "Rate Limited.",
- "pad.modals.rateLimited.explanation": "You sent too many messages to this pad so it disconnected you.",
+ "pad.modals.rateLimited": "Rate Limited.",
+ "pad.modals.rateLimited.explanation": "You sent too many messages to this pad so it disconnected you.",
- "pad.modals.rejected.explanation": "The server rejected a message that was sent by your browser.",
- "pad.modals.rejected.cause": "The server may have been updated while you were viewing the pad, or maybe there is a bug in Etherpad. Try reloading the page.",
+ "pad.modals.rejected.explanation": "The server rejected a message that was sent by your browser.",
+ "pad.modals.rejected.cause": "The server may have been updated while you were viewing the pad, or maybe there is a bug in Etherpad. Try reloading the page.",
- "pad.modals.disconnected": "You have been disconnected.",
- "pad.modals.disconnected.explanation": "The connection to the server was lost",
- "pad.modals.disconnected.cause": "The server may be unavailable. Please notify the service administrator if this continues to happen.",
+ "pad.modals.disconnected": "You have been disconnected.",
+ "pad.modals.disconnected.explanation": "The connection to the server was lost",
+ "pad.modals.disconnected.cause": "The server may be unavailable. Please notify the service administrator if this continues to happen.",
- "pad.share": "Share this pad",
- "pad.share.readonly": "Read only",
- "pad.share.link": "Link",
- "pad.share.emebdcode": "Embed URL",
- "pad.chat": "Chat",
- "pad.chat.title": "Open the chat for this pad.",
- "pad.chat.loadmessages": "Load more messages",
- "pad.chat.stick.title": "Stick chat to screen",
- "pad.chat.writeMessage.placeholder": "Write your message here",
+ "pad.share": "Share this pad",
+ "pad.share.readonly": "Read only",
+ "pad.share.link": "Link",
+ "pad.share.emebdcode": "Embed URL",
+ "pad.chat": "Chat",
+ "pad.chat.title": "Open the chat for this pad.",
+ "pad.chat.loadmessages": "Load more messages",
+ "pad.chat.stick.title": "Stick chat to screen",
+ "pad.chat.writeMessage.placeholder": "Write your message here",
- "timeslider.followContents": "Follow pad content updates",
- "timeslider.pageTitle": "{{appTitle}} Timeslider",
- "timeslider.toolbar.returnbutton": "Return to pad",
- "timeslider.toolbar.authors": "Authors:",
- "timeslider.toolbar.authorsList": "No Authors",
- "timeslider.toolbar.exportlink.title": "Export",
- "timeslider.exportCurrent": "Export current version as:",
- "timeslider.version": "Version {{version}}",
- "timeslider.saved": "Saved {{month}} {{day}}, {{year}}",
+ "timeslider.followContents": "Follow pad content updates",
+ "timeslider.pageTitle": "{{appTitle}} Timeslider",
+ "timeslider.toolbar.returnbutton": "Return to pad",
+ "timeslider.toolbar.authors": "Authors:",
+ "timeslider.toolbar.authorsList": "No Authors",
+ "timeslider.toolbar.exportlink.title": "Export",
+ "timeslider.exportCurrent": "Export current version as:",
+ "timeslider.version": "Version {{version}}",
+ "timeslider.saved": "Saved {{month}} {{day}}, {{year}}",
- "timeslider.playPause": "Playback / Pause Pad Contents",
- "timeslider.backRevision":"Go back a revision in this Pad",
- "timeslider.forwardRevision":"Go forward a revision in this Pad",
+ "timeslider.playPause": "Playback / Pause Pad Contents",
+ "timeslider.backRevision": "Go back a revision in this Pad",
+ "timeslider.forwardRevision": "Go forward a revision in this Pad",
- "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
- "timeslider.month.january": "January",
- "timeslider.month.february": "February",
- "timeslider.month.march": "March",
- "timeslider.month.april": "April",
- "timeslider.month.may": "May",
- "timeslider.month.june": "June",
- "timeslider.month.july": "July",
- "timeslider.month.august": "August",
- "timeslider.month.september": "September",
- "timeslider.month.october": "October",
- "timeslider.month.november": "November",
- "timeslider.month.december": "December",
+ "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
+ "timeslider.month.january": "January",
+ "timeslider.month.february": "February",
+ "timeslider.month.march": "March",
+ "timeslider.month.april": "April",
+ "timeslider.month.may": "May",
+ "timeslider.month.june": "June",
+ "timeslider.month.july": "July",
+ "timeslider.month.august": "August",
+ "timeslider.month.september": "September",
+ "timeslider.month.october": "October",
+ "timeslider.month.november": "November",
+ "timeslider.month.december": "December",
- "timeslider.unnamedauthors": "{{num}} unnamed {[plural(num) one: author, other: authors ]}",
- "pad.savedrevs.marked": "This revision is now marked as a saved revision",
- "pad.savedrevs.timeslider": "You can see saved revisions by visiting the timeslider",
- "pad.userlist.entername": "Enter your name",
- "pad.userlist.unnamed": "unnamed",
- "pad.editbar.clearcolors": "Clear authorship colors on entire document? This cannot be undone",
+ "timeslider.unnamedauthors": "{{num}} unnamed {[plural(num) one: author, other: authors ]}",
+ "pad.savedrevs.marked": "This revision is now marked as a saved revision",
+ "pad.savedrevs.timeslider": "You can see saved revisions by visiting the timeslider",
+ "pad.userlist.entername": "Enter your name",
+ "pad.userlist.unnamed": "unnamed",
+ "pad.editbar.clearcolors": "Clear authorship colors on entire document? This cannot be undone",
- "pad.impexp.importbutton": "Import Now",
- "pad.impexp.importing": "Importing...",
- "pad.impexp.confirmimport": "Importing a file will overwrite the current text of the pad. Are you sure you want to proceed?",
- "pad.impexp.convertFailed": "We were not able to import this file. Please use a different document format or copy paste manually",
- "pad.impexp.padHasData": "We were not able to import this file because this Pad has already had changes, please import to a new pad",
- "pad.impexp.uploadFailed": "The upload failed, please try again",
- "pad.impexp.importfailed": "Import failed",
- "pad.impexp.copypaste": "Please copy paste",
- "pad.impexp.exportdisabled": "Exporting as {{type}} format is disabled. Please contact your system administrator for details.",
- "pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import"
+ "pad.impexp.importbutton": "Import Now",
+ "pad.impexp.importing": "Importing...",
+ "pad.impexp.confirmimport": "Importing a file will overwrite the current text of the pad. Are you sure you want to proceed?",
+ "pad.impexp.convertFailed": "We were not able to import this file. Please use a different document format or copy paste manually",
+ "pad.impexp.padHasData": "We were not able to import this file because this Pad has already had changes, please import to a new pad",
+ "pad.impexp.uploadFailed": "The upload failed, please try again",
+ "pad.impexp.importfailed": "Import failed",
+ "pad.impexp.copypaste": "Please copy paste",
+ "pad.impexp.exportdisabled": "Exporting as {{type}} format is disabled. Please contact your system administrator for details.",
+ "pad.impexp.maxFileSize": "File too big. Contact your site administrator to increase the allowed file size for import"
}
diff --git a/src/locales/et.json b/src/locales/et.json
index 8ec7900a3..997912b08 100644
--- a/src/locales/et.json
+++ b/src/locales/et.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Kristian.kankainen",
- "Tiblu"
- ]
+ "authors": ["Kristian.kankainen", "Tiblu"]
},
"index.newPad": "Uus klade",
"index.createOpenPad": "loo või rööptoimeta kladet nimega:",
diff --git a/src/locales/ff.json b/src/locales/ff.json
index dbb6c3b02..b5c10b8c0 100644
--- a/src/locales/ff.json
+++ b/src/locales/ff.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Ibrahima Malal Sarr"
- ]
+ "authors": ["Ibrahima Malal Sarr"]
},
"admin.page-title": "Tiimtorde Jiiloowo - Etherpad",
"admin_plugins": "Toppitorde Ceŋe",
diff --git a/src/locales/fo.json b/src/locales/fo.json
index c98ade6c4..48b6f5dff 100644
--- a/src/locales/fo.json
+++ b/src/locales/fo.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "EileenSanda"
- ]
+ "authors": ["EileenSanda"]
},
"index.newPad": "Nýggjur teldil",
"pad.toolbar.bold.title": "Við feitum (Ctrl-B)",
diff --git a/src/locales/fy.json b/src/locales/fy.json
index 093b40e32..a202a347c 100644
--- a/src/locales/fy.json
+++ b/src/locales/fy.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Robin van der Vliet"
- ]
+ "authors": ["Robin van der Vliet"]
},
"pad.toolbar.bold.title": "Fet (Ctrl+B)",
"pad.toolbar.italic.title": "Kursyf (Ctrl+I)",
diff --git a/src/locales/gl.json b/src/locales/gl.json
index 352737d39..18c58be90 100644
--- a/src/locales/gl.json
+++ b/src/locales/gl.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Elisardojm",
- "Ghose",
- "Toliño"
- ]
+ "authors": ["Elisardojm", "Ghose", "Toliño"]
},
"admin.page-title": "Panel de administración - Etherpad",
"admin_plugins": "Xestor de complementos",
diff --git a/src/locales/gu.json b/src/locales/gu.json
index d2f7c4fa1..6de719f13 100644
--- a/src/locales/gu.json
+++ b/src/locales/gu.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Bhatakati aatma",
- "Dsvyas",
- "Harsh4101991",
- "KartikMistry"
- ]
+ "authors": ["Bhatakati aatma", "Dsvyas", "Harsh4101991", "KartikMistry"]
},
"index.newPad": "નવું પેડ",
"pad.toolbar.bold.title": "બોલ્ડ",
diff --git a/src/locales/he.json b/src/locales/he.json
index 458ad0ace..5be75a3b5 100644
--- a/src/locales/he.json
+++ b/src/locales/he.json
@@ -1,12 +1,6 @@
{
"@metadata": {
- "authors": [
- "Amire80",
- "Ofrahod",
- "Steeve815",
- "YaronSh",
- "תומר ט"
- ]
+ "authors": ["Amire80", "Ofrahod", "Steeve815", "YaronSh", "תומר ט"]
},
"admin.page-title": "לוח ניהול - Etherpad",
"admin_plugins": "מנהל תוספים",
diff --git a/src/locales/hi.json b/src/locales/hi.json
index b238694a5..56589b9b6 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Sfic"
- ]
+ "authors": ["Sfic"]
},
"pad.toolbar.bold.title": "गहरा (Ctrl+B)",
"pad.toolbar.italic.title": "तिरछा (Ctrl+I)",
diff --git a/src/locales/hr.json b/src/locales/hr.json
index 77b10674a..d99f29d37 100644
--- a/src/locales/hr.json
+++ b/src/locales/hr.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Bugoslav",
- "Hmxhmx",
- "Ponor"
- ]
+ "authors": ["Bugoslav", "Hmxhmx", "Ponor"]
},
"index.newPad": "Novi blokić",
"index.createOpenPad": "ili stvori/otvori blokić s imenom:",
diff --git a/src/locales/hrx.json b/src/locales/hrx.json
index 397e41c9e..c4baf381e 100644
--- a/src/locales/hrx.json
+++ b/src/locales/hrx.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Paul Beppler"
- ]
+ "authors": ["Paul Beppler"]
},
"index.newPad": "Neies Pad",
"index.createOpenPad": "Pad mit follichendem Noome uffmache:",
diff --git a/src/locales/hsb.json b/src/locales/hsb.json
index 62f70740f..cefac571c 100644
--- a/src/locales/hsb.json
+++ b/src/locales/hsb.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Michawiki"
- ]
+ "authors": ["Michawiki"]
},
"admin.page-title": "Administratorowa deska – Etherpad",
"admin_plugins": "Zrjadowak tykačow",
diff --git a/src/locales/hy.json b/src/locales/hy.json
index a842a6c24..289bd0e53 100644
--- a/src/locales/hy.json
+++ b/src/locales/hy.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Armenoid",
- "Kareyac"
- ]
+ "authors": ["Armenoid", "Kareyac"]
},
"admin_plugins.available_install.value": "Տեղադրել",
"admin_plugins.description": "Նկարագրություն",
diff --git a/src/locales/ia.json b/src/locales/ia.json
index 6d0c4f283..88d887528 100644
--- a/src/locales/ia.json
+++ b/src/locales/ia.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "McDutchie"
- ]
+ "authors": ["McDutchie"]
},
"admin.page-title": "Pannello administrative – Etherpad",
"admin_plugins": "Gestor de plug-ins",
diff --git a/src/locales/id.json b/src/locales/id.json
index 49b8f34fa..fdd941c9e 100644
--- a/src/locales/id.json
+++ b/src/locales/id.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Bennylin",
- "IvanLanin",
- "Marwan Mohamad",
- "Veracious"
- ]
+ "authors": ["Bennylin", "IvanLanin", "Marwan Mohamad", "Veracious"]
},
"admin.page-title": "Dasbor Pengurus - Etherpad",
"admin_plugins": "Manajer plugin",
diff --git a/src/locales/is.json b/src/locales/is.json
index 0ff9ec5d6..f0aee3469 100644
--- a/src/locales/is.json
+++ b/src/locales/is.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Sveinki",
- "Sveinn í Felli"
- ]
+ "authors": ["Sveinki", "Sveinn í Felli"]
},
"admin.page-title": "Stjórnborð fyrir stjórnendur - Etherpad",
"admin_plugins": "Stýring viðbóta",
diff --git a/src/locales/kab.json b/src/locales/kab.json
index 77f7fa1ad..3f029a776 100644
--- a/src/locales/kab.json
+++ b/src/locales/kab.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Belkacem77"
- ]
+ "authors": ["Belkacem77"]
},
"index.newPad": "Apad amaynut",
"index.createOpenPad": "neɣ rnu/ldi apad s yisem:",
diff --git a/src/locales/km.json b/src/locales/km.json
index bbb255f8c..dc81094c6 100644
--- a/src/locales/km.json
+++ b/src/locales/km.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Pichnat Thong",
- "Sovichet",
- "វ័ណថារិទ្ធ"
- ]
+ "authors": ["Pichnat Thong", "Sovichet", "វ័ណថារិទ្ធ"]
},
"index.newPad": "ផេតថ្មី",
"index.createOpenPad": "ឬបង្កើត/បើកផេតដែលមានឈ្មោះ៖",
diff --git a/src/locales/kn.json b/src/locales/kn.json
index f1f109c07..e334a0e52 100644
--- a/src/locales/kn.json
+++ b/src/locales/kn.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Nayvik",
- "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"
- ]
+ "authors": ["Nayvik", "ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ"]
},
"admin_plugins.available": "ಲಭ್ಯವಿರುವ ಪ್ಲಗಿನ್ಗಳು",
"admin_plugins.available_not-found": "ಯಾವುದೇ ಪ್ಲಗಿನ್ಗಳು ಸಿಗಲಿಲ್ಲ",
diff --git a/src/locales/krc.json b/src/locales/krc.json
index 76e9bb97a..99a041485 100644
--- a/src/locales/krc.json
+++ b/src/locales/krc.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Ernác",
- "Къарачайлы"
- ]
+ "authors": ["Ernác", "Къарачайлы"]
},
"admin.page-title": "Администраторну панели — Etherpad",
"admin_plugins": "Плагин менеджер",
diff --git a/src/locales/ksh.json b/src/locales/ksh.json
index 8015e4f11..90d4ec6a5 100644
--- a/src/locales/ksh.json
+++ b/src/locales/ksh.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Purodha"
- ]
+ "authors": ["Purodha"]
},
"index.newPad": "Neu Pädd",
"index.createOpenPad": "udder maach e Pädd op med däm Nahme:",
diff --git a/src/locales/lb.json b/src/locales/lb.json
index 8bb3beb77..4be590350 100644
--- a/src/locales/lb.json
+++ b/src/locales/lb.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Gromper",
- "Robby",
- "Soued031",
- "Volvox"
- ]
+ "authors": ["Gromper", "Robby", "Soued031", "Volvox"]
},
"admin_plugins.available_install.value": "Installéieren",
"admin_plugins.description": "Beschreiwung",
diff --git a/src/locales/lki.json b/src/locales/lki.json
index 556368a20..6dc87675a 100644
--- a/src/locales/lki.json
+++ b/src/locales/lki.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Arash71",
- "Hosseinblue",
- "Lakzon"
- ]
+ "authors": ["Arash71", "Hosseinblue", "Lakzon"]
},
"index.newPad": "تازۀpad",
"index.createOpenPad": ":وە نۆم Pad یا سازین/واز کردن یإگلە",
diff --git a/src/locales/lrc.json b/src/locales/lrc.json
index ec343a553..5e603d727 100644
--- a/src/locales/lrc.json
+++ b/src/locales/lrc.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Lorestani",
- "Mogoeilor"
- ]
+ "authors": ["Lorestani", "Mogoeilor"]
},
"index.newPad": "دٱفتٱرچٱ تازٱ",
"pad.toolbar.bold.title": "تۊپور",
diff --git a/src/locales/lv.json b/src/locales/lv.json
index 29d07b19b..3608af29e 100644
--- a/src/locales/lv.json
+++ b/src/locales/lv.json
@@ -1,12 +1,6 @@
{
"@metadata": {
- "authors": [
- "Admresdeserv.",
- "Jmg.cmdi",
- "Oskars",
- "Papuass",
- "Silraks"
- ]
+ "authors": ["Admresdeserv.", "Jmg.cmdi", "Oskars", "Papuass", "Silraks"]
},
"index.newPad": "Tiek izmantots kā pogas teksts. Blociņš, Etherpad kontekstā, ir piezīmju blociņš, uz kura rakstīt.",
"index.createOpenPad": "vai izveidojiet/atveriet Blociņu ar nosaukumu:",
diff --git a/src/locales/map-bms.json b/src/locales/map-bms.json
index d920cc5cb..ed5b39cc3 100644
--- a/src/locales/map-bms.json
+++ b/src/locales/map-bms.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Empu",
- "StefanusRA"
- ]
+ "authors": ["Empu", "StefanusRA"]
},
"index.newPad": "Pad Anyar",
"index.createOpenPad": "utawa gawe/bukak Pad nganggo jeneng:",
diff --git a/src/locales/mg.json b/src/locales/mg.json
index 79a5296d5..68cbfd927 100644
--- a/src/locales/mg.json
+++ b/src/locales/mg.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Jagwar"
- ]
+ "authors": ["Jagwar"]
},
"index.newPad": "Pad vaovao",
"index.createOpenPad": "na hamorona/hanokatra Pad manana anarana:",
diff --git a/src/locales/mk.json b/src/locales/mk.json
index 68ba2f1cd..9bc46ec9a 100644
--- a/src/locales/mk.json
+++ b/src/locales/mk.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Bjankuloski06",
- "Brest",
- "Vlad5250"
- ]
+ "authors": ["Bjankuloski06", "Brest", "Vlad5250"]
},
"admin.page-title": "Администраторска управувачница — Etherpad",
"admin_plugins": "Раководител со приклучоци",
diff --git a/src/locales/mn.json b/src/locales/mn.json
index aa024466c..633ece95e 100644
--- a/src/locales/mn.json
+++ b/src/locales/mn.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "MongolWiki",
- "Munkhzaya.E",
- "Wisdom"
- ]
+ "authors": ["MongolWiki", "Munkhzaya.E", "Wisdom"]
},
"pad.toolbar.bold.title": "Болд тескт (Ctrl-B)",
"pad.toolbar.italic.title": "Налуу тескт (Ctrl-I)",
diff --git a/src/locales/mnw.json b/src/locales/mnw.json
index db7dfa56e..6f11ede04 100644
--- a/src/locales/mnw.json
+++ b/src/locales/mnw.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Aue Nai",
- "咽頭べさ"
- ]
+ "authors": ["Aue Nai", "咽頭べさ"]
},
"admin_plugins.description": "တၚ်ထမံက်ထ္ၜး",
"index.newPad": "တၞးတၟိ",
diff --git a/src/locales/mr.json b/src/locales/mr.json
index a9ecc6baf..050b663a4 100644
--- a/src/locales/mr.json
+++ b/src/locales/mr.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Ganeshgiram",
- "V.narsikar",
- "Ydyashad"
- ]
+ "authors": ["Ganeshgiram", "V.narsikar", "Ydyashad"]
},
"index.newPad": "नव पान",
"pad.toolbar.bold.title": "ठळक (Ctrl-B)",
diff --git a/src/locales/ms.json b/src/locales/ms.json
index a5d8d1deb..daa0c50b5 100644
--- a/src/locales/ms.json
+++ b/src/locales/ms.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Anakmalaysia",
- "Hakimi97",
- "Jeluang Terluang"
- ]
+ "authors": ["Anakmalaysia", "Hakimi97", "Jeluang Terluang"]
},
"admin.page-title": "Papan muka Penyelia - Etherpad",
"index.newPad": "Pad baru",
diff --git a/src/locales/my.json b/src/locales/my.json
index d427bbcb5..0c40047ae 100644
--- a/src/locales/my.json
+++ b/src/locales/my.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Andibecker",
- "Dr Lotus Black"
- ]
+ "authors": ["Andibecker", "Dr Lotus Black"]
},
"admin.page-title": "စီမံခန့်ခွဲသူဒိုင်ခွက် - Etherpad",
"admin_plugins": "ပလပ်အင်မန်နေဂျာ",
diff --git a/src/locales/nah.json b/src/locales/nah.json
index a5c40db10..5c6ace326 100644
--- a/src/locales/nah.json
+++ b/src/locales/nah.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Akapochtli",
- "Languaeditor",
- "Taresi"
- ]
+ "authors": ["Akapochtli", "Languaeditor", "Taresi"]
},
"index.newPad": "Yancuic Pad",
"index.createOpenPad": "auh xicchīhua/xictlapo cē Pad in ītōcā:",
diff --git a/src/locales/nap.json b/src/locales/nap.json
index 70f615b38..0bc341310 100644
--- a/src/locales/nap.json
+++ b/src/locales/nap.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "C.R.",
- "Chelin",
- "Finizio",
- "Ruthven"
- ]
+ "authors": ["C.R.", "Chelin", "Finizio", "Ruthven"]
},
"admin_plugins.name": "Nomme",
"index.newPad": "Nuovo Pad",
diff --git a/src/locales/nds.json b/src/locales/nds.json
index 75cdaabec..64d8dbc43 100644
--- a/src/locales/nds.json
+++ b/src/locales/nds.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Gthoele",
- "Joachim Mos"
- ]
+ "authors": ["Gthoele", "Joachim Mos"]
},
"index.newPad": "Nee'et Pad",
"index.createOpenPad": "oder Pad mit düssen Naam apen maken:",
diff --git a/src/locales/nn.json b/src/locales/nn.json
index ee7720279..4404828bf 100644
--- a/src/locales/nn.json
+++ b/src/locales/nn.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Unhammer"
- ]
+ "authors": ["Unhammer"]
},
"index.newPad": "Ny blokk",
"index.createOpenPad": "eller opprett/opna ei blokk med namnet:",
diff --git a/src/locales/oc.json b/src/locales/oc.json
index 84a015ae4..faf2d4214 100644
--- a/src/locales/oc.json
+++ b/src/locales/oc.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Cedric31",
- "Quentí"
- ]
+ "authors": ["Cedric31", "Quentí"]
},
"admin.page-title": "Panèl d’administracion - Etherpad",
"admin_plugins": "Gestion de las extensions",
diff --git a/src/locales/olo.json b/src/locales/olo.json
index 2121db40b..aca128798 100644
--- a/src/locales/olo.json
+++ b/src/locales/olo.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Denö",
- "Ilja.mos",
- "Mashoi7"
- ]
+ "authors": ["Denö", "Ilja.mos", "Mashoi7"]
},
"pad.toolbar.underline.title": "Alleviivua (Ctrl+U)",
"pad.toolbar.settings.title": "Azetukset",
diff --git a/src/locales/os.json b/src/locales/os.json
index 2f023b27a..f7aceca9c 100644
--- a/src/locales/os.json
+++ b/src/locales/os.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Bouron"
- ]
+ "authors": ["Bouron"]
},
"index.newPad": "Ног",
"index.createOpenPad": "кӕнӕ сараз/бакӕн ног документ ахӕм номимӕ:",
diff --git a/src/locales/pa.json b/src/locales/pa.json
index 071498f98..95d549ca4 100644
--- a/src/locales/pa.json
+++ b/src/locales/pa.json
@@ -1,12 +1,6 @@
{
"@metadata": {
- "authors": [
- "Aalam",
- "Babanwalia",
- "Tow",
- "ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ",
- "ਪ੍ਰਚਾਰਕ"
- ]
+ "authors": ["Aalam", "Babanwalia", "Tow", "ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ", "ਪ੍ਰਚਾਰਕ"]
},
"index.newPad": "ਨਵਾਂ ਪੈਡ",
"index.createOpenPad": "ਜਾਂ ਨਾਂ ਨਾਲ ਨਵਾਂ ਪੈਡ ਬਣਾਓ/ਖੋਲ੍ਹੋ:",
diff --git a/src/locales/pms.json b/src/locales/pms.json
index 2d25ea928..445590e0a 100644
--- a/src/locales/pms.json
+++ b/src/locales/pms.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Borichèt"
- ]
+ "authors": ["Borichèt"]
},
"admin.page-title": "Cruscòt d'aministrator - Etherpad",
"admin_plugins": "Mansé dj'anstalassion",
diff --git a/src/locales/ps.json b/src/locales/ps.json
index 7413c41e2..b7b10df80 100644
--- a/src/locales/ps.json
+++ b/src/locales/ps.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Ahmed-Najib-Biabani-Ibrahimkhel"
- ]
+ "authors": ["Ahmed-Najib-Biabani-Ibrahimkhel"]
},
"index.newPad": "نوې ليکچه",
"index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانيستل:",
diff --git a/src/locales/sc.json b/src/locales/sc.json
index 0ae544a7b..fa3e91b8d 100644
--- a/src/locales/sc.json
+++ b/src/locales/sc.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Adr mm"
- ]
+ "authors": ["Adr mm"]
},
"admin.page-title": "Pannellu de amministratzione - Etherpad",
"admin_plugins": "Gestore de connetores",
diff --git a/src/locales/sd.json b/src/locales/sd.json
index a167196f9..78e1ba0ad 100644
--- a/src/locales/sd.json
+++ b/src/locales/sd.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "BaRaN6161 TURK",
- "Kaleem Bhatti",
- "Mehtab ahmed",
- "Tweety"
- ]
+ "authors": ["BaRaN6161 TURK", "Kaleem Bhatti", "Mehtab ahmed", "Tweety"]
},
"admin_settings": "ترتيبون",
"index.newPad": "نئين پٽي",
diff --git a/src/locales/sh.json b/src/locales/sh.json
index f5c045c55..c6eac49c1 100644
--- a/src/locales/sh.json
+++ b/src/locales/sh.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Conquistador",
- "Vlad5250"
- ]
+ "authors": ["Conquistador", "Vlad5250"]
},
"admin_plugins.available_not-found": "Nijedan plugin nije pronađen.",
"admin_plugins.description": "Opis",
diff --git a/src/locales/shn.json b/src/locales/shn.json
index d03f9e6c7..ed765cebd 100644
--- a/src/locales/shn.json
+++ b/src/locales/shn.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Ninjastrikers",
- "Saimawnkham",
- "Saosukham"
- ]
+ "authors": ["Ninjastrikers", "Saimawnkham", "Saosukham"]
},
"index.newPad": "ၽႅတ်ႉမႂ်ႇ",
"index.createOpenPad": "ဢမ်ႇၼၼ် ၶူင်မႂ်ႇ/ပိုတ်ႇၽႅတ်ႉၵိုၵ်းၸိုဝ်ႈ",
diff --git a/src/locales/skr-arab.json b/src/locales/skr-arab.json
index 67abc7aa8..0aa7be7f0 100644
--- a/src/locales/skr-arab.json
+++ b/src/locales/skr-arab.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Saraiki"
- ]
+ "authors": ["Saraiki"]
},
"admin_plugins": "پلگ ان منیجر",
"admin_plugins.available": "دستیاب پلگ ان",
diff --git a/src/locales/sms.json b/src/locales/sms.json
index 3ea65a3cb..ba21342c7 100644
--- a/src/locales/sms.json
+++ b/src/locales/sms.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Yupik"
- ]
+ "authors": ["Yupik"]
},
"admin_plugins.description": "Deskriptt",
"admin_plugins.name": "Nõmm",
diff --git a/src/locales/sq.json b/src/locales/sq.json
index b6c2fa285..fed2fd3a3 100644
--- a/src/locales/sq.json
+++ b/src/locales/sq.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Besnik b",
- "Eraldkerciku",
- "Kosovastar",
- "Liridon"
- ]
+ "authors": ["Besnik b", "Eraldkerciku", "Kosovastar", "Liridon"]
},
"admin.page-title": "Pult Përgjegjësi - Etherpad",
"admin_plugins": "Përgjegjës shtojcash",
diff --git a/src/locales/sro.json b/src/locales/sro.json
index 03578c299..d929213ed 100644
--- a/src/locales/sro.json
+++ b/src/locales/sro.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "Adr mm",
- "F Samaritani"
- ]
+ "authors": ["Adr mm", "F Samaritani"]
},
"admin.page-title": "Pannellu amministrativu - Etherpad",
"admin_plugins": "Gestore de connetores",
diff --git a/src/locales/sw.json b/src/locales/sw.json
index 33d24add2..4eddb13da 100644
--- a/src/locales/sw.json
+++ b/src/locales/sw.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Andibecker",
- "Edwingudfriend",
- "Muddyb"
- ]
+ "authors": ["Andibecker", "Edwingudfriend", "Muddyb"]
},
"admin.page-title": "Dashibodi ya Usimamizi - Etherpad",
"admin_plugins": "Meneja wa programu-jalizi",
diff --git a/src/locales/ta.json b/src/locales/ta.json
index d36a8d22e..71a0130e9 100644
--- a/src/locales/ta.json
+++ b/src/locales/ta.json
@@ -1,10 +1,6 @@
{
"@metadata": {
- "authors": [
- "Balajijagadesh",
- "ElangoRamanujam",
- "Sank"
- ]
+ "authors": ["Balajijagadesh", "ElangoRamanujam", "Sank"]
},
"index.newPad": "புதிய அட்டை",
"index.createOpenPad": "அல்லது பெயருடன் ஒரு அட்டையை உருவாக்கு/திற",
diff --git a/src/locales/tcy.json b/src/locales/tcy.json
index 0e56ec596..11054ab54 100644
--- a/src/locales/tcy.json
+++ b/src/locales/tcy.json
@@ -1,9 +1,6 @@
{
"@metadata": {
- "authors": [
- "BHARATHESHA ALASANDEMAJALU",
- "VASANTH S.N."
- ]
+ "authors": ["BHARATHESHA ALASANDEMAJALU", "VASANTH S.N."]
},
"index.newPad": "ಪೊಸ ಪ್ಯಾಡ್",
"index.createOpenPad": "ಅತಂಡ ಈ ಪುದರ್ತ ಪ್ಯಾಡನ್ನು ಉಂಡು ಮನ್ಪು/ತೋಜಾಲ:",
diff --git a/src/locales/th.json b/src/locales/th.json
index 7de1e4fdc..3f53b1fe2 100644
--- a/src/locales/th.json
+++ b/src/locales/th.json
@@ -1,11 +1,6 @@
{
"@metadata": {
- "authors": [
- "Aefgh39622",
- "Andibecker",
- "Patsagorn Y.",
- "Trisorn Triboon"
- ]
+ "authors": ["Aefgh39622", "Andibecker", "Patsagorn Y.", "Trisorn Triboon"]
},
"admin.page-title": "แดชบอร์ดผู้ดูแลระบบ - Etherpad",
"admin_plugins": "ตัวจัดการปลั๊กอิน",
diff --git a/src/locales/vec.json b/src/locales/vec.json
index 97892523e..3e146a9f9 100644
--- a/src/locales/vec.json
+++ b/src/locales/vec.json
@@ -1,8 +1,6 @@
{
"@metadata": {
- "authors": [
- "Fierodelveneto"
- ]
+ "authors": ["Fierodelveneto"]
},
"index.newPad": "Novo Pad",
"index.createOpenPad": "O creare o verxare on Pad co'l nome:",
diff --git a/src/node/db/API.ts b/src/node/db/API.ts
index 9ce84fc51..6f4bace9a 100644
--- a/src/node/db/API.ts
+++ b/src/node/db/API.ts
@@ -1,4 +1,3 @@
-'use strict';
/**
* This module provides all API functions
*/
@@ -19,21 +18,21 @@
* limitations under the License.
*/
-const Changeset = require('../../static/js/Changeset');
-const ChatMessage = require('../../static/js/ChatMessage');
-const CustomError = require('../utils/customError');
-const padManager = require('./PadManager');
-const padMessageHandler = require('../handler/PadMessageHandler');
-const readOnlyManager = require('./ReadOnlyManager');
-const groupManager = require('./GroupManager');
-const authorManager = require('./AuthorManager');
-const sessionManager = require('./SessionManager');
-const exportHtml = require('../utils/ExportHtml');
-const exportTxt = require('../utils/ExportTxt');
-const importHtml = require('../utils/ImportHtml');
-const cleanText = require('./Pad').cleanText;
-const PadDiff = require('../utils/padDiff');
-const {checkValidRev, isInt} = require('../utils/checkValidRev');
+const Changeset = require("../../static/js/Changeset");
+const ChatMessage = require("../../static/js/ChatMessage");
+const CustomError = require("../utils/customError");
+const padManager = require("./PadManager");
+const padMessageHandler = require("../handler/PadMessageHandler");
+const readOnlyManager = require("./ReadOnlyManager");
+const groupManager = require("./GroupManager");
+const authorManager = require("./AuthorManager");
+const sessionManager = require("./SessionManager");
+const exportHtml = require("../utils/ExportHtml");
+const exportTxt = require("../utils/ExportTxt");
+const importHtml = require("../utils/ImportHtml");
+const cleanText = require("./Pad").cleanText;
+const PadDiff = require("../utils/padDiff");
+const { checkValidRev, isInt } = require("../utils/checkValidRev");
/* ********************
* GROUP FUNCTIONS ****
@@ -105,8 +104,8 @@ Example returns:
*/
exports.getAttributePool = async (padID: string) => {
- const pad = await getPadSafe(padID, true);
- return {pool: pad.pool};
+ const pad = await getPadSafe(padID, true);
+ return { pool: pad.pool };
};
/**
@@ -123,28 +122,31 @@ Example returns:
*/
exports.getRevisionChangeset = async (padID: string, rev: string) => {
- // try to parse the revision number
- if (rev !== undefined) {
- rev = checkValidRev(rev);
- }
+ // try to parse the revision number
+ if (rev !== undefined) {
+ rev = checkValidRev(rev);
+ }
- // get the pad
- const pad = await getPadSafe(padID, true);
- const head = pad.getHeadRevisionNumber();
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ const head = pad.getHeadRevisionNumber();
- // the client asked for a special revision
- if (rev !== undefined) {
- // check if this is a valid revision
- if (rev > head) {
- throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
- }
+ // the client asked for a special revision
+ if (rev !== undefined) {
+ // check if this is a valid revision
+ if (rev > head) {
+ throw new CustomError(
+ "rev is higher than the head revision of the pad",
+ "apierror",
+ );
+ }
- // get the changeset for this revision
- return await pad.getRevisionChangeset(rev);
- }
+ // get the changeset for this revision
+ return await pad.getRevisionChangeset(rev);
+ }
- // the client wants the latest changeset, lets return it to him
- return await pad.getRevisionChangeset(head);
+ // the client wants the latest changeset, lets return it to him
+ return await pad.getRevisionChangeset(head);
};
/**
@@ -156,32 +158,35 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
*/
exports.getText = async (padID: string, rev: string) => {
- // try to parse the revision number
- if (rev !== undefined) {
- rev = checkValidRev(rev);
- }
+ // try to parse the revision number
+ if (rev !== undefined) {
+ rev = checkValidRev(rev);
+ }
- // get the pad
- const pad = await getPadSafe(padID, true);
- const head = pad.getHeadRevisionNumber();
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ const head = pad.getHeadRevisionNumber();
- // the client asked for a special revision
- if (rev !== undefined) {
- // check if this is a valid revision
- if (rev > head) {
- throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
- }
+ // the client asked for a special revision
+ if (rev !== undefined) {
+ // check if this is a valid revision
+ if (rev > head) {
+ throw new CustomError(
+ "rev is higher than the head revision of the pad",
+ "apierror",
+ );
+ }
- // get the text of this revision
- // getInternalRevisionAText() returns an atext object, but we only want the .text inside it.
- // Details at https://github.com/ether/etherpad-lite/issues/5073
- const {text} = await pad.getInternalRevisionAText(rev);
- return {text};
- }
+ // get the text of this revision
+ // getInternalRevisionAText() returns an atext object, but we only want the .text inside it.
+ // Details at https://github.com/ether/etherpad-lite/issues/5073
+ const { text } = await pad.getInternalRevisionAText(rev);
+ return { text };
+ }
- // the client wants the latest text, lets return it to him
- const text = exportTxt.getTXTFromAtext(pad, pad.atext);
- return {text};
+ // the client wants the latest text, lets return it to him
+ const text = exportTxt.getTXTFromAtext(pad, pad.atext);
+ return { text };
};
/**
@@ -200,17 +205,21 @@ Example returns:
* @param {String} authorId the id of the author, defaulting to empty string
* @returns {Promise}
*/
-exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise => {
- // text is required
- if (typeof text !== 'string') {
- throw new CustomError('text is not a string', 'apierror');
- }
+exports.setText = async (
+ padID: string,
+ text?: string,
+ authorId = "",
+): Promise => {
+ // text is required
+ if (typeof text !== "string") {
+ throw new CustomError("text is not a string", "apierror");
+ }
- // get the pad
- const pad = await getPadSafe(padID, true);
+ // get the pad
+ const pad = await getPadSafe(padID, true);
- await pad.setText(text, authorId);
- await padMessageHandler.updatePadClients(pad);
+ await pad.setText(text, authorId);
+ await padMessageHandler.updatePadClients(pad);
};
/**
@@ -225,15 +234,15 @@ Example returns:
@param {String} text the text of the pad
@param {String} authorId the id of the author, defaulting to empty string
*/
-exports.appendText = async (padID:string, text?: string, authorId:string = '') => {
- // text is required
- if (typeof text !== 'string') {
- throw new CustomError('text is not a string', 'apierror');
- }
+exports.appendText = async (padID: string, text?: string, authorId = "") => {
+ // text is required
+ if (typeof text !== "string") {
+ throw new CustomError("text is not a string", "apierror");
+ }
- const pad = await getPadSafe(padID, true);
- await pad.appendText(text, authorId);
- await padMessageHandler.updatePadClients(pad);
+ const pad = await getPadSafe(padID, true);
+ await pad.appendText(text, authorId);
+ await padMessageHandler.updatePadClients(pad);
};
/**
@@ -247,28 +256,34 @@ Example returns:
@param {String} rev the revision number, defaulting to the latest revision
@return {Promise<{html: string}>} the html of the pad
*/
-exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }> => {
- if (rev !== undefined) {
- rev = checkValidRev(rev);
- }
+exports.getHTML = async (
+ padID: string,
+ rev: string,
+): Promise<{ html: string }> => {
+ if (rev !== undefined) {
+ rev = checkValidRev(rev);
+ }
- const pad = await getPadSafe(padID, true);
+ const pad = await getPadSafe(padID, true);
- // the client asked for a special revision
- if (rev !== undefined) {
- // check if this is a valid revision
- const head = pad.getHeadRevisionNumber();
- if (rev > head) {
- throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
- }
- }
+ // the client asked for a special revision
+ if (rev !== undefined) {
+ // check if this is a valid revision
+ const head = pad.getHeadRevisionNumber();
+ if (rev > head) {
+ throw new CustomError(
+ "rev is higher than the head revision of the pad",
+ "apierror",
+ );
+ }
+ }
- // get the html of this revision
- let html = await exportHtml.getPadHTML(pad, rev);
+ // get the html of this revision
+ let html = await exportHtml.getPadHTML(pad, rev);
- // wrap the HTML
- html = `${html}`;
- return {html};
+ // wrap the HTML
+ html = `${html}`;
+ return { html };
};
/**
@@ -283,24 +298,28 @@ Example returns:
@param {String} html the html of the pad
@param {String} authorId the id of the author, defaulting to empty string
*/
-exports.setHTML = async (padID: string, html:string|object, authorId = '') => {
- // html string is required
- if (typeof html !== 'string') {
- throw new CustomError('html is not a string', 'apierror');
- }
+exports.setHTML = async (
+ padID: string,
+ html: string | object,
+ authorId = "",
+) => {
+ // html string is required
+ if (typeof html !== "string") {
+ throw new CustomError("html is not a string", "apierror");
+ }
- // get the pad
- const pad = await getPadSafe(padID, true);
+ // get the pad
+ const pad = await getPadSafe(padID, true);
- // add a new changeset with the new html to the pad
- try {
- await importHtml.setPadHTML(pad, cleanText(html), authorId);
- } catch (e) {
- throw new CustomError('HTML is malformed', 'apierror');
- }
+ // add a new changeset with the new html to the pad
+ try {
+ await importHtml.setPadHTML(pad, cleanText(html), authorId);
+ } catch (e) {
+ throw new CustomError("HTML is malformed", "apierror");
+ }
- // update the clients on the pad
- padMessageHandler.updatePadClients(pad);
+ // update the clients on the pad
+ padMessageHandler.updatePadClients(pad);
};
/* ****************
@@ -324,41 +343,47 @@ Example returns:
@param {Number} start the start point of the chat-history
@param {Number} end the end point of the chat-history
*/
-exports.getChatHistory = async (padID: string, start:number, end:number) => {
- if (start && end) {
- if (start < 0) {
- throw new CustomError('start is below zero', 'apierror');
- }
- if (end < 0) {
- throw new CustomError('end is below zero', 'apierror');
- }
- if (start > end) {
- throw new CustomError('start is higher than end', 'apierror');
- }
- }
+exports.getChatHistory = async (padID: string, start: number, end: number) => {
+ if (start && end) {
+ if (start < 0) {
+ throw new CustomError("start is below zero", "apierror");
+ }
+ if (end < 0) {
+ throw new CustomError("end is below zero", "apierror");
+ }
+ if (start > end) {
+ throw new CustomError("start is higher than end", "apierror");
+ }
+ }
- // get the pad
- const pad = await getPadSafe(padID, true);
+ // get the pad
+ const pad = await getPadSafe(padID, true);
- const chatHead = pad.chatHead;
+ const chatHead = pad.chatHead;
- // fall back to getting the whole chat-history if a parameter is missing
- if (!start || !end) {
- start = 0;
- end = pad.chatHead;
- }
+ // fall back to getting the whole chat-history if a parameter is missing
+ if (!start || !end) {
+ start = 0;
+ end = pad.chatHead;
+ }
- if (start > chatHead) {
- throw new CustomError('start is higher than the current chatHead', 'apierror');
- }
- if (end > chatHead) {
- throw new CustomError('end is higher than the current chatHead', 'apierror');
- }
+ if (start > chatHead) {
+ throw new CustomError(
+ "start is higher than the current chatHead",
+ "apierror",
+ );
+ }
+ if (end > chatHead) {
+ throw new CustomError(
+ "end is higher than the current chatHead",
+ "apierror",
+ );
+ }
- // the whole message-log and return it to the client
- const messages = await pad.getChatMessages(start, end);
+ // the whole message-log and return it to the client
+ const messages = await pad.getChatMessages(start, end);
- return {messages};
+ return { messages };
};
/**
@@ -374,21 +399,29 @@ Example returns:
@param {String} authorID the id of the author
@param {Number} time the timestamp of the chat-message
*/
-exports.appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => {
- // text is required
- if (typeof text !== 'string') {
- throw new CustomError('text is not a string', 'apierror');
- }
+exports.appendChatMessage = async (
+ padID: string,
+ text: string | object,
+ authorID: string,
+ time: number,
+) => {
+ // text is required
+ if (typeof text !== "string") {
+ throw new CustomError("text is not a string", "apierror");
+ }
- // if time is not an integer value set time to current timestamp
- if (time === undefined || !isInt(time)) {
- time = Date.now();
- }
+ // if time is not an integer value set time to current timestamp
+ if (time === undefined || !isInt(time)) {
+ time = Date.now();
+ }
- // @TODO - missing getPadSafe() call ?
+ // @TODO - missing getPadSafe() call ?
- // save chat message to database and send message to all connected clients
- await padMessageHandler.sendChatMessageToPadClients(new ChatMessage(text, authorID, time), padID);
+ // save chat message to database and send message to all connected clients
+ await padMessageHandler.sendChatMessageToPadClients(
+ new ChatMessage(text, authorID, time),
+ padID,
+ );
};
/* ***************
@@ -405,9 +438,9 @@ Example returns:
@param {String} padID the id of the pad
*/
exports.getRevisionsCount = async (padID: string) => {
- // get the pad
- const pad = await getPadSafe(padID, true);
- return {revisions: pad.getHeadRevisionNumber()};
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ return { revisions: pad.getHeadRevisionNumber() };
};
/**
@@ -420,9 +453,9 @@ Example returns:
@param {String} padID the id of the pad
*/
exports.getSavedRevisionsCount = async (padID: string) => {
- // get the pad
- const pad = await getPadSafe(padID, true);
- return {savedRevisions: pad.getSavedRevisionsNumber()};
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ return { savedRevisions: pad.getSavedRevisionsNumber() };
};
/**
@@ -435,9 +468,9 @@ Example returns:
@param {String} padID the id of the pad
*/
exports.listSavedRevisions = async (padID: string) => {
- // get the pad
- const pad = await getPadSafe(padID, true);
- return {savedRevisions: pad.getSavedRevisionsList()};
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ return { savedRevisions: pad.getSavedRevisionsList() };
};
/**
@@ -451,26 +484,29 @@ Example returns:
@param {Number} rev the revision number, defaulting to the latest revision
*/
exports.saveRevision = async (padID: string, rev: number) => {
- // check if rev is a number
- if (rev !== undefined) {
- rev = checkValidRev(rev);
- }
+ // check if rev is a number
+ if (rev !== undefined) {
+ rev = checkValidRev(rev);
+ }
- // get the pad
- const pad = await getPadSafe(padID, true);
- const head = pad.getHeadRevisionNumber();
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ const head = pad.getHeadRevisionNumber();
- // the client asked for a special revision
- if (rev !== undefined) {
- if (rev > head) {
- throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
- }
- } else {
- rev = pad.getHeadRevisionNumber();
- }
+ // the client asked for a special revision
+ if (rev !== undefined) {
+ if (rev > head) {
+ throw new CustomError(
+ "rev is higher than the head revision of the pad",
+ "apierror",
+ );
+ }
+ } else {
+ rev = pad.getHeadRevisionNumber();
+ }
- const author = await authorManager.createAuthor('API');
- await pad.addSavedRevision(rev, author.authorID, 'Saved through API call');
+ const author = await authorManager.createAuthor("API");
+ await pad.addSavedRevision(rev, author.authorID, "Saved through API call");
};
/**
@@ -483,11 +519,13 @@ Example returns:
@param {String} padID the id of the pad
@return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad
*/
-exports.getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => {
- // get the pad
- const pad = await getPadSafe(padID, true);
- const lastEdited = await pad.getLastEdit();
- return {lastEdited};
+exports.getLastEdited = async (
+ padID: string,
+): Promise<{ lastEdited: number }> => {
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ const lastEdited = await pad.getLastEdit();
+ return { lastEdited };
};
/**
@@ -501,21 +539,24 @@ Example returns:
@param {String} text the initial text of the pad
@param {String} authorId the id of the author, defaulting to empty string
*/
-exports.createPad = async (padID: string, text: string, authorId = '') => {
- if (padID) {
- // ensure there is no $ in the padID
- if (padID.indexOf('$') !== -1) {
- throw new CustomError("createPad can't create group pads", 'apierror');
- }
+exports.createPad = async (padID: string, text: string, authorId = "") => {
+ if (padID) {
+ // ensure there is no $ in the padID
+ if (padID.indexOf("$") !== -1) {
+ throw new CustomError("createPad can't create group pads", "apierror");
+ }
- // check for url special characters
- if (padID.match(/(\/|\?|&|#)/)) {
- throw new CustomError('malformed padID: Remove special characters', 'apierror');
- }
- }
+ // check for url special characters
+ if (padID.match(/(\/|\?|&|#)/)) {
+ throw new CustomError(
+ "malformed padID: Remove special characters",
+ "apierror",
+ );
+ }
+ }
- // create pad
- await getPadSafe(padID, false, text, authorId);
+ // create pad
+ await getPadSafe(padID, false, text, authorId);
};
/**
@@ -528,8 +569,8 @@ Example returns:
@param {String} padID the id of the pad
*/
exports.deletePad = async (padID: string) => {
- const pad = await getPadSafe(padID, true);
- await pad.remove();
+ const pad = await getPadSafe(padID, true);
+ await pad.remove();
};
/**
@@ -543,59 +584,69 @@ exports.deletePad = async (padID: string) => {
@param {Number} rev the revision number, defaulting to the latest revision
@param {String} authorId the id of the author, defaulting to empty string
*/
-exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
- // check if rev is a number
- if (rev === undefined) {
- throw new CustomError('rev is not defined', 'apierror');
- }
- rev = checkValidRev(rev);
+exports.restoreRevision = async (padID: string, rev: number, authorId = "") => {
+ // check if rev is a number
+ if (rev === undefined) {
+ throw new CustomError("rev is not defined", "apierror");
+ }
+ rev = checkValidRev(rev);
- // get the pad
- const pad = await getPadSafe(padID, true);
+ // get the pad
+ const pad = await getPadSafe(padID, true);
- // check if this is a valid revision
- if (rev > pad.getHeadRevisionNumber()) {
- throw new CustomError('rev is higher than the head revision of the pad', 'apierror');
- }
+ // check if this is a valid revision
+ if (rev > pad.getHeadRevisionNumber()) {
+ throw new CustomError(
+ "rev is higher than the head revision of the pad",
+ "apierror",
+ );
+ }
- const atext = await pad.getInternalRevisionAText(rev);
+ const atext = await pad.getInternalRevisionAText(rev);
- const oldText = pad.text();
- atext.text += '\n';
+ const oldText = pad.text();
+ atext.text += "\n";
- const eachAttribRun = (attribs: string[], func:Function) => {
- let textIndex = 0;
- const newTextStart = 0;
- const newTextEnd = atext.text.length;
- for (const op of Changeset.deserializeOps(attribs)) {
- const nextIndex = textIndex + op.chars;
- if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
- func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
- }
- textIndex = nextIndex;
- }
- };
+ const eachAttribRun = (attribs: string[], func: Function) => {
+ let textIndex = 0;
+ const newTextStart = 0;
+ const newTextEnd = atext.text.length;
+ for (const op of Changeset.deserializeOps(attribs)) {
+ const nextIndex = textIndex + op.chars;
+ if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
+ func(
+ Math.max(newTextStart, textIndex),
+ Math.min(newTextEnd, nextIndex),
+ op.attribs,
+ );
+ }
+ textIndex = nextIndex;
+ }
+ };
- // create a new changeset with a helper builder object
- const builder = Changeset.builder(oldText.length);
+ // create a new changeset with a helper builder object
+ const builder = Changeset.builder(oldText.length);
- // assemble each line into the builder
- eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
- builder.insert(atext.text.substring(start, end), attribs);
- });
+ // assemble each line into the builder
+ eachAttribRun(
+ atext.attribs,
+ (start: number, end: number, attribs: string[]) => {
+ builder.insert(atext.text.substring(start, end), attribs);
+ },
+ );
- const lastNewlinePos = oldText.lastIndexOf('\n');
- if (lastNewlinePos < 0) {
- builder.remove(oldText.length - 1, 0);
- } else {
- builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1);
- builder.remove(oldText.length - lastNewlinePos - 1, 0);
- }
+ const lastNewlinePos = oldText.lastIndexOf("\n");
+ if (lastNewlinePos < 0) {
+ builder.remove(oldText.length - 1, 0);
+ } else {
+ builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1);
+ builder.remove(oldText.length - lastNewlinePos - 1, 0);
+ }
- const changeset = builder.toString();
+ const changeset = builder.toString();
- await pad.appendRevision(changeset, authorId);
- await padMessageHandler.updatePadClients(pad);
+ await pad.appendRevision(changeset, authorId);
+ await padMessageHandler.updatePadClients(pad);
};
/**
@@ -610,9 +661,13 @@ Example returns:
@param {String} destinationID the id of the destination pad
@param {Boolean} force whether to overwrite the destination pad if it exists
*/
-exports.copyPad = async (sourceID: string, destinationID: string, force: boolean) => {
- const pad = await getPadSafe(sourceID, true);
- await pad.copy(destinationID, force);
+exports.copyPad = async (
+ sourceID: string,
+ destinationID: string,
+ force: boolean,
+) => {
+ const pad = await getPadSafe(sourceID, true);
+ await pad.copy(destinationID, force);
};
/**
@@ -628,9 +683,14 @@ Example returns:
@param {Boolean} force whether to overwrite the destination pad if it exists
@param {String} authorId the id of the author, defaulting to empty string
*/
-exports.copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId = '') => {
- const pad = await getPadSafe(sourceID, true);
- await pad.copyPadWithoutHistory(destinationID, force, authorId);
+exports.copyPadWithoutHistory = async (
+ sourceID: string,
+ destinationID: string,
+ force: boolean,
+ authorId = "",
+) => {
+ const pad = await getPadSafe(sourceID, true);
+ await pad.copyPadWithoutHistory(destinationID, force, authorId);
};
/**
@@ -645,10 +705,14 @@ Example returns:
@param {String} destinationID the id of the destination pad
@param {Boolean} force whether to overwrite the destination pad if it exists
*/
-exports.movePad = async (sourceID: string, destinationID: string, force:boolean) => {
- const pad = await getPadSafe(sourceID, true);
- await pad.copy(destinationID, force);
- await pad.remove();
+exports.movePad = async (
+ sourceID: string,
+ destinationID: string,
+ force: boolean,
+) => {
+ const pad = await getPadSafe(sourceID, true);
+ await pad.copy(destinationID, force);
+ await pad.remove();
};
/**
@@ -661,13 +725,13 @@ Example returns:
@param {String} padID the id of the pad
*/
exports.getReadOnlyID = async (padID: string) => {
- // we don't need the pad object, but this function does all the security stuff for us
- await getPadSafe(padID, true);
+ // we don't need the pad object, but this function does all the security stuff for us
+ await getPadSafe(padID, true);
- // get the readonlyId
- const readOnlyID = await readOnlyManager.getReadOnlyId(padID);
+ // get the readonlyId
+ const readOnlyID = await readOnlyManager.getReadOnlyId(padID);
- return {readOnlyID};
+ return { readOnlyID };
};
/**
@@ -680,13 +744,13 @@ Example returns:
@param {String} roID the readonly id of the pad
*/
exports.getPadID = async (roID: string) => {
- // get the PadId
- const padID = await readOnlyManager.getPadId(roID);
- if (padID == null) {
- throw new CustomError('padID does not exist', 'apierror');
- }
+ // get the PadId
+ const padID = await readOnlyManager.getPadId(roID);
+ if (padID == null) {
+ throw new CustomError("padID does not exist", "apierror");
+ }
- return {padID};
+ return { padID };
};
/**
@@ -699,19 +763,22 @@ Example returns:
@param {String} padID the id of the pad
@param {Boolean} publicStatus the public status of the pad
*/
-exports.setPublicStatus = async (padID: string, publicStatus: boolean|string) => {
- // ensure this is a group pad
- checkGroupPad(padID, 'publicStatus');
+exports.setPublicStatus = async (
+ padID: string,
+ publicStatus: boolean | string,
+) => {
+ // ensure this is a group pad
+ checkGroupPad(padID, "publicStatus");
- // get the pad
- const pad = await getPadSafe(padID, true);
+ // get the pad
+ const pad = await getPadSafe(padID, true);
- // convert string to boolean
- if (typeof publicStatus === 'string') {
- publicStatus = (publicStatus.toLowerCase() === 'true');
- }
+ // convert string to boolean
+ if (typeof publicStatus === "string") {
+ publicStatus = publicStatus.toLowerCase() === "true";
+ }
- await pad.setPublicStatus(publicStatus);
+ await pad.setPublicStatus(publicStatus);
};
/**
@@ -724,12 +791,12 @@ Example returns:
@param {String} padID the id of the pad
*/
exports.getPublicStatus = async (padID: string) => {
- // ensure this is a group pad
- checkGroupPad(padID, 'publicStatus');
+ // ensure this is a group pad
+ checkGroupPad(padID, "publicStatus");
- // get the pad
- const pad = await getPadSafe(padID, true);
- return {publicStatus: pad.getPublicStatus()};
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ return { publicStatus: pad.getPublicStatus() };
};
/**
@@ -742,10 +809,10 @@ Example returns:
@param {String} padID the id of the pad
*/
exports.listAuthorsOfPad = async (padID: string) => {
- // get the pad
- const pad = await getPadSafe(padID, true);
- const authorIDs = pad.getAllAuthors();
- return {authorIDs};
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ const authorIDs = pad.getAllAuthors();
+ return { authorIDs };
};
/**
@@ -774,8 +841,8 @@ Example returns:
*/
exports.sendClientsMessage = async (padID: string, msg: string) => {
- await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
- padMessageHandler.handleCustomMessage(padID, msg);
+ await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
+ padMessageHandler.handleCustomMessage(padID, msg);
};
/**
@@ -786,8 +853,7 @@ Example returns:
{"code":0,"message":"ok","data":null}
{"code":4,"message":"no or wrong API Key","data":null}
*/
-exports.checkToken = async () => {
-};
+exports.checkToken = async () => {};
/**
getChatHead(padID) returns the chatHead (last number of the last chat-message) of the pad
@@ -799,10 +865,10 @@ Example returns:
@param {String} padID the id of the pad
@return {Promise<{chatHead: number}>} the chatHead of the pad
*/
-exports.getChatHead = async (padID:string): Promise<{ chatHead: number; }> => {
- // get the pad
- const pad = await getPadSafe(padID, true);
- return {chatHead: pad.chatHead};
+exports.getChatHead = async (padID: string): Promise<{ chatHead: number }> => {
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ return { chatHead: pad.chatHead };
};
/**
@@ -825,35 +891,39 @@ Example returns:
@param {Number} startRev the start revision number
@param {Number} endRev the end revision number
*/
-exports.createDiffHTML = async (padID: string, startRev: number, endRev: number) => {
- // check if startRev is a number
- if (startRev !== undefined) {
- startRev = checkValidRev(startRev);
- }
+exports.createDiffHTML = async (
+ padID: string,
+ startRev: number,
+ endRev: number,
+) => {
+ // check if startRev is a number
+ if (startRev !== undefined) {
+ startRev = checkValidRev(startRev);
+ }
- // check if endRev is a number
- if (endRev !== undefined) {
- endRev = checkValidRev(endRev);
- }
+ // check if endRev is a number
+ if (endRev !== undefined) {
+ endRev = checkValidRev(endRev);
+ }
- // get the pad
- const pad = await getPadSafe(padID, true);
- const headRev = pad.getHeadRevisionNumber();
- if (startRev > headRev) startRev = headRev;
+ // get the pad
+ const pad = await getPadSafe(padID, true);
+ const headRev = pad.getHeadRevisionNumber();
+ if (startRev > headRev) startRev = headRev;
- if (endRev > headRev) endRev = headRev;
+ if (endRev > headRev) endRev = headRev;
- let padDiff;
- try {
- padDiff = new PadDiff(pad, startRev, endRev);
- } catch (e:any) {
- throw {stop: e.message};
- }
+ let padDiff;
+ try {
+ padDiff = new PadDiff(pad, startRev, endRev);
+ } catch (e: any) {
+ throw { stop: e.message };
+ }
- const html = await padDiff.getHtml();
- const authors = await padDiff.getAuthors();
+ const html = await padDiff.getHtml();
+ const authors = await padDiff.getAuthors();
- return {html, authors};
+ return { html, authors };
};
/* ********************
@@ -869,19 +939,21 @@ exports.createDiffHTML = async (padID: string, startRev: number, endRev: number)
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.getStats = async () => {
- const sessionInfos = padMessageHandler.sessioninfos;
+ const sessionInfos = padMessageHandler.sessioninfos;
- const sessionKeys = Object.keys(sessionInfos);
- // @ts-ignore
- const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId));
+ const sessionKeys = Object.keys(sessionInfos);
+ // @ts-ignore
+ const activePads = new Set(
+ Object.entries(sessionInfos).map((k) => k[1].padId),
+ );
- const {padIDs} = await padManager.listAllPads();
+ const { padIDs } = await padManager.listAllPads();
- return {
- totalPads: padIDs.length,
- totalSessions: sessionKeys.length,
- totalActivePads: activePads.size,
- };
+ return {
+ totalPads: padIDs.length,
+ totalSessions: sessionKeys.length,
+ totalActivePads: activePads.size,
+ };
};
/* ****************************
@@ -889,39 +961,46 @@ exports.getStats = async () => {
**************************** */
// gets a pad safe
-const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:string, authorId:string = '') => {
- // check if padID is a string
- if (typeof padID !== 'string') {
- throw new CustomError('padID is not a string', 'apierror');
- }
+const getPadSafe = async (
+ padID: string | object,
+ shouldExist: boolean,
+ text?: string,
+ authorId = "",
+) => {
+ // check if padID is a string
+ if (typeof padID !== "string") {
+ throw new CustomError("padID is not a string", "apierror");
+ }
- // check if the padID maches the requirements
- if (!padManager.isValidPadId(padID)) {
- throw new CustomError('padID did not match requirements', 'apierror');
- }
+ // check if the padID maches the requirements
+ if (!padManager.isValidPadId(padID)) {
+ throw new CustomError("padID did not match requirements", "apierror");
+ }
- // check if the pad exists
- const exists = await padManager.doesPadExists(padID);
+ // check if the pad exists
+ const exists = await padManager.doesPadExists(padID);
- if (!exists && shouldExist) {
- // does not exist, but should
- throw new CustomError('padID does not exist', 'apierror');
- }
+ if (!exists && shouldExist) {
+ // does not exist, but should
+ throw new CustomError("padID does not exist", "apierror");
+ }
- if (exists && !shouldExist) {
- // does exist, but shouldn't
- throw new CustomError('padID does already exist', 'apierror');
- }
+ if (exists && !shouldExist) {
+ // does exist, but shouldn't
+ throw new CustomError("padID does already exist", "apierror");
+ }
- // pad exists, let's get it
- return padManager.getPad(padID, text, authorId);
+ // pad exists, let's get it
+ return padManager.getPad(padID, text, authorId);
};
// checks if a padID is part of a group
const checkGroupPad = (padID: string, field: string) => {
- // ensure this is a group pad
- if (padID && padID.indexOf('$') === -1) {
- throw new CustomError(
- `You can only get/set the ${field} of pads that belong to a group`, 'apierror');
- }
+ // ensure this is a group pad
+ if (padID && padID.indexOf("$") === -1) {
+ throw new CustomError(
+ `You can only get/set the ${field} of pads that belong to a group`,
+ "apierror",
+ );
+ }
};
diff --git a/src/node/db/AuthorManager.ts b/src/node/db/AuthorManager.ts
index 2f4e7d751..37c51c02c 100644
--- a/src/node/db/AuthorManager.ts
+++ b/src/node/db/AuthorManager.ts
@@ -1,4 +1,3 @@
-'use strict';
/**
* The AuthorManager controlls all information about the Pad authors
*/
@@ -19,76 +18,79 @@
* limitations under the License.
*/
-const db = require('./DB');
-const CustomError = require('../utils/customError');
-const hooks = require('../../static/js/pluginfw/hooks.js');
-const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
+const db = require("./DB");
+const CustomError = require("../utils/customError");
+const hooks = require("../../static/js/pluginfw/hooks.js");
+const {
+ randomString,
+ padutils: { warnDeprecated },
+} = require("../../static/js/pad_utils");
exports.getColorPalette = () => [
- '#ffc7c7',
- '#fff1c7',
- '#e3ffc7',
- '#c7ffd5',
- '#c7ffff',
- '#c7d5ff',
- '#e3c7ff',
- '#ffc7f1',
- '#ffa8a8',
- '#ffe699',
- '#cfff9e',
- '#99ffb3',
- '#a3ffff',
- '#99b3ff',
- '#cc99ff',
- '#ff99e5',
- '#e7b1b1',
- '#e9dcAf',
- '#cde9af',
- '#bfedcc',
- '#b1e7e7',
- '#c3cdee',
- '#d2b8ea',
- '#eec3e6',
- '#e9cece',
- '#e7e0ca',
- '#d3e5c7',
- '#bce1c5',
- '#c1e2e2',
- '#c1c9e2',
- '#cfc1e2',
- '#e0bdd9',
- '#baded3',
- '#a0f8eb',
- '#b1e7e0',
- '#c3c8e4',
- '#cec5e2',
- '#b1d5e7',
- '#cda8f0',
- '#f0f0a8',
- '#f2f2a6',
- '#f5a8eb',
- '#c5f9a9',
- '#ececbb',
- '#e7c4bc',
- '#daf0b2',
- '#b0a0fd',
- '#bce2e7',
- '#cce2bb',
- '#ec9afe',
- '#edabbd',
- '#aeaeea',
- '#c4e7b1',
- '#d722bb',
- '#f3a5e7',
- '#ffa8a8',
- '#d8c0c5',
- '#eaaedd',
- '#adc6eb',
- '#bedad1',
- '#dee9af',
- '#e9afc2',
- '#f8d2a0',
- '#b3b3e6',
+ "#ffc7c7",
+ "#fff1c7",
+ "#e3ffc7",
+ "#c7ffd5",
+ "#c7ffff",
+ "#c7d5ff",
+ "#e3c7ff",
+ "#ffc7f1",
+ "#ffa8a8",
+ "#ffe699",
+ "#cfff9e",
+ "#99ffb3",
+ "#a3ffff",
+ "#99b3ff",
+ "#cc99ff",
+ "#ff99e5",
+ "#e7b1b1",
+ "#e9dcAf",
+ "#cde9af",
+ "#bfedcc",
+ "#b1e7e7",
+ "#c3cdee",
+ "#d2b8ea",
+ "#eec3e6",
+ "#e9cece",
+ "#e7e0ca",
+ "#d3e5c7",
+ "#bce1c5",
+ "#c1e2e2",
+ "#c1c9e2",
+ "#cfc1e2",
+ "#e0bdd9",
+ "#baded3",
+ "#a0f8eb",
+ "#b1e7e0",
+ "#c3c8e4",
+ "#cec5e2",
+ "#b1d5e7",
+ "#cda8f0",
+ "#f0f0a8",
+ "#f2f2a6",
+ "#f5a8eb",
+ "#c5f9a9",
+ "#ececbb",
+ "#e7c4bc",
+ "#daf0b2",
+ "#b0a0fd",
+ "#bce2e7",
+ "#cce2bb",
+ "#ec9afe",
+ "#edabbd",
+ "#aeaeea",
+ "#c4e7b1",
+ "#d722bb",
+ "#f3a5e7",
+ "#ffa8a8",
+ "#d8c0c5",
+ "#eaaedd",
+ "#adc6eb",
+ "#bedad1",
+ "#dee9af",
+ "#e9afc2",
+ "#f8d2a0",
+ "#b3b3e6",
];
/**
@@ -96,9 +98,9 @@ exports.getColorPalette = () => [
* @param {String} authorID The id of the author
*/
exports.doesAuthorExist = async (authorID: string) => {
- const author = await db.get(`globalAuthor:${authorID}`);
+ const author = await db.get(`globalAuthor:${authorID}`);
- return author != null;
+ return author != null;
};
/**
@@ -107,34 +109,33 @@ exports.doesAuthorExist = async (authorID: string) => {
*/
exports.doesAuthorExists = exports.doesAuthorExist;
-
/**
* Returns the AuthorID for a mapper. We can map using a mapperkey,
* so far this is token2author and mapper2author
* @param {String} mapperkey The database key name for this mapper
* @param {String} mapper The mapper
*/
-const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
- // try to map to an author
- const author = await db.get(`${mapperkey}:${mapper}`);
+const mapAuthorWithDBKey = async (mapperkey: string, mapper: string) => {
+ // try to map to an author
+ const author = await db.get(`${mapperkey}:${mapper}`);
- if (author == null) {
- // there is no author with this mapper, so create one
- const author = await exports.createAuthor(null);
+ if (author == null) {
+ // there is no author with this mapper, so create one
+ const author = await exports.createAuthor(null);
- // create the token2author relation
- await db.set(`${mapperkey}:${mapper}`, author.authorID);
+ // create the token2author relation
+ await db.set(`${mapperkey}:${mapper}`, author.authorID);
- // return the author
- return author;
- }
+ // return the author
+ return author;
+ }
- // there is an author with this mapper
- // update the timestamp of this author
- await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now());
+ // there is an author with this mapper
+ // update the timestamp of this author
+ await db.setSub(`globalAuthor:${author}`, ["timestamp"], Date.now());
- // return the author
- return {authorID: author};
+ // return the author
+ return { authorID: author };
};
/**
@@ -143,10 +144,10 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
* @return {Promise}
*/
const getAuthor4Token = async (token: string) => {
- const author = await mapAuthorWithDBKey('token2author', token);
+ const author = await mapAuthorWithDBKey("token2author", token);
- // return only the sub value authorID
- return author ? author.authorID : author;
+ // return only the sub value authorID
+ return author ? author.authorID : author;
};
/**
@@ -156,10 +157,10 @@ const getAuthor4Token = async (token: string) => {
* @return {Promise<*>}
*/
exports.getAuthorId = async (token: string, user: object) => {
- const context = {dbKey: token, token, user};
- let [authorId] = await hooks.aCallFirst('getAuthorId', context);
- if (!authorId) authorId = await getAuthor4Token(context.dbKey);
- return authorId;
+ const context = { dbKey: token, token, user };
+ let [authorId] = await hooks.aCallFirst("getAuthorId", context);
+ if (!authorId) authorId = await getAuthor4Token(context.dbKey);
+ return authorId;
};
/**
@@ -169,9 +170,10 @@ exports.getAuthorId = async (token: string, user: object) => {
* @param {String} token The token
*/
exports.getAuthor4Token = async (token: string) => {
- warnDeprecated(
- 'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
- return await getAuthor4Token(token);
+ warnDeprecated(
+ "AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead",
+ );
+ return await getAuthor4Token(token);
};
/**
@@ -179,95 +181,100 @@ exports.getAuthor4Token = async (token: string) => {
* @param {String} authorMapper The mapper
* @param {String} name The name of the author (optional)
*/
-exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string) => {
- const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
+exports.createAuthorIfNotExistsFor = async (
+ authorMapper: string,
+ name: string,
+) => {
+ const author = await mapAuthorWithDBKey("mapper2author", authorMapper);
- if (name) {
- // set the name of this author
- await exports.setAuthorName(author.authorID, name);
- }
+ if (name) {
+ // set the name of this author
+ await exports.setAuthorName(author.authorID, name);
+ }
- return author;
+ return author;
};
-
/**
* Internal function that creates the database entry for an author
* @param {String} name The name of the author
*/
exports.createAuthor = async (name: string) => {
- // create the new author name
- const author = `a.${randomString(16)}`;
+ // create the new author name
+ const author = `a.${randomString(16)}`;
- // create the globalAuthors db entry
- const authorObj = {
- colorId: Math.floor(Math.random() * (exports.getColorPalette().length)),
- name,
- timestamp: Date.now(),
- };
+ // create the globalAuthors db entry
+ const authorObj = {
+ colorId: Math.floor(Math.random() * exports.getColorPalette().length),
+ name,
+ timestamp: Date.now(),
+ };
- // set the global author db entry
- await db.set(`globalAuthor:${author}`, authorObj);
+ // set the global author db entry
+ await db.set(`globalAuthor:${author}`, authorObj);
- return {authorID: author};
+ return { authorID: author };
};
/**
* Returns the Author Obj of the author
* @param {String} author The id of the author
*/
-exports.getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`);
+exports.getAuthor = async (author: string) =>
+ await db.get(`globalAuthor:${author}`);
/**
* Returns the color Id of the author
* @param {String} author The id of the author
*/
-exports.getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']);
+exports.getAuthorColorId = async (author: string) =>
+ await db.getSub(`globalAuthor:${author}`, ["colorId"]);
/**
* Sets the color Id of the author
* @param {String} author The id of the author
* @param {String} colorId The color id of the author
*/
-exports.setAuthorColorId = async (author: string, colorId: string) => await db.setSub(
- `globalAuthor:${author}`, ['colorId'], colorId);
+exports.setAuthorColorId = async (author: string, colorId: string) =>
+ await db.setSub(`globalAuthor:${author}`, ["colorId"], colorId);
/**
* Returns the name of the author
* @param {String} author The id of the author
*/
-exports.getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']);
+exports.getAuthorName = async (author: string) =>
+ await db.getSub(`globalAuthor:${author}`, ["name"]);
/**
* Sets the name of the author
* @param {String} author The id of the author
* @param {String} name The name of the author
*/
-exports.setAuthorName = async (author: string, name: string) => await db.setSub(
- `globalAuthor:${author}`, ['name'], name);
+exports.setAuthorName = async (author: string, name: string) =>
+ await db.setSub(`globalAuthor:${author}`, ["name"], name);
/**
* Returns an array of all pads this author contributed to
* @param {String} authorID The id of the author
*/
exports.listPadsOfAuthor = async (authorID: string) => {
- /* There are two other places where this array is manipulated:
- * (1) When the author is added to a pad, the author object is also updated
- * (2) When a pad is deleted, each author of that pad is also updated
- */
+ /* There are two other places where this array is manipulated:
+ * (1) When the author is added to a pad, the author object is also updated
+ * (2) When a pad is deleted, each author of that pad is also updated
+ */
- // get the globalAuthor
- const author = await db.get(`globalAuthor:${authorID}`);
+ // get the globalAuthor
+ const author = await db.get(`globalAuthor:${authorID}`);
- if (author == null) {
- // author does not exist
- throw new CustomError('authorID does not exist', 'apierror');
- }
+ if (author == null) {
+ // author does not exist
+ throw new CustomError("authorID does not exist", "apierror");
+ }
- // everything is fine, return the pad IDs
- const padIDs = Object.keys(author.padIDs || {});
+ // everything is fine, return the pad IDs
+ const padIDs = Object.keys(author.padIDs || {});
- return {padIDs};
+ return { padIDs };
};
/**
@@ -276,25 +283,25 @@ exports.listPadsOfAuthor = async (authorID: string) => {
* @param {String} padID The id of the pad the author contributes to
*/
exports.addPad = async (authorID: string, padID: string) => {
- // get the entry
- const author = await db.get(`globalAuthor:${authorID}`);
+ // get the entry
+ const author = await db.get(`globalAuthor:${authorID}`);
- if (author == null) return;
+ if (author == null) return;
- /*
- * ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
- * to perform a strict check here
- */
- if (!author.padIDs) {
- // the entry doesn't exist so far, let's create it
- author.padIDs = {};
- }
+ /*
+ * ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
+ * to perform a strict check here
+ */
+ if (!author.padIDs) {
+ // the entry doesn't exist so far, let's create it
+ author.padIDs = {};
+ }
- // add the entry for this pad
- author.padIDs[padID] = 1; // anything, because value is not used
+ // add the entry for this pad
+ author.padIDs[padID] = 1; // anything, because value is not used
- // save the new element back
- await db.set(`globalAuthor:${authorID}`, author);
+ // save the new element back
+ await db.set(`globalAuthor:${authorID}`, author);
};
/**
@@ -303,13 +310,13 @@ exports.addPad = async (authorID: string, padID: string) => {
* @param {String} padID The id of the pad the author contributes to
*/
exports.removePad = async (authorID: string, padID: string) => {
- const author = await db.get(`globalAuthor:${authorID}`);
+ const author = await db.get(`globalAuthor:${authorID}`);
- if (author == null) return;
+ if (author == null) return;
- if (author.padIDs != null) {
- // remove pad from author
- delete author.padIDs[padID];
- await db.set(`globalAuthor:${authorID}`, author);
- }
+ if (author.padIDs != null) {
+ // remove pad from author
+ delete author.padIDs[padID];
+ await db.set(`globalAuthor:${authorID}`, author);
+ }
};
diff --git a/src/node/db/DB.ts b/src/node/db/DB.ts
index 542da6735..9b4434f35 100644
--- a/src/node/db/DB.ts
+++ b/src/node/db/DB.ts
@@ -1,5 +1,3 @@
-'use strict';
-
/**
* The DB Module provides a database initialized with the settings
* provided by the settings module
@@ -21,12 +19,12 @@
* limitations under the License.
*/
-import ueberDB from 'ueberdb2';
-const settings = require('../utils/Settings');
-import log4js from 'log4js';
-const stats = require('../stats')
+import ueberDB from "ueberdb2";
+const settings = require("../utils/Settings");
+import log4js from "log4js";
+const stats = require("../stats");
-const logger = log4js.getLogger('ueberDB');
+const logger = log4js.getLogger("ueberDB");
/**
* The UeberDB Object that provides the database functions
@@ -37,24 +35,30 @@ exports.db = null;
* Initializes the database with the settings provided by the settings module
*/
exports.init = async () => {
- exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger);
- await exports.db.init();
- if (exports.db.metrics != null) {
- for (const [metric, value] of Object.entries(exports.db.metrics)) {
- if (typeof value !== 'number') continue;
- stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
- }
- }
- for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
- const f = exports.db[fn];
- exports[fn] = async (...args:string[]) => await f.call(exports.db, ...args);
- Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
- Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
- }
+ exports.db = new ueberDB.Database(
+ settings.dbType,
+ settings.dbSettings,
+ null,
+ logger,
+ );
+ await exports.db.init();
+ if (exports.db.metrics != null) {
+ for (const [metric, value] of Object.entries(exports.db.metrics)) {
+ if (typeof value !== "number") continue;
+ stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]);
+ }
+ }
+ for (const fn of ["get", "set", "findKeys", "getSub", "setSub", "remove"]) {
+ const f = exports.db[fn];
+ exports[fn] = async (...args: string[]) =>
+ await f.call(exports.db, ...args);
+ Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
+ Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
+ }
};
-exports.shutdown = async (hookName: string, context:any) => {
- if (exports.db != null) await exports.db.close();
- exports.db = null;
- logger.log('Database closed');
+exports.shutdown = async (hookName: string, context: any) => {
+ if (exports.db != null) await exports.db.close();
+ exports.db = null;
+ logger.log("Database closed");
};
diff --git a/src/node/db/GroupManager.ts b/src/node/db/GroupManager.ts
index 0524c4eda..16dc6f5c8 100644
--- a/src/node/db/GroupManager.ts
+++ b/src/node/db/GroupManager.ts
@@ -1,4 +1,3 @@
-'use strict';
/**
* The Group Manager provides functions to manage groups in the database
*/
@@ -19,22 +18,22 @@
* limitations under the License.
*/
-const CustomError = require('../utils/customError');
-const randomString = require('../../static/js/pad_utils').randomString;
-const db = require('./DB');
-const padManager = require('./PadManager');
-const sessionManager = require('./SessionManager');
+const CustomError = require("../utils/customError");
+const randomString = require("../../static/js/pad_utils").randomString;
+const db = require("./DB");
+const padManager = require("./PadManager");
+const sessionManager = require("./SessionManager");
/**
* Lists all groups
* @return {Promise<{groupIDs: string[]}>} The ids of all groups
*/
exports.listAllGroups = async () => {
- let groups = await db.get('groups');
- groups = groups || {};
+ let groups = await db.get("groups");
+ groups = groups || {};
- const groupIDs = Object.keys(groups);
- return {groupIDs};
+ const groupIDs = Object.keys(groups);
+ return { groupIDs };
};
/**
@@ -43,38 +42,44 @@ exports.listAllGroups = async () => {
* @return {Promise} Resolves when the group is deleted
*/
exports.deleteGroup = async (groupID: string): Promise => {
- const group = await db.get(`group:${groupID}`);
+ const group = await db.get(`group:${groupID}`);
- // ensure group exists
- if (group == null) {
- // group does not exist
- throw new CustomError('groupID does not exist', 'apierror');
- }
+ // ensure group exists
+ if (group == null) {
+ // group does not exist
+ throw new CustomError("groupID does not exist", "apierror");
+ }
- // iterate through all pads of this group and delete them (in parallel)
- await Promise.all(Object.keys(group.pads).map(async (padId) => {
- const pad = await padManager.getPad(padId);
- await pad.remove();
- }));
+ // iterate through all pads of this group and delete them (in parallel)
+ await Promise.all(
+ Object.keys(group.pads).map(async (padId) => {
+ const pad = await padManager.getPad(padId);
+ await pad.remove();
+ }),
+ );
- // Delete associated sessions in parallel. This should be done before deleting the group2sessions
- // record because deleting a session updates the group2sessions record.
- const {sessionIDs = {}} = await db.get(`group2sessions:${groupID}`) || {};
- await Promise.all(Object.keys(sessionIDs).map(async (sessionId) => {
- await sessionManager.deleteSession(sessionId);
- }));
+ // Delete associated sessions in parallel. This should be done before deleting the group2sessions
+ // record because deleting a session updates the group2sessions record.
+ const { sessionIDs = {} } = (await db.get(`group2sessions:${groupID}`)) || {};
+ await Promise.all(
+ Object.keys(sessionIDs).map(async (sessionId) => {
+ await sessionManager.deleteSession(sessionId);
+ }),
+ );
- await Promise.all([
- db.remove(`group2sessions:${groupID}`),
- // UeberDB's setSub() method atomically reads the record, updates the appropriate property, and
- // writes the result. Setting a property to `undefined` deletes that property (JSON.stringify()
- // ignores such properties).
- db.setSub('groups', [groupID], undefined),
- ...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)),
- ]);
+ await Promise.all([
+ db.remove(`group2sessions:${groupID}`),
+ // UeberDB's setSub() method atomically reads the record, updates the appropriate property, and
+ // writes the result. Setting a property to `undefined` deletes that property (JSON.stringify()
+ // ignores such properties).
+ db.setSub("groups", [groupID], undefined),
+ ...Object.keys(group.mappings || {}).map(
+ async (m) => await db.remove(`mapper2group:${m}`),
+ ),
+ ]);
- // Remove the group record after updating the `groups` record so that the state is consistent.
- await db.remove(`group:${groupID}`);
+ // Remove the group record after updating the `groups` record so that the state is consistent.
+ await db.remove(`group:${groupID}`);
};
/**
@@ -83,10 +88,10 @@ exports.deleteGroup = async (groupID: string): Promise => {
* @return {Promise} Resolves to true if the group exists
*/
exports.doesGroupExist = async (groupID: string) => {
- // try to get the group entry
- const group = await db.get(`group:${groupID}`);
+ // try to get the group entry
+ const group = await db.get(`group:${groupID}`);
- return (group != null);
+ return group != null;
};
/**
@@ -94,13 +99,13 @@ exports.doesGroupExist = async (groupID: string) => {
* @return {Promise<{groupID: string}>} the id of the new group
*/
exports.createGroup = async () => {
- const groupID = `g.${randomString(16)}`;
- await db.set(`group:${groupID}`, {pads: {}, mappings: {}});
- // Add the group to the `groups` record after the group's individual record is created so that
- // the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates
- // the appropriate property, and writes the result.
- await db.setSub('groups', [groupID], 1);
- return {groupID};
+ const groupID = `g.${randomString(16)}`;
+ await db.set(`group:${groupID}`, { pads: {}, mappings: {} });
+ // Add the group to the `groups` record after the group's individual record is created so that
+ // the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates
+ // the appropriate property, and writes the result.
+ await db.setSub("groups", [groupID], 1);
+ return { groupID };
};
/**
@@ -108,22 +113,22 @@ exports.createGroup = async () => {
* @param groupMapper the mapper of the group
* @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID
*/
-exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
- if (typeof groupMapper !== 'string') {
- throw new CustomError('groupMapper is not a string', 'apierror');
- }
- const groupID = await db.get(`mapper2group:${groupMapper}`);
- if (groupID && await exports.doesGroupExist(groupID)) return {groupID};
- const result = await exports.createGroup();
- await Promise.all([
- db.set(`mapper2group:${groupMapper}`, result.groupID),
- // Remember the mapping in the group record so that it can be cleaned up when the group is
- // deleted. Although the core Etherpad API does not support multiple mappings for the same
- // group, the database record does support multiple mappings in case a plugin decides to extend
- // the core Etherpad functionality. (It's also easy to implement it this way.)
- db.setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1),
- ]);
- return result;
+exports.createGroupIfNotExistsFor = async (groupMapper: string | object) => {
+ if (typeof groupMapper !== "string") {
+ throw new CustomError("groupMapper is not a string", "apierror");
+ }
+ const groupID = await db.get(`mapper2group:${groupMapper}`);
+ if (groupID && (await exports.doesGroupExist(groupID))) return { groupID };
+ const result = await exports.createGroup();
+ await Promise.all([
+ db.set(`mapper2group:${groupMapper}`, result.groupID),
+ // Remember the mapping in the group record so that it can be cleaned up when the group is
+ // deleted. Although the core Etherpad API does not support multiple mappings for the same
+ // group, the database record does support multiple mappings in case a plugin decides to extend
+ // the core Etherpad functionality. (It's also easy to implement it this way.)
+ db.setSub(`group:${result.groupID}`, ["mappings", groupMapper], 1),
+ ]);
+ return result;
};
/**
@@ -134,32 +139,37 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => {
* @param {String} authorId The id of the author
* @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad
*/
-exports.createGroupPad = async (groupID: string, padName: string, text: string, authorId: string = ''): Promise<{ padID: string; }> => {
- // create the padID
- const padID = `${groupID}$${padName}`;
+exports.createGroupPad = async (
+ groupID: string,
+ padName: string,
+ text: string,
+ authorId = "",
+): Promise<{ padID: string }> => {
+ // create the padID
+ const padID = `${groupID}$${padName}`;
- // ensure group exists
- const groupExists = await exports.doesGroupExist(groupID);
+ // ensure group exists
+ const groupExists = await exports.doesGroupExist(groupID);
- if (!groupExists) {
- throw new CustomError('groupID does not exist', 'apierror');
- }
+ if (!groupExists) {
+ throw new CustomError("groupID does not exist", "apierror");
+ }
- // ensure pad doesn't exist already
- const padExists = await padManager.doesPadExists(padID);
+ // ensure pad doesn't exist already
+ const padExists = await padManager.doesPadExists(padID);
- if (padExists) {
- // pad exists already
- throw new CustomError('padName does already exist', 'apierror');
- }
+ if (padExists) {
+ // pad exists already
+ throw new CustomError("padName does already exist", "apierror");
+ }
- // create the pad
- await padManager.getPad(padID, text, authorId);
+ // create the pad
+ await padManager.getPad(padID, text, authorId);
- // create an entry in the group for this pad
- await db.setSub(`group:${groupID}`, ['pads', padID], 1);
+ // create an entry in the group for this pad
+ await db.setSub(`group:${groupID}`, ["pads", padID], 1);
- return {padID};
+ return { padID };
};
/**
@@ -167,17 +177,17 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string,
* @param {String} groupID The id of the group
* @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group
*/
-exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => {
- const exists = await exports.doesGroupExist(groupID);
+exports.listPads = async (groupID: string): Promise<{ padIDs: string[] }> => {
+ const exists = await exports.doesGroupExist(groupID);
- // ensure the group exists
- if (!exists) {
- throw new CustomError('groupID does not exist', 'apierror');
- }
+ // ensure the group exists
+ if (!exists) {
+ throw new CustomError("groupID does not exist", "apierror");
+ }
- // group exists, let's get the pads
- const result = await db.getSub(`group:${groupID}`, ['pads']);
- const padIDs = Object.keys(result);
+ // group exists, let's get the pads
+ const result = await db.getSub(`group:${groupID}`, ["pads"]);
+ const padIDs = Object.keys(result);
- return {padIDs};
+ return { padIDs };
};
diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts
index fa4af994d..a991d32ca 100644
--- a/src/node/db/Pad.ts
+++ b/src/node/db/Pad.ts
@@ -1,30 +1,31 @@
-'use strict';
-import {Database} from "ueberdb2";
-import {AChangeSet, APool, AText} from "../types/PadType";
-import {MapArrayType} from "../types/MapType";
+import type { Database } from "ueberdb2";
+import type { MapArrayType } from "../types/MapType";
+import type { AChangeSet, APool, AText } from "../types/PadType";
/**
* The pad object, defined with joose
*/
-const AttributeMap = require('../../static/js/AttributeMap');
-const Changeset = require('../../static/js/Changeset');
-const ChatMessage = require('../../static/js/ChatMessage');
-const AttributePool = require('../../static/js/AttributePool');
-const Stream = require('../utils/Stream');
-const assert = require('assert').strict;
-const db = require('./DB');
-const settings = require('../utils/Settings');
-const authorManager = require('./AuthorManager');
-const padManager = require('./PadManager');
-const padMessageHandler = require('../handler/PadMessageHandler');
-const groupManager = require('./GroupManager');
-const CustomError = require('../utils/customError');
-const readOnlyManager = require('./ReadOnlyManager');
-const randomString = require('../utils/randomstring');
-const hooks = require('../../static/js/pluginfw/hooks');
-const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
-const promises = require('../utils/promises');
+const AttributeMap = require("../../static/js/AttributeMap");
+const Changeset = require("../../static/js/Changeset");
+const ChatMessage = require("../../static/js/ChatMessage");
+const AttributePool = require("../../static/js/AttributePool");
+const Stream = require("../utils/Stream");
+const assert = require("assert").strict;
+const db = require("./DB");
+const settings = require("../utils/Settings");
+const authorManager = require("./AuthorManager");
+const padManager = require("./PadManager");
+const padMessageHandler = require("../handler/PadMessageHandler");
+const groupManager = require("./GroupManager");
+const CustomError = require("../utils/customError");
+const readOnlyManager = require("./ReadOnlyManager");
+const randomString = require("../utils/randomstring");
+const hooks = require("../../static/js/pluginfw/hooks");
+const {
+ padutils: { warnDeprecated },
+} = require("../../static/js/pad_utils");
+const promises = require("../utils/promises");
/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
@@ -32,734 +33,842 @@ const promises = require('../utils/promises');
* @param {String} txt The text to clean
* @returns {String} The cleaned text
*/
-exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
- .replace(/\r/g, '\n')
- .replace(/\t/g, ' ')
- .replace(/\xa0/g, ' ');
+exports.cleanText = (txt: string): string =>
+ txt
+ .replace(/\r\n/g, "\n")
+ .replace(/\r/g, "\n")
+ .replace(/\t/g, " ")
+ .replace(/\xa0/g, " ");
class Pad {
- private db: Database;
- private atext: AText;
- private pool: APool;
- private head: number;
- private chatHead: number;
- private publicStatus: boolean;
- private id: string;
- private savedRevisions: any[];
- /**
- * @param id
- * @param [database] - Database object to access this pad's records (and only this pad's records;
- * the shared global Etherpad database object is still used for all other pad accesses, such
- * as copying the pad). Defaults to the shared global Etherpad database object. This parameter
- * can be used to shard pad storage across multiple database backends, to put each pad in its
- * own database table, or to validate imported pad data before it is written to the database.
- */
- constructor(id:string, database = db) {
- this.db = database;
- this.atext = Changeset.makeAText('\n');
- this.pool = new AttributePool();
- this.head = -1;
- this.chatHead = -1;
- this.publicStatus = false;
- this.id = id;
- this.savedRevisions = [];
- }
-
- apool() {
- return this.pool;
- }
-
- getHeadRevisionNumber() {
- return this.head;
- }
-
- getSavedRevisionsNumber() {
- return this.savedRevisions.length;
- }
-
- getSavedRevisionsList() {
- const savedRev = this.savedRevisions.map((rev) => rev.revNum);
- savedRev.sort((a, b) => a - b);
- return savedRev;
- }
-
- getPublicStatus() {
- return this.publicStatus;
- }
-
- /**
- * Appends a new revision
- * @param {Object} aChangeset The changeset to append to the pad
- * @param {String} authorId The id of the author
- * @return {Promise}
- */
- async appendRevision(aChangeset:AChangeSet, authorId = '') {
- const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
- if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
- this.head !== -1) {
- return this.head;
- }
- Changeset.copyAText(newAText, this.atext);
-
- const newRev = ++this.head;
-
- // ex. getNumForAuthor
- if (authorId !== '') this.pool.putAttrib(['author', authorId]);
-
- const hook = this.head === 0 ? 'padCreate' : 'padUpdate';
- await Promise.all([
- // @ts-ignore
- this.db.set(`pad:${this.id}:revs:${newRev}`, {
- changeset: aChangeset,
- meta: {
- author: authorId,
- timestamp: Date.now(),
- ...newRev === this.getKeyRevisionNumber(newRev) ? {
- pool: this.pool,
- atext: this.atext,
- } : {},
- },
- }),
- this.saveToDatabase(),
- authorId && authorManager.addPad(authorId, this.id),
- hooks.aCallAll(hook, {
- pad: this,
- authorId,
- get author() {
- warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
- return this.authorId;
- },
- set author(authorId) {
- warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
- this.authorId = authorId;
- },
- ...this.head === 0 ? {} : {
- revs: newRev,
- changeset: aChangeset,
- },
- }),
- ]);
- return newRev;
- }
-
- toJSON() {
- const o:Pad = {...this, pool: this.pool.toJsonable()};
- // @ts-ignore
- delete o.db;
- // @ts-ignore
- delete o.id;
- return o;
- }
-
- // save all attributes to the database
- async saveToDatabase() {
- // @ts-ignore
- await this.db.set(`pad:${this.id}`, this);
- }
-
- // get time of last edit (changeset application)
- async getLastEdit() {
- const revNum = this.getHeadRevisionNumber();
- // @ts-ignore
- return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']);
- }
-
- async getRevisionChangeset(revNum: number) {
- // @ts-ignore
- return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']);
- }
-
- async getRevisionAuthor(revNum: number) {
- // @ts-ignore
- return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']);
- }
-
- async getRevisionDate(revNum: number) {
- // @ts-ignore
- return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']);
- }
-
- /**
- * @param {number} revNum - Must be a key revision number (see `getKeyRevisionNumber`).
- * @returns The attribute text stored at `revNum`.
- */
- async _getKeyRevisionAText(revNum: number) {
- // @ts-ignore
- return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'atext']);
- }
-
- /**
- * Returns all authors that worked on this pad
- * @return {[String]} The id of authors who contributed to this pad
- */
- getAllAuthors() {
- const authorIds = [];
-
- for (const key in this.pool.numToAttrib) {
- if (this.pool.numToAttrib[key][0] === 'author' && this.pool.numToAttrib[key][1] !== '') {
- authorIds.push(this.pool.numToAttrib[key][1]);
- }
- }
-
- return authorIds;
- }
-
- async getInternalRevisionAText(targetRev: number) {
- const keyRev = this.getKeyRevisionNumber(targetRev);
- const headRev = this.getHeadRevisionNumber();
- if (targetRev > headRev) targetRev = headRev;
- const [keyAText, changesets] = await Promise.all([
- this._getKeyRevisionAText(keyRev),
- Promise.all(
- Stream.range(keyRev + 1, targetRev + 1).map(this.getRevisionChangeset.bind(this))),
- ]);
- const apool = this.apool();
- let atext = keyAText;
- for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
- return atext;
- }
-
- async getRevision(revNum: number) {
- return await this.db.get(`pad:${this.id}:revs:${revNum}`);
- }
-
- async getAllAuthorColors() {
- const authorIds = this.getAllAuthors();
- const returnTable:MapArrayType = {};
- const colorPalette = authorManager.getColorPalette();
-
- await Promise.all(
- authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId:string) => {
- // colorId might be a hex color or an number out of the palette
- returnTable[authorId] = colorPalette[colorId] || colorId;
- })));
-
- return returnTable;
- }
-
- getValidRevisionRange(startRev: any, endRev:any) {
- startRev = parseInt(startRev, 10);
- const head = this.getHeadRevisionNumber();
- endRev = endRev ? parseInt(endRev, 10) : head;
-
- if (isNaN(startRev) || startRev < 0 || startRev > head) {
- startRev = null;
- }
-
- if (isNaN(endRev) || endRev < startRev) {
- endRev = null;
- } else if (endRev > head) {
- endRev = head;
- }
-
- if (startRev != null && endRev != null) {
- return {startRev, endRev};
- }
- return null;
- }
-
- getKeyRevisionNumber(revNum: number) {
- return Math.floor(revNum / 100) * 100;
- }
-
- /**
- * @returns {string} The pad's text.
- */
- text(): string {
- return this.atext.text;
- }
-
- /**
- * Splices text into the pad. If the result of the splice does not end with a newline, one will be
- * automatically appended.
- *
- * @param {number} start - Location in pad text to start removing and inserting characters. Must
- * be a non-negative integer less than or equal to `this.text().length`.
- * @param {number} ndel - Number of characters to remove starting at `start`. Must be a
- * non-negative integer less than or equal to `this.text().length - start`.
- * @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted).
- * @param {string} [authorId] - Author ID of the user making the change (if applicable).
- */
- async spliceText(start:number, ndel:number, ins: string, authorId: string = '') {
- if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`);
- if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`);
- const orig = this.text();
- assert(orig.endsWith('\n'));
- if (start + ndel > orig.length) throw new RangeError('start/delete past the end of the text');
- ins = exports.cleanText(ins);
- const willEndWithNewline =
- start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
- ins.endsWith('\n') ||
- (!ins && start > 0 && orig[start - 1] === '\n');
- if (!willEndWithNewline) ins += '\n';
- if (ndel === 0 && ins.length === 0) return;
- const changeset = Changeset.makeSplice(orig, start, ndel, ins);
- await this.appendRevision(changeset, authorId);
- }
-
- /**
- * Replaces the pad's text with new text.
- *
- * @param {string} newText - The pad's new text. If this string does not end with a newline, one
- * will be automatically appended.
- * @param {string} [authorId] - The author ID of the user that initiated the change, if
- * applicable.
- */
- async setText(newText: string, authorId = '') {
- await this.spliceText(0, this.text().length, newText, authorId);
- }
-
- /**
- * Appends text to the pad.
- *
- * @param {string} newText - Text to insert just BEFORE the pad's existing terminating newline.
- * @param {string} [authorId] - The author ID of the user that initiated the change, if
- * applicable.
- */
- async appendText(newText:string, authorId = '') {
- await this.spliceText(this.text().length - 1, 0, newText, authorId);
- }
-
- /**
- * Adds a chat message to the pad, including saving it to the database.
- *
- * @param {(ChatMessage|string)} msgOrText - Either a chat message object (recommended) or a
- * string containing the raw text of the user's chat message (deprecated).
- * @param {?string} [authorId] - The user's author ID. Deprecated; use `msgOrText.authorId`
- * instead.
- * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
- * `msgOrText.time` instead.
- */
- async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
- const msg =
- msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
- this.chatHead++;
- await Promise.all([
- // Don't save the display name in the database because the user can change it at any time. The
- // `displayName` property will be populated with the current value when the message is read
- // from the database.
- this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {...msg, displayName: undefined}),
- this.saveToDatabase(),
- ]);
- }
-
- /**
- * @param {number} entryNum - ID of the desired chat message.
- * @returns {?ChatMessage}
- */
- async getChatMessage(entryNum: number) {
- const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`);
- if (entry == null) return null;
- const message = ChatMessage.fromObject(entry);
- message.displayName = await authorManager.getAuthorName(message.authorId);
- return message;
- }
-
- /**
- * @param {number} start - ID of the first desired chat message.
- * @param {number} end - ID of the last desired chat message.
- * @returns {ChatMessage[]} Any existing messages with IDs between `start` (inclusive) and `end`
- * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open
- * interval as is typical in code.
- */
- async getChatMessages(start: string, end: number) {
- const entries =
- await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this)));
-
- // sort out broken chat entries
- // it looks like in happened in the past that the chat head was
- // incremented, but the chat message wasn't added
- return entries.filter((entry) => {
- const pass = (entry != null);
- if (!pass) {
- console.warn(`WARNING: Found broken chat entry in pad ${this.id}`);
- }
- return pass;
- });
- }
-
- async init(text:string, authorId = '') {
- // try to load the pad
- const value = await this.db.get(`pad:${this.id}`);
-
- // if this pad exists, load it
- if (value != null) {
- Object.assign(this, value);
- if ('pool' in value) this.pool = new AttributePool().fromJsonable(value.pool);
- } else {
- if (text == null) {
- const context = {pad: this, authorId, type: 'text', content: settings.defaultPadText};
- await hooks.aCallAll('padDefaultContent', context);
- if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
- text = exports.cleanText(context.content);
- }
- const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
- await this.appendRevision(firstChangeset, authorId);
- }
- await hooks.aCallAll('padLoad', {pad: this});
- }
-
- async copy(destinationID: string, force: boolean) {
- // Kick everyone from this pad.
- // This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
- // Do we really need to kick everyone out?
- // padMessageHandler.kickSessionsFromPad(sourceID);
-
- // flush the source pad:
- await this.saveToDatabase();
-
- // if it's a group pad, let's make sure the group exists.
- const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
-
- // if force is true and already exists a Pad with the same id, remove that Pad
- await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
-
- const copyRecord = async (keySuffix: string) => {
- const val = await this.db.get(`pad:${this.id}${keySuffix}`);
- await db.set(`pad:${destinationID}${keySuffix}`, val);
- };
-
- const promises = (function* () {
- yield copyRecord('');
- // @ts-ignore
- yield* Stream.range(0, this.head + 1).map((i) => copyRecord(`:revs:${i}`));
- // @ts-ignore
- yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`));
- // @ts-ignore
- yield this.copyAuthorInfoToDestinationPad(destinationID);
- if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
- }).call(this);
- for (const p of new Stream(promises).batch(100).buffer(99)) await p;
-
- // Initialize the new pad (will update the listAllPads cache)
- const dstPad = await padManager.getPad(destinationID, null);
-
- // let the plugins know the pad was copied
- await hooks.aCallAll('padCopy', {
- get originalPad() {
- warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
- return this.srcPad;
- },
- get destinationID() {
- warnDeprecated(
- 'padCopy destinationID context property is deprecated; use dstPad.id instead');
- return this.dstPad.id;
- },
- srcPad: this,
- dstPad,
- });
-
- return {padID: destinationID};
- }
-
- async checkIfGroupExistAndReturnIt(destinationID: string) {
- let destGroupID:false|string = false;
-
- if (destinationID.indexOf('$') >= 0) {
- destGroupID = destinationID.split('$')[0];
- const groupExists = await groupManager.doesGroupExist(destGroupID);
-
- // group does not exist
- if (!groupExists) {
- throw new CustomError('groupID does not exist for destinationID', 'apierror');
- }
- }
- return destGroupID;
- }
-
- async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) {
- // if the pad exists, we should abort, unless forced.
- const exists = await padManager.doesPadExist(destinationID);
-
- // allow force to be a string
- if (typeof force === 'string') {
- force = (force.toLowerCase() === 'true');
- } else {
- force = !!force;
- }
-
- if (exists) {
- if (!force) {
- console.error('erroring out without force');
- throw new CustomError('destinationID already exists', 'apierror');
- }
-
- // exists and forcing
- const pad = await padManager.getPad(destinationID);
- await pad.remove();
- }
- }
-
- async copyAuthorInfoToDestinationPad(destinationID: string) {
- // add the new sourcePad to all authors who contributed to the old one
- await Promise.all(this.getAllAuthors().map(
- (authorID) => authorManager.addPad(authorID, destinationID)));
- }
-
- async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') {
- // flush the source pad
- this.saveToDatabase();
-
- // if it's a group pad, let's make sure the group exists.
- const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
-
- // if force is true and already exists a Pad with the same id, remove that Pad
- await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
-
- await this.copyAuthorInfoToDestinationPad(destinationID);
-
- // Group pad? Add it to the group's list
- if (destGroupID) {
- await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
- }
-
- // initialize the pad with a new line to avoid getting the defaultText
- const dstPad = await padManager.getPad(destinationID, '\n', authorId);
- dstPad.pool = this.pool.clone();
-
- const oldAText = this.atext;
-
- // based on Changeset.makeSplice
- const assem = Changeset.smartOpAssembler();
- for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
- assem.endDocument();
-
- // although we have instantiated the dstPad with '\n', an additional '\n' is
- // added internally, so the pad text on the revision 0 is "\n\n"
- const oldLength = 2;
-
- const newLength = assem.getLengthChange();
- const newText = oldAText.text;
-
- // create a changeset that removes the previous text and add the newText with
- // all atributes present on the source pad
- const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
- dstPad.appendRevision(changeset, authorId);
-
- await hooks.aCallAll('padCopy', {
- get originalPad() {
- warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
- return this.srcPad;
- },
- get destinationID() {
- warnDeprecated(
- 'padCopy destinationID context property is deprecated; use dstPad.id instead');
- return this.dstPad.id;
- },
- srcPad: this,
- dstPad,
- });
-
- return {padID: destinationID};
- }
-
- async remove() {
- const padID = this.id;
- const p = [];
-
- // kick everyone from this pad
- padMessageHandler.kickSessionsFromPad(padID);
-
- // delete all relations - the original code used async.parallel but
- // none of the operations except getting the group depended on callbacks
- // so the database operations here are just started and then left to
- // run to completion
-
- // is it a group pad? -> delete the entry of this pad in the group
- if (padID.indexOf('$') >= 0) {
- // it is a group pad
- const groupID = padID.substring(0, padID.indexOf('$'));
- const group = await db.get(`group:${groupID}`);
-
- // remove the pad entry
- delete group.pads[padID];
-
- // set the new value
- p.push(db.set(`group:${groupID}`, group));
- }
-
- // remove the readonly entries
- p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => {
- await db.remove(`readonly2pad:${readonlyID}`);
- }));
- p.push(db.remove(`pad2readonly:${padID}`));
-
- // delete all chat messages
- p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
- await this.db.remove(`pad:${this.id}:chat:${i}`, null);
- }));
-
- // delete all revisions
- p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => {
- await this.db.remove(`pad:${this.id}:revs:${i}`, null);
- }));
-
- // remove pad from all authors who contributed
- this.getAllAuthors().forEach((authorId) => {
- p.push(authorManager.removePad(authorId, padID));
- });
-
- // delete the pad entry and delete pad from padManager
- p.push(padManager.removePad(padID));
- p.push(hooks.aCallAll('padRemove', {
- get padID() {
- warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
- return this.pad.id;
- },
- pad: this,
- }));
- await Promise.all(p);
- }
-
- // set in db
- async setPublicStatus(publicStatus: boolean) {
- this.publicStatus = publicStatus;
- await this.saveToDatabase();
- }
-
- async addSavedRevision(revNum: string, savedById: string, label: string) {
- // if this revision is already saved, return silently
- for (const i in this.savedRevisions) {
- if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) {
- return;
- }
- }
-
- // build the saved revision object
- const savedRevision:MapArrayType = {};
- savedRevision.revNum = revNum;
- savedRevision.savedById = savedById;
- savedRevision.label = label || `Revision ${revNum}`;
- savedRevision.timestamp = Date.now();
- savedRevision.id = randomString(10);
-
- // save this new saved revision
- this.savedRevisions.push(savedRevision);
- await this.saveToDatabase();
- }
-
- getSavedRevisions() {
- return this.savedRevisions;
- }
-
- /**
- * Asserts that all pad data is consistent. Throws if inconsistent.
- */
- async check() {
- assert(this.id != null);
- assert.equal(typeof this.id, 'string');
-
- const head = this.getHeadRevisionNumber();
- assert(head != null);
- assert(Number.isInteger(head));
- assert(head >= -1);
-
- const savedRevisionsList = this.getSavedRevisionsList();
- assert(Array.isArray(savedRevisionsList));
- assert.equal(this.getSavedRevisionsNumber(), savedRevisionsList.length);
- let prevSavedRev = null;
- for (const rev of savedRevisionsList) {
- assert(rev != null);
- assert(Number.isInteger(rev));
- assert(rev >= 0);
- assert(rev <= head);
- assert(prevSavedRev == null || rev > prevSavedRev);
- prevSavedRev = rev;
- }
- const savedRevisions = this.getSavedRevisions();
- assert(Array.isArray(savedRevisions));
- assert.equal(savedRevisions.length, savedRevisionsList.length);
- const savedRevisionsIds = new Set();
- for (const savedRev of savedRevisions) {
- assert(savedRev != null);
- assert.equal(typeof savedRev, 'object');
- assert(savedRevisionsList.includes(savedRev.revNum));
- assert(savedRev.id != null);
- assert.equal(typeof savedRev.id, 'string');
- assert(!savedRevisionsIds.has(savedRev.id));
- savedRevisionsIds.add(savedRev.id);
- }
-
- const pool = this.apool();
- assert(pool instanceof AttributePool);
- await pool.check();
-
- const authorIds = new Set();
- pool.eachAttrib((k, v) => {
- if (k === 'author' && v) authorIds.add(v);
- });
- const revs = Stream.range(0, head + 1)
- .map(async (r: number) => {
- const isKeyRev = r === this.getKeyRevisionNumber(r);
- try {
- return await Promise.all([
- r,
- this.getRevisionChangeset(r),
- this.getRevisionAuthor(r),
- this.getRevisionDate(r),
- isKeyRev,
- isKeyRev ? this._getKeyRevisionAText(r) : null,
- ]);
- } catch (err:any) {
- err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
- throw err;
- }
- })
- .batch(100).buffer(99);
- let atext = Changeset.makeAText('\n');
- for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
- try {
- assert(authorId != null);
- assert.equal(typeof authorId, 'string');
- if (authorId) authorIds.add(authorId);
- assert(timestamp != null);
- assert.equal(typeof timestamp, 'number');
- assert(timestamp > 0);
- assert(changeset != null);
- assert.equal(typeof changeset, 'string');
- Changeset.checkRep(changeset);
- const unpacked = Changeset.unpack(changeset);
- let text = atext.text;
- for (const op of Changeset.deserializeOps(unpacked.ops)) {
- if (['=', '-'].includes(op.opcode)) {
- assert(text.length >= op.chars);
- const consumed = text.slice(0, op.chars);
- const nlines = (consumed.match(/\n/g) || []).length;
- assert.equal(op.lines, nlines);
- if (op.lines > 0) assert(consumed.endsWith('\n'));
- text = text.slice(op.chars);
- }
- assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
- }
- atext = Changeset.applyToAText(changeset, atext, pool);
- if (isKeyRev) assert.deepEqual(keyAText, atext);
- } catch (err:any) {
- err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
- throw err;
- }
- }
- assert.equal(this.text(), atext.text);
- assert.deepEqual(this.atext, atext);
- assert.deepEqual(this.getAllAuthors().sort(), [...authorIds].sort());
-
- assert(this.chatHead != null);
- assert(Number.isInteger(this.chatHead));
- assert(this.chatHead >= -1);
- const chats = Stream.range(0, this.chatHead + 1)
- .map(async (c: number) => {
- try {
- const msg = await this.getChatMessage(c);
- assert(msg != null);
- assert(msg instanceof ChatMessage);
- } catch (err:any) {
- err.message = `(pad ${this.id} chat message ${c}) ${err.message}`;
- throw err;
- }
- })
- .batch(100).buffer(99);
- for (const p of chats) await p;
-
- await hooks.aCallAll('padCheck', {pad: this});
- }
+ private db: Database;
+ private atext: AText;
+ private pool: APool;
+ private head: number;
+ private chatHead: number;
+ private publicStatus: boolean;
+ private id: string;
+ private savedRevisions: any[];
+ /**
+ * @param id
+ * @param [database] - Database object to access this pad's records (and only this pad's records;
+ * the shared global Etherpad database object is still used for all other pad accesses, such
+ * as copying the pad). Defaults to the shared global Etherpad database object. This parameter
+ * can be used to shard pad storage across multiple database backends, to put each pad in its
+ * own database table, or to validate imported pad data before it is written to the database.
+ */
+ constructor(id: string, database = db) {
+ this.db = database;
+ this.atext = Changeset.makeAText("\n");
+ this.pool = new AttributePool();
+ this.head = -1;
+ this.chatHead = -1;
+ this.publicStatus = false;
+ this.id = id;
+ this.savedRevisions = [];
+ }
+
+ apool() {
+ return this.pool;
+ }
+
+ getHeadRevisionNumber() {
+ return this.head;
+ }
+
+ getSavedRevisionsNumber() {
+ return this.savedRevisions.length;
+ }
+
+ getSavedRevisionsList() {
+ const savedRev = this.savedRevisions.map((rev) => rev.revNum);
+ savedRev.sort((a, b) => a - b);
+ return savedRev;
+ }
+
+ getPublicStatus() {
+ return this.publicStatus;
+ }
+
+ /**
+ * Appends a new revision
+ * @param {Object} aChangeset The changeset to append to the pad
+ * @param {String} authorId The id of the author
+ * @return {Promise}
+ */
+ async appendRevision(aChangeset: AChangeSet, authorId = "") {
+ const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
+ if (
+ newAText.text === this.atext.text &&
+ newAText.attribs === this.atext.attribs &&
+ this.head !== -1
+ ) {
+ return this.head;
+ }
+ Changeset.copyAText(newAText, this.atext);
+
+ const newRev = ++this.head;
+
+ // ex. getNumForAuthor
+ if (authorId !== "") this.pool.putAttrib(["author", authorId]);
+
+ const hook = this.head === 0 ? "padCreate" : "padUpdate";
+ await Promise.all([
+ // @ts-ignore
+ this.db.set(`pad:${this.id}:revs:${newRev}`, {
+ changeset: aChangeset,
+ meta: {
+ author: authorId,
+ timestamp: Date.now(),
+ ...(newRev === this.getKeyRevisionNumber(newRev)
+ ? {
+ pool: this.pool,
+ atext: this.atext,
+ }
+ : {}),
+ },
+ }),
+ this.saveToDatabase(),
+ authorId && authorManager.addPad(authorId, this.id),
+ hooks.aCallAll(hook, {
+ pad: this,
+ authorId,
+ get author() {
+ warnDeprecated(
+ `${hook} hook author context is deprecated; use authorId instead`,
+ );
+ return this.authorId;
+ },
+ set author(authorId) {
+ warnDeprecated(
+ `${hook} hook author context is deprecated; use authorId instead`,
+ );
+ this.authorId = authorId;
+ },
+ ...(this.head === 0
+ ? {}
+ : {
+ revs: newRev,
+ changeset: aChangeset,
+ }),
+ }),
+ ]);
+ return newRev;
+ }
+
+ toJSON() {
+ const o: Pad = { ...this, pool: this.pool.toJsonable() };
+ // @ts-ignore
+ delete o.db;
+ // @ts-ignore
+ delete o.id;
+ return o;
+ }
+
+ // save all attributes to the database
+ async saveToDatabase() {
+ // @ts-ignore
+ await this.db.set(`pad:${this.id}`, this);
+ }
+
+ // get time of last edit (changeset application)
+ async getLastEdit() {
+ const revNum = this.getHeadRevisionNumber();
+ // @ts-ignore
+ return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
+ "meta",
+ "timestamp",
+ ]);
+ }
+
+ async getRevisionChangeset(revNum: number) {
+ // @ts-ignore
+ return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ["changeset"]);
+ }
+
+ async getRevisionAuthor(revNum: number) {
+ // @ts-ignore
+ return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
+ "meta",
+ "author",
+ ]);
+ }
+
+ async getRevisionDate(revNum: number) {
+ // @ts-ignore
+ return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
+ "meta",
+ "timestamp",
+ ]);
+ }
+
+ /**
+ * @param {number} revNum - Must be a key revision number (see `getKeyRevisionNumber`).
+ * @returns The attribute text stored at `revNum`.
+ */
+ async _getKeyRevisionAText(revNum: number) {
+ // @ts-ignore
+ return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, [
+ "meta",
+ "atext",
+ ]);
+ }
+
+ /**
+ * Returns all authors that worked on this pad
+ * @return {[String]} The id of authors who contributed to this pad
+ */
+ getAllAuthors() {
+ const authorIds = [];
+
+ for (const key in this.pool.numToAttrib) {
+ if (
+ this.pool.numToAttrib[key][0] === "author" &&
+ this.pool.numToAttrib[key][1] !== ""
+ ) {
+ authorIds.push(this.pool.numToAttrib[key][1]);
+ }
+ }
+
+ return authorIds;
+ }
+
+ async getInternalRevisionAText(targetRev: number) {
+ const keyRev = this.getKeyRevisionNumber(targetRev);
+ const headRev = this.getHeadRevisionNumber();
+ if (targetRev > headRev) targetRev = headRev;
+ const [keyAText, changesets] = await Promise.all([
+ this._getKeyRevisionAText(keyRev),
+ Promise.all(
+ Stream.range(keyRev + 1, targetRev + 1).map(
+ this.getRevisionChangeset.bind(this),
+ ),
+ ),
+ ]);
+ const apool = this.apool();
+ let atext = keyAText;
+ for (const cs of changesets)
+ atext = Changeset.applyToAText(cs, atext, apool);
+ return atext;
+ }
+
+ async getRevision(revNum: number) {
+ return await this.db.get(`pad:${this.id}:revs:${revNum}`);
+ }
+
+ async getAllAuthorColors() {
+ const authorIds = this.getAllAuthors();
+ const returnTable: MapArrayType = {};
+ const colorPalette = authorManager.getColorPalette();
+
+ await Promise.all(
+ authorIds.map((authorId) =>
+ authorManager.getAuthorColorId(authorId).then((colorId: string) => {
+ // colorId might be a hex color or an number out of the palette
+ returnTable[authorId] = colorPalette[colorId] || colorId;
+ }),
+ ),
+ );
+
+ return returnTable;
+ }
+
+ getValidRevisionRange(startRev: any, endRev: any) {
+ startRev = Number.parseInt(startRev, 10);
+ const head = this.getHeadRevisionNumber();
+ endRev = endRev ? Number.parseInt(endRev, 10) : head;
+
+ if (isNaN(startRev) || startRev < 0 || startRev > head) {
+ startRev = null;
+ }
+
+ if (isNaN(endRev) || endRev < startRev) {
+ endRev = null;
+ } else if (endRev > head) {
+ endRev = head;
+ }
+
+ if (startRev != null && endRev != null) {
+ return { startRev, endRev };
+ }
+ return null;
+ }
+
+ getKeyRevisionNumber(revNum: number) {
+ return Math.floor(revNum / 100) * 100;
+ }
+
+ /**
+ * @returns {string} The pad's text.
+ */
+ text(): string {
+ return this.atext.text;
+ }
+
+ /**
+ * Splices text into the pad. If the result of the splice does not end with a newline, one will be
+ * automatically appended.
+ *
+ * @param {number} start - Location in pad text to start removing and inserting characters. Must
+ * be a non-negative integer less than or equal to `this.text().length`.
+ * @param {number} ndel - Number of characters to remove starting at `start`. Must be a
+ * non-negative integer less than or equal to `this.text().length - start`.
+ * @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted).
+ * @param {string} [authorId] - Author ID of the user making the change (if applicable).
+ */
+ async spliceText(start: number, ndel: number, ins: string, authorId = "") {
+ if (start < 0)
+ throw new RangeError(`start index must be non-negative (is ${start})`);
+ if (ndel < 0)
+ throw new RangeError(
+ `characters to delete must be non-negative (is ${ndel})`,
+ );
+ const orig = this.text();
+ assert(orig.endsWith("\n"));
+ if (start + ndel > orig.length)
+ throw new RangeError("start/delete past the end of the text");
+ ins = exports.cleanText(ins);
+ const willEndWithNewline =
+ start + ndel < orig.length || // Keeping last char (which is guaranteed to be a newline).
+ ins.endsWith("\n") ||
+ (!ins && start > 0 && orig[start - 1] === "\n");
+ if (!willEndWithNewline) ins += "\n";
+ if (ndel === 0 && ins.length === 0) return;
+ const changeset = Changeset.makeSplice(orig, start, ndel, ins);
+ await this.appendRevision(changeset, authorId);
+ }
+
+ /**
+ * Replaces the pad's text with new text.
+ *
+ * @param {string} newText - The pad's new text. If this string does not end with a newline, one
+ * will be automatically appended.
+ * @param {string} [authorId] - The author ID of the user that initiated the change, if
+ * applicable.
+ */
+ async setText(newText: string, authorId = "") {
+ await this.spliceText(0, this.text().length, newText, authorId);
+ }
+
+ /**
+ * Appends text to the pad.
+ *
+ * @param {string} newText - Text to insert just BEFORE the pad's existing terminating newline.
+ * @param {string} [authorId] - The author ID of the user that initiated the change, if
+ * applicable.
+ */
+ async appendText(newText: string, authorId = "") {
+ await this.spliceText(this.text().length - 1, 0, newText, authorId);
+ }
+
+ /**
+ * Adds a chat message to the pad, including saving it to the database.
+ *
+ * @param {(ChatMessage|string)} msgOrText - Either a chat message object (recommended) or a
+ * string containing the raw text of the user's chat message (deprecated).
+ * @param {?string} [authorId] - The user's author ID. Deprecated; use `msgOrText.authorId`
+ * instead.
+ * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
+ * `msgOrText.time` instead.
+ */
+ async appendChatMessage(
+ msgOrText: string | typeof ChatMessage,
+ authorId = null,
+ time = null,
+ ) {
+ const msg =
+ msgOrText instanceof ChatMessage
+ ? msgOrText
+ : new ChatMessage(msgOrText, authorId, time);
+ this.chatHead++;
+ await Promise.all([
+ // Don't save the display name in the database because the user can change it at any time. The
+ // `displayName` property will be populated with the current value when the message is read
+ // from the database.
+ this.db.set(`pad:${this.id}:chat:${this.chatHead}`, {
+ ...msg,
+ displayName: undefined,
+ }),
+ this.saveToDatabase(),
+ ]);
+ }
+
+ /**
+ * @param {number} entryNum - ID of the desired chat message.
+ * @returns {?ChatMessage}
+ */
+ async getChatMessage(entryNum: number) {
+ const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`);
+ if (entry == null) return null;
+ const message = ChatMessage.fromObject(entry);
+ message.displayName = await authorManager.getAuthorName(message.authorId);
+ return message;
+ }
+
+ /**
+ * @param {number} start - ID of the first desired chat message.
+ * @param {number} end - ID of the last desired chat message.
+ * @returns {ChatMessage[]} Any existing messages with IDs between `start` (inclusive) and `end`
+ * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open
+ * interval as is typical in code.
+ */
+ async getChatMessages(start: string, end: number) {
+ const entries = await Promise.all(
+ Stream.range(start, end + 1).map(this.getChatMessage.bind(this)),
+ );
+
+ // sort out broken chat entries
+ // it looks like in happened in the past that the chat head was
+ // incremented, but the chat message wasn't added
+ return entries.filter((entry) => {
+ const pass = entry != null;
+ if (!pass) {
+ console.warn(`WARNING: Found broken chat entry in pad ${this.id}`);
+ }
+ return pass;
+ });
+ }
+
+ async init(text: string, authorId = "") {
+ // try to load the pad
+ const value = await this.db.get(`pad:${this.id}`);
+
+ // if this pad exists, load it
+ if (value != null) {
+ Object.assign(this, value);
+ if ("pool" in value)
+ this.pool = new AttributePool().fromJsonable(value.pool);
+ } else {
+ if (text == null) {
+ const context = {
+ pad: this,
+ authorId,
+ type: "text",
+ content: settings.defaultPadText,
+ };
+ await hooks.aCallAll("padDefaultContent", context);
+ if (context.type !== "text")
+ throw new Error(`unsupported content type: ${context.type}`);
+ text = exports.cleanText(context.content);
+ }
+ const firstChangeset = Changeset.makeSplice("\n", 0, 0, text);
+ await this.appendRevision(firstChangeset, authorId);
+ }
+ await hooks.aCallAll("padLoad", { pad: this });
+ }
+
+ async copy(destinationID: string, force: boolean) {
+ // Kick everyone from this pad.
+ // This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
+ // Do we really need to kick everyone out?
+ // padMessageHandler.kickSessionsFromPad(sourceID);
+
+ // flush the source pad:
+ await this.saveToDatabase();
+
+ // if it's a group pad, let's make sure the group exists.
+ const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
+
+ // if force is true and already exists a Pad with the same id, remove that Pad
+ await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
+
+ const copyRecord = async (keySuffix: string) => {
+ const val = await this.db.get(`pad:${this.id}${keySuffix}`);
+ await db.set(`pad:${destinationID}${keySuffix}`, val);
+ };
+
+ const promises = function* () {
+ yield copyRecord("");
+ // @ts-ignore
+ yield* Stream.range(0, this.head + 1).map((i) =>
+ copyRecord(`:revs:${i}`),
+ );
+ // @ts-ignore
+ yield* Stream.range(0, this.chatHead + 1).map((i) =>
+ copyRecord(`:chat:${i}`),
+ );
+ // @ts-ignore
+ yield this.copyAuthorInfoToDestinationPad(destinationID);
+ if (destGroupID)
+ yield db.setSub(`group:${destGroupID}`, ["pads", destinationID], 1);
+ }.call(this);
+ for (const p of new Stream(promises).batch(100).buffer(99)) await p;
+
+ // Initialize the new pad (will update the listAllPads cache)
+ const dstPad = await padManager.getPad(destinationID, null);
+
+ // let the plugins know the pad was copied
+ await hooks.aCallAll("padCopy", {
+ get originalPad() {
+ warnDeprecated(
+ "padCopy originalPad context property is deprecated; use srcPad instead",
+ );
+ return this.srcPad;
+ },
+ get destinationID() {
+ warnDeprecated(
+ "padCopy destinationID context property is deprecated; use dstPad.id instead",
+ );
+ return this.dstPad.id;
+ },
+ srcPad: this,
+ dstPad,
+ });
+
+ return { padID: destinationID };
+ }
+
+ async checkIfGroupExistAndReturnIt(destinationID: string) {
+ let destGroupID: false | string = false;
+
+ if (destinationID.indexOf("$") >= 0) {
+ destGroupID = destinationID.split("$")[0];
+ const groupExists = await groupManager.doesGroupExist(destGroupID);
+
+ // group does not exist
+ if (!groupExists) {
+ throw new CustomError(
+ "groupID does not exist for destinationID",
+ "apierror",
+ );
+ }
+ }
+ return destGroupID;
+ }
+
+ async removePadIfForceIsTrueAndAlreadyExist(
+ destinationID: string,
+ force: boolean | string,
+ ) {
+ // if the pad exists, we should abort, unless forced.
+ const exists = await padManager.doesPadExist(destinationID);
+
+ // allow force to be a string
+ if (typeof force === "string") {
+ force = force.toLowerCase() === "true";
+ } else {
+ force = !!force;
+ }
+
+ if (exists) {
+ if (!force) {
+ console.error("erroring out without force");
+ throw new CustomError("destinationID already exists", "apierror");
+ }
+
+ // exists and forcing
+ const pad = await padManager.getPad(destinationID);
+ await pad.remove();
+ }
+ }
+
+ async copyAuthorInfoToDestinationPad(destinationID: string) {
+ // add the new sourcePad to all authors who contributed to the old one
+ await Promise.all(
+ this.getAllAuthors().map((authorID) =>
+ authorManager.addPad(authorID, destinationID),
+ ),
+ );
+ }
+
+ async copyPadWithoutHistory(
+ destinationID: string,
+ force: string | boolean,
+ authorId = "",
+ ) {
+ // flush the source pad
+ this.saveToDatabase();
+
+ // if it's a group pad, let's make sure the group exists.
+ const destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
+
+ // if force is true and already exists a Pad with the same id, remove that Pad
+ await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
+
+ await this.copyAuthorInfoToDestinationPad(destinationID);
+
+ // Group pad? Add it to the group's list
+ if (destGroupID) {
+ await db.setSub(`group:${destGroupID}`, ["pads", destinationID], 1);
+ }
+
+ // initialize the pad with a new line to avoid getting the defaultText
+ const dstPad = await padManager.getPad(destinationID, "\n", authorId);
+ dstPad.pool = this.pool.clone();
+
+ const oldAText = this.atext;
+
+ // based on Changeset.makeSplice
+ const assem = Changeset.smartOpAssembler();
+ for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
+ assem.endDocument();
+
+ // although we have instantiated the dstPad with '\n', an additional '\n' is
+ // added internally, so the pad text on the revision 0 is "\n\n"
+ const oldLength = 2;
+
+ const newLength = assem.getLengthChange();
+ const newText = oldAText.text;
+
+ // create a changeset that removes the previous text and add the newText with
+ // all atributes present on the source pad
+ const changeset = Changeset.pack(
+ oldLength,
+ newLength,
+ assem.toString(),
+ newText,
+ );
+ dstPad.appendRevision(changeset, authorId);
+
+ await hooks.aCallAll("padCopy", {
+ get originalPad() {
+ warnDeprecated(
+ "padCopy originalPad context property is deprecated; use srcPad instead",
+ );
+ return this.srcPad;
+ },
+ get destinationID() {
+ warnDeprecated(
+ "padCopy destinationID context property is deprecated; use dstPad.id instead",
+ );
+ return this.dstPad.id;
+ },
+ srcPad: this,
+ dstPad,
+ });
+
+ return { padID: destinationID };
+ }
+
+ async remove() {
+ const padID = this.id;
+ const p = [];
+
+ // kick everyone from this pad
+ padMessageHandler.kickSessionsFromPad(padID);
+
+ // delete all relations - the original code used async.parallel but
+ // none of the operations except getting the group depended on callbacks
+ // so the database operations here are just started and then left to
+ // run to completion
+
+ // is it a group pad? -> delete the entry of this pad in the group
+ if (padID.indexOf("$") >= 0) {
+ // it is a group pad
+ const groupID = padID.substring(0, padID.indexOf("$"));
+ const group = await db.get(`group:${groupID}`);
+
+ // remove the pad entry
+ delete group.pads[padID];
+
+ // set the new value
+ p.push(db.set(`group:${groupID}`, group));
+ }
+
+ // remove the readonly entries
+ p.push(
+ readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => {
+ await db.remove(`readonly2pad:${readonlyID}`);
+ }),
+ );
+ p.push(db.remove(`pad2readonly:${padID}`));
+
+ // delete all chat messages
+ p.push(
+ promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
+ await this.db.remove(`pad:${this.id}:chat:${i}`, null);
+ }),
+ );
+
+ // delete all revisions
+ p.push(
+ promises.timesLimit(this.head + 1, 500, async (i: string) => {
+ await this.db.remove(`pad:${this.id}:revs:${i}`, null);
+ }),
+ );
+
+ // remove pad from all authors who contributed
+ this.getAllAuthors().forEach((authorId) => {
+ p.push(authorManager.removePad(authorId, padID));
+ });
+
+ // delete the pad entry and delete pad from padManager
+ p.push(padManager.removePad(padID));
+ p.push(
+ hooks.aCallAll("padRemove", {
+ get padID() {
+ warnDeprecated(
+ "padRemove padID context property is deprecated; use pad.id instead",
+ );
+ return this.pad.id;
+ },
+ pad: this,
+ }),
+ );
+ await Promise.all(p);
+ }
+
+ // set in db
+ async setPublicStatus(publicStatus: boolean) {
+ this.publicStatus = publicStatus;
+ await this.saveToDatabase();
+ }
+
+ async addSavedRevision(revNum: string, savedById: string, label: string) {
+ // if this revision is already saved, return silently
+ for (const i in this.savedRevisions) {
+ if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) {
+ return;
+ }
+ }
+
+ // build the saved revision object
+ const savedRevision: MapArrayType = {};
+ savedRevision.revNum = revNum;
+ savedRevision.savedById = savedById;
+ savedRevision.label = label || `Revision ${revNum}`;
+ savedRevision.timestamp = Date.now();
+ savedRevision.id = randomString(10);
+
+ // save this new saved revision
+ this.savedRevisions.push(savedRevision);
+ await this.saveToDatabase();
+ }
+
+ getSavedRevisions() {
+ return this.savedRevisions;
+ }
+
+ /**
+ * Asserts that all pad data is consistent. Throws if inconsistent.
+ */
+ async check() {
+ assert(this.id != null);
+ assert.equal(typeof this.id, "string");
+
+ const head = this.getHeadRevisionNumber();
+ assert(head != null);
+ assert(Number.isInteger(head));
+ assert(head >= -1);
+
+ const savedRevisionsList = this.getSavedRevisionsList();
+ assert(Array.isArray(savedRevisionsList));
+ assert.equal(this.getSavedRevisionsNumber(), savedRevisionsList.length);
+ let prevSavedRev = null;
+ for (const rev of savedRevisionsList) {
+ assert(rev != null);
+ assert(Number.isInteger(rev));
+ assert(rev >= 0);
+ assert(rev <= head);
+ assert(prevSavedRev == null || rev > prevSavedRev);
+ prevSavedRev = rev;
+ }
+ const savedRevisions = this.getSavedRevisions();
+ assert(Array.isArray(savedRevisions));
+ assert.equal(savedRevisions.length, savedRevisionsList.length);
+ const savedRevisionsIds = new Set();
+ for (const savedRev of savedRevisions) {
+ assert(savedRev != null);
+ assert.equal(typeof savedRev, "object");
+ assert(savedRevisionsList.includes(savedRev.revNum));
+ assert(savedRev.id != null);
+ assert.equal(typeof savedRev.id, "string");
+ assert(!savedRevisionsIds.has(savedRev.id));
+ savedRevisionsIds.add(savedRev.id);
+ }
+
+ const pool = this.apool();
+ assert(pool instanceof AttributePool);
+ await pool.check();
+
+ const authorIds = new Set();
+ pool.eachAttrib((k, v) => {
+ if (k === "author" && v) authorIds.add(v);
+ });
+ const revs = Stream.range(0, head + 1)
+ .map(async (r: number) => {
+ const isKeyRev = r === this.getKeyRevisionNumber(r);
+ try {
+ return await Promise.all([
+ r,
+ this.getRevisionChangeset(r),
+ this.getRevisionAuthor(r),
+ this.getRevisionDate(r),
+ isKeyRev,
+ isKeyRev ? this._getKeyRevisionAText(r) : null,
+ ]);
+ } catch (err: any) {
+ err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
+ throw err;
+ }
+ })
+ .batch(100)
+ .buffer(99);
+ let atext = Changeset.makeAText("\n");
+ for await (const [
+ r,
+ changeset,
+ authorId,
+ timestamp,
+ isKeyRev,
+ keyAText,
+ ] of revs) {
+ try {
+ assert(authorId != null);
+ assert.equal(typeof authorId, "string");
+ if (authorId) authorIds.add(authorId);
+ assert(timestamp != null);
+ assert.equal(typeof timestamp, "number");
+ assert(timestamp > 0);
+ assert(changeset != null);
+ assert.equal(typeof changeset, "string");
+ Changeset.checkRep(changeset);
+ const unpacked = Changeset.unpack(changeset);
+ let text = atext.text;
+ for (const op of Changeset.deserializeOps(unpacked.ops)) {
+ if (["=", "-"].includes(op.opcode)) {
+ assert(text.length >= op.chars);
+ const consumed = text.slice(0, op.chars);
+ const nlines = (consumed.match(/\n/g) || []).length;
+ assert.equal(op.lines, nlines);
+ if (op.lines > 0) assert(consumed.endsWith("\n"));
+ text = text.slice(op.chars);
+ }
+ assert.equal(
+ op.attribs,
+ AttributeMap.fromString(op.attribs, pool).toString(),
+ );
+ }
+ atext = Changeset.applyToAText(changeset, atext, pool);
+ if (isKeyRev) assert.deepEqual(keyAText, atext);
+ } catch (err: any) {
+ err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
+ throw err;
+ }
+ }
+ assert.equal(this.text(), atext.text);
+ assert.deepEqual(this.atext, atext);
+ assert.deepEqual(this.getAllAuthors().sort(), [...authorIds].sort());
+
+ assert(this.chatHead != null);
+ assert(Number.isInteger(this.chatHead));
+ assert(this.chatHead >= -1);
+ const chats = Stream.range(0, this.chatHead + 1)
+ .map(async (c: number) => {
+ try {
+ const msg = await this.getChatMessage(c);
+ assert(msg != null);
+ assert(msg instanceof ChatMessage);
+ } catch (err: any) {
+ err.message = `(pad ${this.id} chat message ${c}) ${err.message}`;
+ throw err;
+ }
+ })
+ .batch(100)
+ .buffer(99);
+ for (const p of chats) await p;
+
+ await hooks.aCallAll("padCheck", { pad: this });
+ }
}
exports.Pad = Pad;
diff --git a/src/node/db/PadManager.ts b/src/node/db/PadManager.ts
index 54dbbf089..d509c91fe 100644
--- a/src/node/db/PadManager.ts
+++ b/src/node/db/PadManager.ts
@@ -1,4 +1,3 @@
-'use strict';
/**
* The Pad Manager is a Factory for pad Objects
*/
@@ -19,13 +18,13 @@
* limitations under the License.
*/
-import {MapArrayType} from "../types/MapType";
-import {PadType} from "../types/PadType";
+import type { MapArrayType } from "../types/MapType";
+import type { PadType } from "../types/PadType";
-const CustomError = require('../utils/customError');
-const Pad = require('../db/Pad');
-const db = require('./DB');
-const settings = require('../utils/Settings');
+const CustomError = require("../utils/customError");
+const Pad = require("../db/Pad");
+const db = require("./DB");
+const settings = require("../utils/Settings");
/**
* A cache of all loaded Pads.
@@ -38,18 +37,16 @@ const settings = require('../utils/Settings');
* If this is needed in other places, it would be wise to make this a prototype
* that's defined somewhere more sensible.
*/
-const globalPads:MapArrayType = {
- get(name: string)
- {
- return this[`:${name}`];
- },
- set(name: string, value: any)
- {
- this[`:${name}`] = value;
- },
- remove(name: string) {
- delete this[`:${name}`];
- },
+const globalPads: MapArrayType = {
+ get(name: string) {
+ return this[`:${name}`];
+ },
+ set(name: string, value: any) {
+ this[`:${name}`] = value;
+ },
+ remove(name: string) {
+ delete this[`:${name}`];
+ },
};
/**
@@ -57,45 +54,45 @@ const globalPads:MapArrayType = {
*
* Updated without db access as new pads are created/old ones removed.
*/
-const padList = new class {
- private _cachedList: string[] | null;
- private _list: Set;
- private _loaded: Promise | null;
- constructor() {
- this._cachedList = null;
- this._list = new Set();
- this._loaded = null;
- }
+const padList = new (class {
+ private _cachedList: string[] | null;
+ private _list: Set;
+ private _loaded: Promise | null;
+ constructor() {
+ this._cachedList = null;
+ this._list = new Set();
+ this._loaded = null;
+ }
- /**
- * Returns all pads in alphabetical order as array.
- * @returns {Promise} A promise that resolves to an array of pad IDs.
- */
- async getPads() {
- if (!this._loaded) {
- this._loaded = (async () => {
- const dbData = await db.findKeys('pad:*', '*:*:*');
- if (dbData == null) return;
- for (const val of dbData) this.addPad(val.replace(/^pad:/, ''));
- })();
- }
- await this._loaded;
- if (!this._cachedList) this._cachedList = [...this._list].sort();
- return this._cachedList;
- }
+ /**
+ * Returns all pads in alphabetical order as array.
+ * @returns {Promise} A promise that resolves to an array of pad IDs.
+ */
+ async getPads() {
+ if (!this._loaded) {
+ this._loaded = (async () => {
+ const dbData = await db.findKeys("pad:*", "*:*:*");
+ if (dbData == null) return;
+ for (const val of dbData) this.addPad(val.replace(/^pad:/, ""));
+ })();
+ }
+ await this._loaded;
+ if (!this._cachedList) this._cachedList = [...this._list].sort();
+ return this._cachedList;
+ }
- addPad(name: string) {
- if (this._list.has(name)) return;
- this._list.add(name);
- this._cachedList = null;
- }
+ addPad(name: string) {
+ if (this._list.has(name)) return;
+ this._list.add(name);
+ this._cachedList = null;
+ }
- removePad(name: string) {
- if (!this._list.has(name)) return;
- this._list.delete(name);
- this._cachedList = null;
- }
-}();
+ removePad(name: string) {
+ if (!this._list.has(name)) return;
+ this._list.delete(name);
+ this._cachedList = null;
+ }
+})();
// initialises the all-knowing data structure
@@ -106,57 +103,58 @@ const padList = new class {
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
* applicable).
*/
-exports.getPad = async (id: string, text?: string|null, authorId:string|null = ''):Promise => {
- // check if this is a valid padId
- if (!exports.isValidPadId(id)) {
- throw new CustomError(`${id} is not a valid padId`, 'apierror');
- }
+exports.getPad = async (
+ id: string,
+ text?: string | null,
+ authorId: string | null = "",
+): Promise => {
+ // check if this is a valid padId
+ if (!exports.isValidPadId(id)) {
+ throw new CustomError(`${id} is not a valid padId`, "apierror");
+ }
- // check if this is a valid text
- if (text != null) {
- // check if text is a string
- if (typeof text !== 'string') {
- throw new CustomError('text is not a string', 'apierror');
- }
+ // check if this is a valid text
+ if (text != null) {
+ // check if text is a string
+ if (typeof text !== "string") {
+ throw new CustomError("text is not a string", "apierror");
+ }
- // check if text is less than 100k chars
- if (text.length > 100000) {
- throw new CustomError('text must be less than 100k chars', 'apierror');
- }
- }
+ // check if text is less than 100k chars
+ if (text.length > 100000) {
+ throw new CustomError("text must be less than 100k chars", "apierror");
+ }
+ }
- let pad = globalPads.get(id);
+ let pad = globalPads.get(id);
- // return pad if it's already loaded
- if (pad != null) {
- return pad;
- }
+ // return pad if it's already loaded
+ if (pad != null) {
+ return pad;
+ }
- // try to load pad
- pad = new Pad.Pad(id);
+ // try to load pad
+ pad = new Pad.Pad(id);
- // initialize the pad
- await pad.init(text, authorId);
- globalPads.set(id, pad);
- padList.addPad(id);
+ // initialize the pad
+ await pad.init(text, authorId);
+ globalPads.set(id, pad);
+ padList.addPad(id);
- return pad;
+ return pad;
};
exports.listAllPads = async () => {
- const padIDs = await padList.getPads();
+ const padIDs = await padList.getPads();
- return {padIDs};
+ return { padIDs };
};
-
-
-
// checks if a pad exists
exports.doesPadExist = async (padId: string) => {
- const value = await db.get(`pad:${padId}`);
+ const value = await db.get(`pad:${padId}`);
- return (value != null && value.atext);
+ return value != null && value.atext;
};
// alias for backwards compatibility
@@ -167,44 +165,45 @@ exports.doesPadExists = exports.doesPadExist;
* time, and allow us to "play back" these changes so legacy padIds can be found.
*/
const padIdTransforms = [
- [/\s+/g, '_'],
- [/:+/g, '_'],
+ [/\s+/g, "_"],
+ [/:+/g, "_"],
];
// returns a sanitized padId, respecting legacy pad id formats
exports.sanitizePadId = async (padId: string) => {
- for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
- const exists = await exports.doesPadExist(padId);
+ for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
+ const exists = await exports.doesPadExist(padId);
- if (exists) {
- return padId;
- }
+ if (exists) {
+ return padId;
+ }
- const [from, to] = padIdTransforms[i];
+ const [from, to] = padIdTransforms[i];
- // @ts-ignore
- padId = padId.replace(from, to);
- }
+ // @ts-ignore
+ padId = padId.replace(from, to);
+ }
- if (settings.lowerCasePadIds) padId = padId.toLowerCase();
+ if (settings.lowerCasePadIds) padId = padId.toLowerCase();
- // we're out of possible transformations, so just return it
- return padId;
+ // we're out of possible transformations, so just return it
+ return padId;
};
-exports.isValidPadId = (padId: string) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
+exports.isValidPadId = (padId: string) =>
+ /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
/**
* Removes the pad from database and unloads it.
*/
exports.removePad = async (padId: string) => {
- const p = db.remove(`pad:${padId}`);
- exports.unloadPad(padId);
- padList.removePad(padId);
- await p;
+ const p = db.remove(`pad:${padId}`);
+ exports.unloadPad(padId);
+ padList.removePad(padId);
+ await p;
};
// removes a pad from the cache
exports.unloadPad = (padId: string) => {
- globalPads.remove(padId);
+ globalPads.remove(padId);
};
diff --git a/src/node/db/ReadOnlyManager.ts b/src/node/db/ReadOnlyManager.ts
index 23639d665..f3e445092 100644
--- a/src/node/db/ReadOnlyManager.ts
+++ b/src/node/db/ReadOnlyManager.ts
@@ -1,4 +1,3 @@
-'use strict';
/**
* The ReadOnlyManager manages the database and rendering releated to read only pads
*/
@@ -19,37 +18,35 @@
* limitations under the License.
*/
-
-const db = require('./DB');
-const randomString = require('../utils/randomstring');
-
+const db = require("./DB");
+const randomString = require("../utils/randomstring");
/**
* checks if the id pattern matches a read-only pad id
* @param {String} id the pad's id
* @return {Boolean} true if the id is readonly
*/
-exports.isReadOnlyId = (id:string) => id.startsWith('r.');
+exports.isReadOnlyId = (id: string) => id.startsWith("r.");
/**
* returns a read only id for a pad
* @param {String} padId the id of the pad
* @return {String} the read only id
*/
-exports.getReadOnlyId = async (padId:string) => {
- // check if there is a pad2readonly entry
- let readOnlyId = await db.get(`pad2readonly:${padId}`);
+exports.getReadOnlyId = async (padId: string) => {
+ // check if there is a pad2readonly entry
+ let readOnlyId = await db.get(`pad2readonly:${padId}`);
- // there is no readOnly Entry in the database, let's create one
- if (readOnlyId == null) {
- readOnlyId = `r.${randomString(16)}`;
- await Promise.all([
- db.set(`pad2readonly:${padId}`, readOnlyId),
- db.set(`readonly2pad:${readOnlyId}`, padId),
- ]);
- }
+ // there is no readOnly Entry in the database, let's create one
+ if (readOnlyId == null) {
+ readOnlyId = `r.${randomString(16)}`;
+ await Promise.all([
+ db.set(`pad2readonly:${padId}`, readOnlyId),
+ db.set(`readonly2pad:${readOnlyId}`, padId),
+ ]);
+ }
- return readOnlyId;
+ return readOnlyId;
};
/**
@@ -57,19 +54,20 @@ exports.getReadOnlyId = async (padId:string) => {
* @param {String} readOnlyId read only id
* @return {String} the padId
*/
-exports.getPadId = async (readOnlyId:string) => await db.get(`readonly2pad:${readOnlyId}`);
+exports.getPadId = async (readOnlyId: string) =>
+ await db.get(`readonly2pad:${readOnlyId}`);
/**
* returns the padId and readonlyPadId in an object for any id
* @param {String} id read only id or real pad id
* @return {Object} an object with the padId and readonlyPadId
*/
-exports.getIds = async (id:string) => {
- const readonly = exports.isReadOnlyId(id);
+exports.getIds = async (id: string) => {
+ const readonly = exports.isReadOnlyId(id);
- // Might be null, if this is an unknown read-only id
- const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id);
- const padId = readonly ? await exports.getPadId(id) : id;
+ // Might be null, if this is an unknown read-only id
+ const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id);
+ const padId = readonly ? await exports.getPadId(id) : id;
- return {readOnlyPadId, padId, readonly};
+ return { readOnlyPadId, padId, readonly };
};
diff --git a/src/node/db/SecurityManager.ts b/src/node/db/SecurityManager.ts
index 326bf3659..b5298040c 100644
--- a/src/node/db/SecurityManager.ts
+++ b/src/node/db/SecurityManager.ts
@@ -1,4 +1,3 @@
-'use strict';
/**
* Controls the security of pad access
*/
@@ -19,20 +18,20 @@
* limitations under the License.
*/
-import {UserSettingsObject} from "../types/UserSettingsObject";
+import type { UserSettingsObject } from "../types/UserSettingsObject";
-const authorManager = require('./AuthorManager');
-const hooks = require('../../static/js/pluginfw/hooks.js');
-const padManager = require('./PadManager');
-const readOnlyManager = require('./ReadOnlyManager');
-const sessionManager = require('./SessionManager');
-const settings = require('../utils/Settings');
-const webaccess = require('../hooks/express/webaccess');
-const log4js = require('log4js');
-const authLogger = log4js.getLogger('auth');
-const {padutils} = require('../../static/js/pad_utils');
+const authorManager = require("./AuthorManager");
+const hooks = require("../../static/js/pluginfw/hooks.js");
+const padManager = require("./PadManager");
+const readOnlyManager = require("./ReadOnlyManager");
+const sessionManager = require("./SessionManager");
+const settings = require("../utils/Settings");
+const webaccess = require("../hooks/express/webaccess");
+const log4js = require("log4js");
+const authLogger = log4js.getLogger("auth");
+const { padutils } = require("../../static/js/pad_utils");
-const DENY = Object.freeze({accessStatus: 'deny'});
+const DENY = Object.freeze({ accessStatus: "deny" });
/**
* Determines whether the user can access a pad.
@@ -57,94 +56,123 @@ const DENY = Object.freeze({accessStatus: 'deny'});
* @param {Object} userSettings
* @return {DENY|{accessStatus: String, authorID: String}}
*/
-exports.checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject) => {
- if (!padID) {
- authLogger.debug('access denied: missing padID');
- return DENY;
- }
+exports.checkAccess = async (
+ padID: string,
+ sessionCookie: string,
+ token: string,
+ userSettings: UserSettingsObject,
+) => {
+ if (!padID) {
+ authLogger.debug("access denied: missing padID");
+ return DENY;
+ }
- let canCreate = !settings.editOnly;
+ let canCreate = !settings.editOnly;
- if (readOnlyManager.isReadOnlyId(padID)) {
- canCreate = false;
- padID = await readOnlyManager.getPadId(padID);
- if (padID == null) {
- authLogger.debug('access denied: read-only pad ID for a pad that does not exist');
- return DENY;
- }
- }
+ if (readOnlyManager.isReadOnlyId(padID)) {
+ canCreate = false;
+ padID = await readOnlyManager.getPadId(padID);
+ if (padID == null) {
+ authLogger.debug(
+ "access denied: read-only pad ID for a pad that does not exist",
+ );
+ return DENY;
+ }
+ }
- // Authentication and authorization checks.
- if (settings.loadTest) {
- console.warn(
- 'bypassing socket.io authentication and authorization checks due to settings.loadTest');
- } else if (settings.requireAuthentication) {
- if (userSettings == null) {
- authLogger.debug('access denied: authentication is required');
- return DENY;
- }
- if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false;
- if (userSettings.readOnly) canCreate = false;
- // Note: userSettings.padAuthorizations should still be populated even if
- // settings.requireAuthorization is false.
- const padAuthzs = userSettings.padAuthorizations || {};
- const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
- if (!level) {
- authLogger.debug('access denied: unauthorized');
- return DENY;
- }
- if (level !== 'create') canCreate = false;
- }
+ // Authentication and authorization checks.
+ if (settings.loadTest) {
+ console.warn(
+ "bypassing socket.io authentication and authorization checks due to settings.loadTest",
+ );
+ } else if (settings.requireAuthentication) {
+ if (userSettings == null) {
+ authLogger.debug("access denied: authentication is required");
+ return DENY;
+ }
+ if (userSettings.canCreate != null && !userSettings.canCreate)
+ canCreate = false;
+ if (userSettings.readOnly) canCreate = false;
+ // Note: userSettings.padAuthorizations should still be populated even if
+ // settings.requireAuthorization is false.
+ const padAuthzs = userSettings.padAuthorizations || {};
+ const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
+ if (!level) {
+ authLogger.debug("access denied: unauthorized");
+ return DENY;
+ }
+ if (level !== "create") canCreate = false;
+ }
- // allow plugins to deny access
- const isFalse = (x:boolean) => x === false;
- if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
- authLogger.debug('access denied: an onAccessCheck hook function returned false');
- return DENY;
- }
+ // allow plugins to deny access
+ const isFalse = (x: boolean) => x === false;
+ if (
+ hooks
+ .callAll("onAccessCheck", { padID, token, sessionCookie })
+ .some(isFalse)
+ ) {
+ authLogger.debug(
+ "access denied: an onAccessCheck hook function returned false",
+ );
+ return DENY;
+ }
- const padExists = await padManager.doesPadExist(padID);
- if (!padExists && !canCreate) {
- authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
- return DENY;
- }
+ const padExists = await padManager.doesPadExist(padID);
+ if (!padExists && !canCreate) {
+ authLogger.debug(
+ "access denied: user attempted to create a pad, which is prohibited",
+ );
+ return DENY;
+ }
- const sessionAuthorID = await sessionManager.findAuthorID(padID.split('$')[0], sessionCookie);
- if (settings.requireSession && !sessionAuthorID) {
- authLogger.debug('access denied: HTTP API session is required');
- return DENY;
- }
- if (!sessionAuthorID && token != null && !padutils.isValidAuthorToken(token)) {
- // The author token should be kept secret, so do not log it.
- authLogger.debug('access denied: invalid author token');
- return DENY;
- }
+ const sessionAuthorID = await sessionManager.findAuthorID(
+ padID.split("$")[0],
+ sessionCookie,
+ );
+ if (settings.requireSession && !sessionAuthorID) {
+ authLogger.debug("access denied: HTTP API session is required");
+ return DENY;
+ }
+ if (
+ !sessionAuthorID &&
+ token != null &&
+ !padutils.isValidAuthorToken(token)
+ ) {
+ // The author token should be kept secret, so do not log it.
+ authLogger.debug("access denied: invalid author token");
+ return DENY;
+ }
- const grant = {
- accessStatus: 'grant',
- authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings),
- };
+ const grant = {
+ accessStatus: "grant",
+ authorID:
+ sessionAuthorID || (await authorManager.getAuthorId(token, userSettings)),
+ };
- if (!padID.includes('$')) {
- // Only group pads can be private, so there is nothing more to check for this non-group pad.
- return grant;
- }
+ if (!padID.includes("$")) {
+ // Only group pads can be private, so there is nothing more to check for this non-group pad.
+ return grant;
+ }
- if (!padExists) {
- if (sessionAuthorID == null) {
- authLogger.debug('access denied: must have an HTTP API session to create a group pad');
- return DENY;
- }
- // Creating a group pad, so there is no public status to check.
- return grant;
- }
+ if (!padExists) {
+ if (sessionAuthorID == null) {
+ authLogger.debug(
+ "access denied: must have an HTTP API session to create a group pad",
+ );
+ return DENY;
+ }
+ // Creating a group pad, so there is no public status to check.
+ return grant;
+ }
- const pad = await padManager.getPad(padID);
+ const pad = await padManager.getPad(padID);
- if (!pad.getPublicStatus() && sessionAuthorID == null) {
- authLogger.debug('access denied: must have an HTTP API session to access private group pads');
- return DENY;
- }
+ if (!pad.getPublicStatus() && sessionAuthorID == null) {
+ authLogger.debug(
+ "access denied: must have an HTTP API session to access private group pads",
+ );
+ return DENY;
+ }
- return grant;
+ return grant;
};
diff --git a/src/node/db/SessionManager.ts b/src/node/db/SessionManager.ts
index c0e43a659..25ae96220 100644
--- a/src/node/db/SessionManager.ts
+++ b/src/node/db/SessionManager.ts
@@ -1,4 +1,3 @@
-'use strict';
/**
* The Session Manager provides functions to manage session in the database,
* it only provides session management for sessions created by the API
@@ -20,12 +19,12 @@
* limitations under the License.
*/
-const CustomError = require('../utils/customError');
-const promises = require('../utils/promises');
-const randomString = require('../utils/randomstring');
-const db = require('./DB');
-const groupManager = require('./GroupManager');
-const authorManager = require('./AuthorManager');
+const CustomError = require("../utils/customError");
+const promises = require("../utils/promises");
+const randomString = require("../utils/randomstring");
+const db = require("./DB");
+const groupManager = require("./GroupManager");
+const authorManager = require("./AuthorManager");
/**
* Finds the author ID for a session with matching ID and group.
@@ -36,52 +35,59 @@ const authorManager = require('./AuthorManager');
* sessionCookie, and is bound to a group with the given ID, then this returns the author ID
* bound to the session. Otherwise, returns undefined.
*/
-exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
- if (!sessionCookie) return undefined;
- /*
- * Sometimes, RFC 6265-compliant web servers may send back a cookie whose
- * value is enclosed in double quotes, such as:
- *
- * Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,
- * s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard
- *
- * Where the double quotes at the start and the end of the header value are
- * just delimiters. This is perfectly legal: Etherpad parsing logic should
- * cope with that, and remove the quotes early in the request phase.
- *
- * Somehow, this does not happen, and in such cases the actual value that
- * sessionCookie ends up having is:
- *
- * sessionCookie = '"s.37cf5299fbf981e14121fba3a588c02b,s.2b21517bf50729d8130ab85736a11346"'
- *
- * As quick measure, let's strip the double quotes (when present).
- * Note that here we are being minimal, limiting ourselves to just removing
- * quotes at the start and the end of the string.
- *
- * Fixes #3819.
- * Also, see #3820.
- */
- const sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(',');
- const sessionInfoPromises = sessionIDs.map(async (id) => {
- try {
- return await exports.getSessionInfo(id);
- } catch (err:any) {
- if (err.message === 'sessionID does not exist') {
- console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
- } else {
- throw err;
- }
- }
- return undefined;
- });
- const now = Math.floor(Date.now() / 1000);
- const isMatch = (si: {
- groupID: string;
- validUntil: number;
- }|null) => (si != null && si.groupID === groupID && now < si.validUntil);
- const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
- if (sessionInfo == null) return undefined;
- return sessionInfo.authorID;
+exports.findAuthorID = async (groupID: string, sessionCookie: string) => {
+ if (!sessionCookie) return undefined;
+ /*
+ * Sometimes, RFC 6265-compliant web servers may send back a cookie whose
+ * value is enclosed in double quotes, such as:
+ *
+ * Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,
+ * s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard
+ *
+ * Where the double quotes at the start and the end of the header value are
+ * just delimiters. This is perfectly legal: Etherpad parsing logic should
+ * cope with that, and remove the quotes early in the request phase.
+ *
+ * Somehow, this does not happen, and in such cases the actual value that
+ * sessionCookie ends up having is:
+ *
+ * sessionCookie = '"s.37cf5299fbf981e14121fba3a588c02b,s.2b21517bf50729d8130ab85736a11346"'
+ *
+ * As quick measure, let's strip the double quotes (when present).
+ * Note that here we are being minimal, limiting ourselves to just removing
+ * quotes at the start and the end of the string.
+ *
+ * Fixes #3819.
+ * Also, see #3820.
+ */
+ const sessionIDs = sessionCookie.replace(/^"|"$/g, "").split(",");
+ const sessionInfoPromises = sessionIDs.map(async (id) => {
+ try {
+ return await exports.getSessionInfo(id);
+ } catch (err: any) {
+ if (err.message === "sessionID does not exist") {
+ console.debug(
+ `SessionManager getAuthorID: no session exists with ID ${id}`,
+ );
+ } else {
+ throw err;
+ }
+ }
+ return undefined;
+ });
+ const now = Math.floor(Date.now() / 1000);
+ const isMatch = (
+ si: {
+ groupID: string;
+ validUntil: number;
+ } | null,
+ ) => si != null && si.groupID === groupID && now < si.validUntil;
+ const sessionInfo = await promises.firstSatisfies(
+ sessionInfoPromises,
+ isMatch,
+ );
+ if (sessionInfo == null) return undefined;
+ return sessionInfo.authorID;
};
/**
@@ -90,9 +96,9 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
* @return {Promise} Resolves to true if the session exists
*/
exports.doesSessionExist = async (sessionID: string) => {
- // check if the database entry of this session exists
- const session = await db.get(`session:${sessionID}`);
- return (session != null);
+ // check if the database entry of this session exists
+ const session = await db.get(`session:${sessionID}`);
+ return session != null;
};
/**
@@ -102,60 +108,64 @@ exports.doesSessionExist = async (sessionID: string) => {
* @param {Number} validUntil The unix timestamp when the session should expire
* @return {Promise<{sessionID: string}>} the id of the new session
*/
-exports.createSession = async (groupID: string, authorID: string, validUntil: number) => {
- // check if the group exists
- const groupExists = await groupManager.doesGroupExist(groupID);
- if (!groupExists) {
- throw new CustomError('groupID does not exist', 'apierror');
- }
+exports.createSession = async (
+ groupID: string,
+ authorID: string,
+ validUntil: number,
+) => {
+ // check if the group exists
+ const groupExists = await groupManager.doesGroupExist(groupID);
+ if (!groupExists) {
+ throw new CustomError("groupID does not exist", "apierror");
+ }
- // check if the author exists
- const authorExists = await authorManager.doesAuthorExist(authorID);
- if (!authorExists) {
- throw new CustomError('authorID does not exist', 'apierror');
- }
+ // check if the author exists
+ const authorExists = await authorManager.doesAuthorExist(authorID);
+ if (!authorExists) {
+ throw new CustomError("authorID does not exist", "apierror");
+ }
- // try to parse validUntil if it's not a number
- if (typeof validUntil !== 'number') {
- validUntil = parseInt(validUntil);
- }
+ // try to parse validUntil if it's not a number
+ if (typeof validUntil !== "number") {
+ validUntil = Number.parseInt(validUntil);
+ }
- // check it's a valid number
- if (isNaN(validUntil)) {
- throw new CustomError('validUntil is not a number', 'apierror');
- }
+ // check it's a valid number
+ if (isNaN(validUntil)) {
+ throw new CustomError("validUntil is not a number", "apierror");
+ }
- // ensure this is not a negative number
- if (validUntil < 0) {
- throw new CustomError('validUntil is a negative number', 'apierror');
- }
+ // ensure this is not a negative number
+ if (validUntil < 0) {
+ throw new CustomError("validUntil is a negative number", "apierror");
+ }
- // ensure this is not a float value
- if (!isInt(validUntil)) {
- throw new CustomError('validUntil is a float value', 'apierror');
- }
+ // ensure this is not a float value
+ if (!isInt(validUntil)) {
+ throw new CustomError("validUntil is a float value", "apierror");
+ }
- // check if validUntil is in the future
- if (validUntil < Math.floor(Date.now() / 1000)) {
- throw new CustomError('validUntil is in the past', 'apierror');
- }
+ // check if validUntil is in the future
+ if (validUntil < Math.floor(Date.now() / 1000)) {
+ throw new CustomError("validUntil is in the past", "apierror");
+ }
- // generate sessionID
- const sessionID = `s.${randomString(16)}`;
+ // generate sessionID
+ const sessionID = `s.${randomString(16)}`;
- // set the session into the database
- await db.set(`session:${sessionID}`, {groupID, authorID, validUntil});
+ // set the session into the database
+ await db.set(`session:${sessionID}`, { groupID, authorID, validUntil });
- // Add the session ID to the group2sessions and author2sessions records after creating the session
- // so that the state is consistent.
- await Promise.all([
- // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
- // property, and writes the result.
- db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1),
- db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1),
- ]);
+ // Add the session ID to the group2sessions and author2sessions records after creating the session
+ // so that the state is consistent.
+ await Promise.all([
+ // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
+ // property, and writes the result.
+ db.setSub(`group2sessions:${groupID}`, ["sessionIDs", sessionID], 1),
+ db.setSub(`author2sessions:${authorID}`, ["sessionIDs", sessionID], 1),
+ ]);
- return {sessionID};
+ return { sessionID };
};
/**
@@ -163,17 +173,17 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu
* @param {String} sessionID The id of the session
* @return {Promise
п р с т у ф х ц ч ш щ ю я
ь
`,
- wantHTML: 'а б в г ґ д е є ж з и і ї й к л м н о multiple lines in pre
п р с т у ф х ц ч ш щ ю я ь
',
- wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n',
- },
- 'preIntroducesASpace': {
- description: 'pre should be on a new line not preceded by a space',
- input: `
+ wantHTML:
+ "
а б в г ґ д е є ж з и і ї й к л м н о multiple lines in pre
п р с т у ф х ц ч ш щ ю я ь
",
+ wantText:
+ "а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n",
+ },
+ preIntroducesASpace: {
+ description: "pre should be on a new line not preceded by a space",
+ input: `
1
preline
`,
- wantHTML: '1 preline
',
- wantText: '1\npreline\n\n\n',
- },
- 'dontDeleteSpaceInsideElements': {
- description: 'Preserve spaces inside elements',
- input: 'Need more space s ! ',
- wantHTML: 'Need more space s !
',
- wantText: 'Need more space s !\n\n',
- },
- 'dontDeleteSpaceOutsideElements': {
- description: 'Preserve spaces outside elements',
- input: 'Need more space s ! ',
- wantHTML: 'Need more space s !
',
- wantText: 'Need more space s !\n\n',
- },
- 'dontDeleteSpaceAtEndOfElement': {
- description: 'Preserve spaces at the end of an element',
- input: 'Need more space s ! ',
- wantHTML: 'Need more space s !
',
- wantText: 'Need more space s !\n\n',
- },
- 'dontDeleteSpaceAtBeginOfElements': {
- description: 'Preserve spaces at the start of an element',
- input: 'Need more space s ! ',
- wantHTML: 'Need more space s !
',
- wantText: 'Need more space s !\n\n',
- },
+ wantHTML:
+ "1 preline
",
+ wantText: "1\npreline\n\n\n",
+ },
+ dontDeleteSpaceInsideElements: {
+ description: "Preserve spaces inside elements",
+ input:
+ "Need more space s ! ",
+ wantHTML:
+ "Need more space s !
",
+ wantText: "Need more space s !\n\n",
+ },
+ dontDeleteSpaceOutsideElements: {
+ description: "Preserve spaces outside elements",
+ input:
+ "Need more space s ! ",
+ wantHTML:
+ "Need more space s !
",
+ wantText: "Need more space s !\n\n",
+ },
+ dontDeleteSpaceAtEndOfElement: {
+ description: "Preserve spaces at the end of an element",
+ input:
+ "Need more space s ! ",
+ wantHTML:
+ "Need more space s !
",
+ wantText: "Need more space s !\n\n",
+ },
+ dontDeleteSpaceAtBeginOfElements: {
+ description: "Preserve spaces at the start of an element",
+ input:
+ "Need more space s ! ",
+ wantHTML:
+ "Need more space s !
",
+ wantText: "Need more space s !\n\n",
+ },
};
describe(__filename, function () {
- this.timeout(1000);
+ this.timeout(1000);
- before(async function () { agent = await common.init(); });
+ before(async () => {
+ agent = await common.init();
+ });
- Object.keys(testImports).forEach((testName) => {
- describe(testName, function () {
- const testPadId = makeid();
- const test = testImports[testName];
- if (test.disabled) {
- return xit(`DISABLED: ${testName}`, function (done) {
- done();
- });
- }
+ Object.keys(testImports).forEach((testName) => {
+ describe(testName, () => {
+ const testPadId = makeid();
+ const test = testImports[testName];
+ if (test.disabled) {
+ return xit(`DISABLED: ${testName}`, (done) => {
+ done();
+ });
+ }
- it('createPad', async function () {
- const res = await agent.get(`${endPoint('createPad')}?padID=${testPadId}`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
+ it("createPad", async () => {
+ const res = await agent
+ .get(`${endPoint("createPad")}?padID=${testPadId}`)
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
- it('setHTML', async function () {
- const res = await agent.get(`${endPoint('setHTML')}?padID=${testPadId}` +
- `&html=${encodeURIComponent(test.input)}`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
+ it("setHTML", async () => {
+ const res = await agent
+ .get(
+ `${endPoint("setHTML")}?padID=${testPadId}` +
+ `&html=${encodeURIComponent(test.input)}`,
+ )
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
- it('getHTML', async function () {
- const res = await agent.get(`${endPoint('getHTML')}?padID=${testPadId}`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.html, test.wantHTML);
- });
+ it("getHTML", async () => {
+ const res = await agent
+ .get(`${endPoint("getHTML")}?padID=${testPadId}`)
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.html, test.wantHTML);
+ });
- it('getText', async function () {
- const res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.text, test.wantText);
- });
- });
- });
+ it("getText", async () => {
+ const res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.text, test.wantText);
+ });
+ });
+ });
});
function makeid() {
- let text = '';
- const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let text = "";
+ const possible =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- for (let i = 0; i < 5; i++) {
- text += possible.charAt(Math.floor(Math.random() * possible.length));
- }
- return text;
+ for (let i = 0; i < 5; i++) {
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
+ }
+ return text;
}
diff --git a/src/tests/backend/specs/api/importexportGetPost.ts b/src/tests/backend/specs/api/importexportGetPost.ts
index 355699bc2..2b6036d27 100644
--- a/src/tests/backend/specs/api/importexportGetPost.ts
+++ b/src/tests/backend/specs/api/importexportGetPost.ts
@@ -1,20 +1,18 @@
-'use strict';
-
/*
* Import and Export tests for the /p/whateverPadId/import and /p/whateverPadId/export endpoints.
*/
-import {MapArrayType} from "../../../../node/types/MapType";
-import {SuperTestStatic} from "supertest";
-import TestAgent from "supertest/lib/agent";
+import { SuperTestStatic } from "supertest";
+import type TestAgent from "supertest/lib/agent";
+import type { MapArrayType } from "../../../../node/types/MapType";
-const assert = require('assert').strict;
-const common = require('../../common');
-const fs = require('fs');
-const settings = require('../../../../node/utils/Settings');
-const superagent = require('superagent');
-const padManager = require('../../../../node/db/PadManager');
-const plugins = require('../../../../static/js/pluginfw/plugin_defs');
+const assert = require("assert").strict;
+const common = require("../../common");
+const fs = require("fs");
+const settings = require("../../../../node/utils/Settings");
+const superagent = require("superagent");
+const padManager = require("../../../../node/db/PadManager");
+const plugins = require("../../../../static/js/pluginfw/plugin_defs");
const padText = fs.readFileSync(`${__dirname}/test.txt`);
const etherpadDoc = fs.readFileSync(`${__dirname}/test.etherpad`);
@@ -29,35 +27,39 @@ const testPadId = makeid();
const testPadIdEnc = encodeURIComponent(testPadId);
const deleteTestPad = async () => {
- if (await padManager.doesPadExist(testPadId)) {
- const pad = await padManager.getPad(testPadId);
- await pad.remove();
- }
+ if (await padManager.doesPadExist(testPadId)) {
+ const pad = await padManager.getPad(testPadId);
+ await pad.remove();
+ }
};
describe(__filename, function () {
- this.timeout(45000);
- before(async function () { agent = await common.init(); });
+ this.timeout(45000);
+ before(async () => {
+ agent = await common.init();
+ });
- describe('Connectivity', function () {
- it('can connect', async function () {
- await agent.get('/api/')
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('Content-Type', /json/);
- });
- });
+ describe("Connectivity", () => {
+ it("can connect", async () => {
+ await agent
+ .get("/api/")
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ });
+ });
- describe('API Versioning', function () {
- it('finds the version tag', async function () {
- await agent.get('/api/')
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect((res:any) => assert(res.body.currentVersion));
- });
- });
+ describe("API Versioning", () => {
+ it("finds the version tag", async () => {
+ await agent
+ .get("/api/")
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect((res: any) => assert(res.body.currentVersion));
+ });
+ });
- /*
+ /*
Tests
-----
@@ -83,723 +85,861 @@ describe(__filename, function () {
curl -s -v --form file=@/home/jose/test.txt http://127.0.0.1:9001/p/foo/import
*/
- describe('Imports and Exports', function () {
- const backups:MapArrayType = {};
+ describe("Imports and Exports", () => {
+ const backups: MapArrayType = {};
- beforeEach(async function () {
- backups.hooks = {};
- for (const hookName of ['preAuthorize', 'authenticate', 'authorize']) {
- backups.hooks[hookName] = plugins.hooks[hookName];
- plugins.hooks[hookName] = [];
- }
- // Note: This is a shallow copy.
- backups.settings = Object.assign({}, settings);
- settings.requireAuthentication = false;
- settings.requireAuthorization = false;
- settings.users = {user: {password: 'user-password'}};
- });
+ beforeEach(async () => {
+ backups.hooks = {};
+ for (const hookName of ["preAuthorize", "authenticate", "authorize"]) {
+ backups.hooks[hookName] = plugins.hooks[hookName];
+ plugins.hooks[hookName] = [];
+ }
+ // Note: This is a shallow copy.
+ backups.settings = Object.assign({}, settings);
+ settings.requireAuthentication = false;
+ settings.requireAuthorization = false;
+ settings.users = { user: { password: "user-password" } };
+ });
- afterEach(async function () {
- Object.assign(plugins.hooks, backups.hooks);
- // Note: This does not unset settings that were added.
- Object.assign(settings, backups.settings);
- });
+ afterEach(async () => {
+ Object.assign(plugins.hooks, backups.hooks);
+ // Note: This does not unset settings that were added.
+ Object.assign(settings, backups.settings);
+ });
- it('creates a new Pad, imports content to it, checks that content', async function () {
- await agent.get(`${endPoint('createPad')}?padID=${testPadId}`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.equal(res.body.code, 0));
- await agent.post(`/p/${testPadId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
- .expect(200);
- await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect((res:any) => assert.equal(res.body.data.text, padText.toString()));
- });
+ it("creates a new Pad, imports content to it, checks that content", async () => {
+ await agent
+ .get(`${endPoint("createPad")}?padID=${testPadId}`)
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/)
+ .expect((res: any) => assert.equal(res.body.code, 0));
+ await agent
+ .post(`/p/${testPadId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", padText, {
+ filename: "/test.txt",
+ contentType: "text/plain",
+ })
+ .expect(200);
+ await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect((res: any) =>
+ assert.equal(res.body.data.text, padText.toString()),
+ );
+ });
- describe('export from read-only pad ID', function () {
- let readOnlyId:string;
+ describe("export from read-only pad ID", () => {
+ let readOnlyId: string;
- // This ought to be before(), but it must run after the top-level beforeEach() above.
- beforeEach(async function () {
- if (readOnlyId != null) return;
- await agent.post(`/p/${testPadId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', padText, {filename: '/test.txt', contentType: 'text/plain'})
- .expect(200);
- const res = await agent.get(`${endPoint('getReadOnlyID')}?padID=${testPadId}`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.equal(res.body.code, 0));
- readOnlyId = res.body.data.readOnlyID;
- });
+ // This ought to be before(), but it must run after the top-level beforeEach() above.
+ beforeEach(async () => {
+ if (readOnlyId != null) return;
+ await agent
+ .post(`/p/${testPadId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", padText, {
+ filename: "/test.txt",
+ contentType: "text/plain",
+ })
+ .expect(200);
+ const res = await agent
+ .get(`${endPoint("getReadOnlyID")}?padID=${testPadId}`)
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/)
+ .expect((res: any) => assert.equal(res.body.code, 0));
+ readOnlyId = res.body.data.readOnlyID;
+ });
- for (const authn of [false, true]) {
- describe(`requireAuthentication = ${authn}`, function () {
- // This ought to be before(), but it must run after the top-level beforeEach() above.
- beforeEach(async function () {
- settings.requireAuthentication = authn;
- });
+ for (const authn of [false, true]) {
+ describe(`requireAuthentication = ${authn}`, () => {
+ // This ought to be before(), but it must run after the top-level beforeEach() above.
+ beforeEach(async () => {
+ settings.requireAuthentication = authn;
+ });
- for (const exportType of ['html', 'txt', 'etherpad']) {
- describe(`export to ${exportType}`, function () {
- let text:string;
+ for (const exportType of ["html", "txt", "etherpad"]) {
+ describe(`export to ${exportType}`, () => {
+ let text: string;
- // This ought to be before(), but it must run after the top-level beforeEach() above.
- beforeEach(async function () {
- if (text != null) return;
- let req = agent.get(`/p/${readOnlyId}/export/${exportType}`)
- .set("authorization", await common.generateJWTToken());
- if (authn) req = req.auth('user', 'user-password');
- const res = await req
- .expect(200)
- .buffer(true).parse(superagent.parse.text);
- text = res.text;
- });
+ // This ought to be before(), but it must run after the top-level beforeEach() above.
+ beforeEach(async () => {
+ if (text != null) return;
+ let req = agent
+ .get(`/p/${readOnlyId}/export/${exportType}`)
+ .set("authorization", await common.generateJWTToken());
+ if (authn) req = req.auth("user", "user-password");
+ const res = await req
+ .expect(200)
+ .buffer(true)
+ .parse(superagent.parse.text);
+ text = res.text;
+ });
- it('export OK', async function () {
- assert.match(text, /This is the/);
- });
+ it("export OK", async () => {
+ assert.match(text, /This is the/);
+ });
- it('writable pad ID is not leaked', async function () {
- assert(!text.includes(testPadId));
- });
+ it("writable pad ID is not leaked", async () => {
+ assert(!text.includes(testPadId));
+ });
- it('re-import to read-only pad ID gives 403 forbidden', async function () {
- let req = agent.post(`/p/${readOnlyId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', Buffer.from(text), {
- filename: `/test.${exportType}`,
- contentType: 'text/plain',
- });
- if (authn) req = req.auth('user', 'user-password');
- await req.expect(403);
- });
+ it("re-import to read-only pad ID gives 403 forbidden", async () => {
+ let req = agent
+ .post(`/p/${readOnlyId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", Buffer.from(text), {
+ filename: `/test.${exportType}`,
+ contentType: "text/plain",
+ });
+ if (authn) req = req.auth("user", "user-password");
+ await req.expect(403);
+ });
- it('re-import to read-write pad ID gives 200 OK', async function () {
- // The new pad ID must differ from testPadId because Etherpad refuses to import
- // .etherpad files on top of a pad that already has edits.
- let req = agent.post(`/p/${testPadId}_import/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', Buffer.from(text), {
- filename: `/test.${exportType}`,
- contentType: 'text/plain',
- });
- if (authn) req = req.auth('user', 'user-password');
- await req.expect(200);
- });
- });
- }
- });
- }
- });
+ it("re-import to read-write pad ID gives 200 OK", async () => {
+ // The new pad ID must differ from testPadId because Etherpad refuses to import
+ // .etherpad files on top of a pad that already has edits.
+ let req = agent
+ .post(`/p/${testPadId}_import/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", Buffer.from(text), {
+ filename: `/test.${exportType}`,
+ contentType: "text/plain",
+ });
+ if (authn) req = req.auth("user", "user-password");
+ await req.expect(200);
+ });
+ });
+ }
+ });
+ }
+ });
- describe('Import/Export tests requiring AbiWord/LibreOffice', function () {
- before(async function () {
- if ((!settings.abiword || settings.abiword.indexOf('/') === -1) &&
- (!settings.soffice || settings.soffice.indexOf('/') === -1)) {
- this.skip();
- }
- });
+ describe("Import/Export tests requiring AbiWord/LibreOffice", () => {
+ before(async function () {
+ if (
+ (!settings.abiword || settings.abiword.indexOf("/") === -1) &&
+ (!settings.soffice || settings.soffice.indexOf("/") === -1)
+ ) {
+ this.skip();
+ }
+ });
- // For some reason word import does not work in testing..
- // TODO: fix support for .doc files..
- it('Tries to import .doc that uses soffice or abiword', async function () {
- await agent.post(`/p/${testPadId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', wordDoc, {filename: '/test.doc', contentType: 'application/msword'})
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.deepEqual(res.body, {
- code: 0,
- message: 'ok',
- data: {directDatabaseAccess: false},
- }));
- });
+ // For some reason word import does not work in testing..
+ // TODO: fix support for .doc files..
+ it("Tries to import .doc that uses soffice or abiword", async () => {
+ await agent
+ .post(`/p/${testPadId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", wordDoc, {
+ filename: "/test.doc",
+ contentType: "application/msword",
+ })
+ .expect(200)
+ .expect("Content-Type", /json/)
+ .expect((res: any) =>
+ assert.deepEqual(res.body, {
+ code: 0,
+ message: "ok",
+ data: { directDatabaseAccess: false },
+ }),
+ );
+ });
- it('exports DOC', async function () {
- await agent.get(`/p/${testPadId}/export/doc`)
- .set("authorization", await common.generateJWTToken())
- .buffer(true).parse(superagent.parse['application/octet-stream'])
- .expect(200)
- .expect((res:any) => assert(res.body.length >= 9000));
- });
+ it("exports DOC", async () => {
+ await agent
+ .get(`/p/${testPadId}/export/doc`)
+ .set("authorization", await common.generateJWTToken())
+ .buffer(true)
+ .parse(superagent.parse["application/octet-stream"])
+ .expect(200)
+ .expect((res: any) => assert(res.body.length >= 9000));
+ });
- it('Tries to import .docx that uses soffice or abiword', async function () {
- await agent.post(`/p/${testPadId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', wordXDoc, {
- filename: '/test.docx',
- contentType:
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- })
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.deepEqual(res.body, {
- code: 0,
- message: 'ok',
- data: {directDatabaseAccess: false},
- }));
- });
+ it("Tries to import .docx that uses soffice or abiword", async () => {
+ await agent
+ .post(`/p/${testPadId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", wordXDoc, {
+ filename: "/test.docx",
+ contentType:
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ })
+ .expect(200)
+ .expect("Content-Type", /json/)
+ .expect((res: any) =>
+ assert.deepEqual(res.body, {
+ code: 0,
+ message: "ok",
+ data: { directDatabaseAccess: false },
+ }),
+ );
+ });
- it('exports DOC from imported DOCX', async function () {
- await agent.get(`/p/${testPadId}/export/doc`)
- .set("authorization", await common.generateJWTToken())
- .buffer(true).parse(superagent.parse['application/octet-stream'])
- .expect(200)
- .expect((res:any) => assert(res.body.length >= 9100));
- });
+ it("exports DOC from imported DOCX", async () => {
+ await agent
+ .get(`/p/${testPadId}/export/doc`)
+ .set("authorization", await common.generateJWTToken())
+ .buffer(true)
+ .parse(superagent.parse["application/octet-stream"])
+ .expect(200)
+ .expect((res: any) => assert(res.body.length >= 9100));
+ });
- it('Tries to import .pdf that uses soffice or abiword', async function () {
- await agent.post(`/p/${testPadId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', pdfDoc, {filename: '/test.pdf', contentType: 'application/pdf'})
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.deepEqual(res.body, {
- code: 0,
- message: 'ok',
- data: {directDatabaseAccess: false},
- }));
- });
+ it("Tries to import .pdf that uses soffice or abiword", async () => {
+ await agent
+ .post(`/p/${testPadId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", pdfDoc, {
+ filename: "/test.pdf",
+ contentType: "application/pdf",
+ })
+ .expect(200)
+ .expect("Content-Type", /json/)
+ .expect((res: any) =>
+ assert.deepEqual(res.body, {
+ code: 0,
+ message: "ok",
+ data: { directDatabaseAccess: false },
+ }),
+ );
+ });
- it('exports PDF', async function () {
- await agent.get(`/p/${testPadId}/export/pdf`)
- .set("authorization", await common.generateJWTToken())
- .buffer(true).parse(superagent.parse['application/octet-stream'])
- .expect(200)
- .expect((res:any) => assert(res.body.length >= 1000));
- });
+ it("exports PDF", async () => {
+ await agent
+ .get(`/p/${testPadId}/export/pdf`)
+ .set("authorization", await common.generateJWTToken())
+ .buffer(true)
+ .parse(superagent.parse["application/octet-stream"])
+ .expect(200)
+ .expect((res: any) => assert(res.body.length >= 1000));
+ });
- it('Tries to import .odt that uses soffice or abiword', async function () {
- await agent.post(`/p/${testPadId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', odtDoc, {filename: '/test.odt', contentType: 'application/odt'})
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.deepEqual(res.body, {
- code: 0,
- message: 'ok',
- data: {directDatabaseAccess: false},
- }));
- });
+ it("Tries to import .odt that uses soffice or abiword", async () => {
+ await agent
+ .post(`/p/${testPadId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", odtDoc, {
+ filename: "/test.odt",
+ contentType: "application/odt",
+ })
+ .expect(200)
+ .expect("Content-Type", /json/)
+ .expect((res: any) =>
+ assert.deepEqual(res.body, {
+ code: 0,
+ message: "ok",
+ data: { directDatabaseAccess: false },
+ }),
+ );
+ });
- it('exports ODT', async function () {
- await agent.get(`/p/${testPadId}/export/odt`)
- .set("authorization", await common.generateJWTToken())
- .buffer(true).parse(superagent.parse['application/octet-stream'])
- .expect(200)
- .expect((res:any) => assert(res.body.length >= 7000));
- });
- }); // End of AbiWord/LibreOffice tests.
+ it("exports ODT", async () => {
+ await agent
+ .get(`/p/${testPadId}/export/odt`)
+ .set("authorization", await common.generateJWTToken())
+ .buffer(true)
+ .parse(superagent.parse["application/octet-stream"])
+ .expect(200)
+ .expect((res: any) => assert(res.body.length >= 7000));
+ });
+ }); // End of AbiWord/LibreOffice tests.
- it('Tries to import .etherpad', async function () {
- this.timeout(3000);
- await agent.post(`/p/${testPadId}/import`)
- .set("authorization", await common.generateJWTToken())
- .attach('file', etherpadDoc, {
- filename: '/test.etherpad',
- contentType: 'application/etherpad',
- })
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.deepEqual(res.body, {
- code: 0,
- message: 'ok',
- data: {directDatabaseAccess: true},
- }));
- });
+ it("Tries to import .etherpad", async function () {
+ this.timeout(3000);
+ await agent
+ .post(`/p/${testPadId}/import`)
+ .set("authorization", await common.generateJWTToken())
+ .attach("file", etherpadDoc, {
+ filename: "/test.etherpad",
+ contentType: "application/etherpad",
+ })
+ .expect(200)
+ .expect("Content-Type", /json/)
+ .expect((res: any) =>
+ assert.deepEqual(res.body, {
+ code: 0,
+ message: "ok",
+ data: { directDatabaseAccess: true },
+ }),
+ );
+ });
- it('exports Etherpad', async function () {
- this.timeout(3000);
- await agent.get(`/p/${testPadId}/export/etherpad`)
- .set("authorization", await common.generateJWTToken())
- .buffer(true).parse(superagent.parse.text)
- .expect(200)
- .expect(/hello/);
- });
+ it("exports Etherpad", async function () {
+ this.timeout(3000);
+ await agent
+ .get(`/p/${testPadId}/export/etherpad`)
+ .set("authorization", await common.generateJWTToken())
+ .buffer(true)
+ .parse(superagent.parse.text)
+ .expect(200)
+ .expect(/hello/);
+ });
- it('exports HTML for this Etherpad file', async function () {
- this.timeout(3000);
- await agent.get(`/p/${testPadId}/export/html`)
- .set("authorization", await common.generateJWTToken())
- .expect(200)
- .expect('content-type', 'text/html; charset=utf-8')
- .expect(/
hello<\/ul><\/li><\/ul>/);
- });
+ it("exports HTML for this Etherpad file", async function () {
+ this.timeout(3000);
+ await agent
+ .get(`/p/${testPadId}/export/html`)
+ .set("authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("content-type", "text/html; charset=utf-8")
+ .expect(
+ /
';
/*
* When exported back, Etherpad produces an html which is not exactly the same
* textually, but at least it remains standard compliant and has an equal DOM
* structure.
*/
-const expectedHtml = '
one
two
0
1
2
3
4
item
item1
item2
';
+const expectedHtml =
+ '
one
two
0
1
2
3
4
item
item1
item2
';
/*
* Html document with space between list items, to test its import and
* verify it is exported back correctly
*/
-const ulSpaceHtml = '
one
';
+const ulSpaceHtml =
+ '
one
';
/*
* When exported back, Etherpad produces an html which is not exactly the same
* textually, but at least it remains standard compliant and has an equal DOM
* structure.
*/
-const expectedSpaceHtml = '
one
';
+const expectedSpaceHtml =
+ '
one
';
-describe(__filename, function () {
- before(async function () {
- agent = await common.init();
- const res = await agent.get('/api/')
- .expect(200)
- .expect('Content-Type', /json/);
- apiVersion = res.body.currentVersion;
- assert(apiVersion);
- });
+describe(__filename, () => {
+ before(async () => {
+ agent = await common.init();
+ const res = await agent
+ .get("/api/")
+ .expect(200)
+ .expect("Content-Type", /json/);
+ apiVersion = res.body.currentVersion;
+ assert(apiVersion);
+ });
- describe('Sanity checks', function () {
- it('errors with invalid oauth token', async function () {
- // This is broken because Etherpad doesn't handle HTTP codes properly see #2343
- await agent.get(`/api/${apiVersion}/createPad?padID=test`)
- .set("Authorization", (await common.generateJWTToken()).substring(0, 10))
- .expect(401);
- });
- });
+ describe("Sanity checks", () => {
+ it("errors with invalid oauth token", async () => {
+ // This is broken because Etherpad doesn't handle HTTP codes properly see #2343
+ await agent
+ .get(`/api/${apiVersion}/createPad?padID=test`)
+ .set(
+ "Authorization",
+ (await common.generateJWTToken()).substring(0, 10),
+ )
+ .expect(401);
+ });
+ });
- /* Pad Tests Order of execution
+ /* Pad Tests Order of execution
-> deletePad -- This gives us a guaranteed clear environment
-> createPad
-> getRevisions -- Should be 0
@@ -110,624 +118,725 @@ describe(__filename, function () {
*/
- describe('Tests', function () {
- it('deletes a Pad that does not exist', async function () {
- await agent.get(`${endPoint('deletePad')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200) // @TODO: we shouldn't expect 200 here since the pad may not exist
- .expect('Content-Type', /json/);
- });
-
- it('creates a new Pad', async function () {
- const res = await agent.get(`${endPoint('createPad')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('gets revision count of Pad', async function () {
- const res = await agent.get(`${endPoint('getRevisionsCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.equal(res.body.data.revisions, 0);
- });
-
- it('gets saved revisions count of Pad', async function () {
- const res = await agent.get(`${endPoint('getSavedRevisionsCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.equal(res.body.data.savedRevisions, 0);
- });
-
- it('gets saved revision list of Pad', async function () {
- const res = await agent.get(`${endPoint('listSavedRevisions')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.deepEqual(res.body.data.savedRevisions, []);
- });
-
- it('get the HTML of Pad', async function () {
- const res = await agent.get(`${endPoint('getHTML')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert(res.body.data.html.length > 1);
- });
-
- it('list all pads', async function () {
- const res = await agent.get(endPoint('listAllPads'))
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert(res.body.data.padIDs.includes(testPadId));
- });
-
- it('deletes the Pad', async function () {
- const res = await agent.get(`${endPoint('deletePad')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('list all pads again', async function () {
- const res = await agent.get(endPoint('listAllPads'))
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert(!res.body.data.padIDs.includes(testPadId));
- });
-
- it('get the HTML of a Pad -- Should return a failure', async function () {
- const res = await agent.get(`${endPoint('getHTML')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 1);
- });
-
- it('creates a new Pad with text', async function () {
- const res = await agent.get(`${endPoint('createPad')}?padID=${testPadId}&text=testText`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('gets the Pad text and expect it to be testText with trailing \\n', async function () {
- const res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.text, 'testText\n');
- });
-
- it('set text', async function () {
- const res = await agent.post(endPoint('setText'))
- .set("Authorization", (await common.generateJWTToken()))
- .send({
- padID: testPadId,
- text: 'testTextTwo',
- })
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('gets the Pad text', async function () {
- const res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.text, 'testTextTwo\n');
- });
-
- it('gets Revision Count of a Pad', async function () {
- const res = await agent.get(`${endPoint('getRevisionsCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.revisions, 1);
- });
-
- it('saves Revision', async function () {
- const res = await agent.get(`${endPoint('saveRevision')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('gets saved revisions count of Pad again', async function () {
- const res = await agent.get(`${endPoint('getSavedRevisionsCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.equal(res.body.data.savedRevisions, 1);
- });
-
- it('gets saved revision list of Pad again', async function () {
- const res = await agent.get(`${endPoint('listSavedRevisions')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.deepEqual(res.body.data.savedRevisions, [1]);
- });
-
- it('gets User Count of a Pad', async function () {
- const res = await agent.get(`${endPoint('padUsersCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.padUsersCount, 0);
- });
-
- it('Gets the Read Only ID of a Pad', async function () {
- const res = await agent.get(`${endPoint('getReadOnlyID')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert(res.body.data.readOnlyID);
- });
-
- it('Get Authors of the Pad', async function () {
- const res = await agent.get(`${endPoint('listAuthorsOfPad')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.authorIDs.length, 0);
- });
-
- it('Get When Pad was left Edited', async function () {
- const res = await agent.get(`${endPoint('getLastEdited')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert(res.body.data.lastEdited);
- lastEdited = res.body.data.lastEdited;
- });
-
- it('set text again', async function () {
- const res = await agent.post(endPoint('setText'))
- .set("Authorization", (await common.generateJWTToken()))
- .send({
- padID: testPadId,
- text: 'testTextThree',
- })
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('Get When Pad was left Edited again', async function () {
- const res = await agent.get(`${endPoint('getLastEdited')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert(res.body.data.lastEdited > lastEdited);
- });
-
- it('gets User Count of a Pad again', async function () {
- const res = await agent.get(`${endPoint('padUsers')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.padUsers.length, 0);
- });
-
- it('deletes a Pad', async function () {
- const res = await agent.get(`${endPoint('deletePad')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('creates the Pad again', async function () {
- const res = await agent.get(`${endPoint('createPad')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('Sets text on a pad Id', async function () {
- const res = await agent.post(`${endPoint('setText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .field({text})
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('Gets text on a pad Id', async function () {
- const res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.equal(res.body.data.text, `${text}\n`);
- });
-
- it('Sets text on a pad Id including an explicit newline', async function () {
- const res = await agent.post(`${endPoint('setText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .field({text: `${text}\n`})
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it("Gets text on a pad Id and doesn't have an excess newline", async function () {
- const res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.equal(res.body.data.text, `${text}\n`);
- });
-
- it('Gets when pad was last edited', async function () {
- const res = await agent.get(`${endPoint('getLastEdited')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.notEqual(res.body.lastEdited, 0);
- });
-
- it('Move a Pad to a different Pad ID', async function () {
- const res = await agent.get(
- `${endPoint('movePad')}?sourceID=${testPadId}&destinationID=${newPadId}&force=true`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('Gets text from new pad', async function () {
- const res = await agent.get(`${endPoint('getText')}?padID=${newPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.text, `${text}\n`);
- });
-
- it('Move pad back to original ID', async function () {
- const res = await agent.get(
- `${endPoint('movePad')}?sourceID=${newPadId}&destinationID=${testPadId}&force=false`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('Get text using original ID', async function () {
- const res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.text, `${text}\n`);
- });
-
- it('Get last edit of original ID', async function () {
- const res = await agent.get(`${endPoint('getLastEdited')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.notEqual(res.body.lastEdited, 0);
- });
-
- it('Append text to a pad Id', async function () {
- let res = await agent.get(
- `${endPoint('appendText', '1.2.13')}?padID=${testPadId}&text=hello`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.equal(res.body.data.text, `${text}hello\n`);
- });
-
- it('getText of old revision', async function () {
- let res = await agent.get(`${endPoint('getRevisionsCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- const rev = res.body.data.revisions;
- assert(rev != null);
- assert(Number.isInteger(rev));
- assert(rev > 0);
- res = await agent.get(`${endPoint('getText')}?padID=${testPadId}&rev=${rev - 1}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- assert.equal(res.body.data.text, `${text}\n`);
- });
-
- it('Sets the HTML of a Pad attempting to pass ugly HTML', async function () {
- const html = '
Hello HTML
';
- const res = await agent.post(endPoint('setHTML'))
- .set("Authorization", (await common.generateJWTToken()))
- .send({
- padID: testPadId,
- html,
- })
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('Pad with complex nested lists of different types', async function () {
- let res = await agent.post(endPoint('setHTML'))
- .set("Authorization", (await common.generateJWTToken()))
- .send({
- padID: testPadId,
- html: ulHtml,
- })
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- res = await agent.get(`${endPoint('getHTML')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- const receivedHtml = res.body.data.html.replace(' ', '').toLowerCase();
- assert.equal(receivedHtml, expectedHtml);
- });
-
- it('Pad with white space between list items', async function () {
- let res = await agent.get(`${endPoint('setHTML')}?padID=${testPadId}&html=${ulSpaceHtml}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- res = await agent.get(`${endPoint('getHTML')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- const receivedHtml = res.body.data.html.replace(' ', '').toLowerCase();
- assert.equal(receivedHtml, expectedSpaceHtml);
- });
-
- it('errors if pad can be created', async function () {
- await Promise.all(['/', '%23', '%3F', '%26'].map(async (badUrlChar) => {
- const res = await agent.get(`${endPoint('createPad')}?padID=${badUrlChar}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 1);
- }));
- });
-
- it('copies the content of a existent pad', async function () {
- const res = await agent.get(
- `${endPoint('copyPad')}?sourceID=${testPadId}&destinationID=${copiedPadId}&force=true`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- it('does not add an useless revision', async function () {
- let res = await agent.post(`${endPoint('setText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .field({text: 'identical text\n'})
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
-
- res = await agent.get(`${endPoint('getText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.text, 'identical text\n');
-
- res = await agent.get(`${endPoint('getRevisionsCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- const revCount = res.body.data.revisions;
-
- res = await agent.post(`${endPoint('setText')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .field({text: 'identical text\n'})
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
-
- res = await agent.get(`${endPoint('getRevisionsCount')}?padID=${testPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.data.revisions, revCount);
- });
-
- it('creates a new Pad with empty text', async function () {
- await agent.get(`${endPoint('createPad')}?padID=${anotherPadId}&text=`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect('Content-Type', /json/)
- .expect(200)
- .expect((res:any) => {
- assert.equal(res.body.code, 0, 'Unable to create new Pad');
- });
- await agent.get(`${endPoint('getText')}?padID=${anotherPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect('Content-Type', /json/)
- .expect(200)
- .expect((res:any) => {
- assert.equal(res.body.code, 0, 'Unable to get pad text');
- assert.equal(res.body.data.text, '\n', 'Pad text is not empty');
- });
- });
-
- it('deletes with empty text', async function () {
- await agent.get(`${endPoint('deletePad')}?padID=${anotherPadId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect('Content-Type', /json/)
- .expect(200)
- .expect((res: any) => {
- assert.equal(res.body.code, 0, 'Unable to delete empty Pad');
- });
- });
- });
-
- describe('copyPadWithoutHistory', function () {
- const sourcePadId = makeid();
- let newPad:string;
-
- before(async function () {
- await createNewPadWithHtml(sourcePadId, ulHtml);
- });
-
- beforeEach(async function () {
- newPad = makeid();
- });
-
- it('returns a successful response', async function () {
- const res = await agent.get(`${endPoint('copyPadWithoutHistory')}?sourceID=${sourcePadId}` +
- `&destinationID=${newPad}&force=false`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- });
-
- // this test validates if the source pad's text and attributes are kept
- it('creates a new pad with the same content as the source pad', async function () {
- let res = await agent.get(`${endPoint('copyPadWithoutHistory')}?sourceID=${sourcePadId}` +
- `&destinationID=${newPad}&force=false`)
- .set("Authorization", (await common.generateJWTToken()));
- assert.equal(res.body.code, 0);
- res = await agent.get(`${endPoint('getHTML')}?padID=${newPad}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200);
- const receivedHtml = res.body.data.html.replace('
', '').toLowerCase();
- assert.equal(receivedHtml, expectedHtml);
- });
-
- it('copying to a non-existent group throws an error', async function () {
- const padWithNonExistentGroup = `notExistentGroup$${newPad}`;
- const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` +
- `?sourceID=${sourcePadId}` +
- `&destinationID=${padWithNonExistentGroup}&force=true`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200);
- assert.equal(res.body.code, 1);
- });
-
- describe('copying to an existing pad', function () {
- beforeEach(async function () {
- await createNewPadWithHtml(newPad, ulHtml);
- });
-
- it('force=false fails', async function () {
- const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` +
- `?sourceID=${sourcePadId}` +
- `&destinationID=${newPad}&force=false`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200);
- assert.equal(res.body.code, 1);
- });
-
- it('force=true succeeds', async function () {
- const res = await agent.get(`${endPoint('copyPadWithoutHistory')}` +
- `?sourceID=${sourcePadId}` +
- `&destinationID=${newPad}&force=true`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200);
- assert.equal(res.body.code, 0);
- });
- });
-
- // Regression test for https://github.com/ether/etherpad-lite/issues/5296
- it('source and destination attribute pools are independent', async function () {
- // Strategy for this test:
- // 1. Create a new pad without bold or italic text
- // 2. Use copyPadWithoutHistory to copy the pad.
- // 3. Add some bold text (but no italic text!) to the source pad. This should add a bold
- // attribute to the source pad's pool but not to the destination pad's pool.
- // 4. Add some italic text (but no bold text!) to the destination pad. This should add an
- // italic attribute to the destination pad's pool with the same number as the newly added
- // bold attribute in the source pad's pool.
- // 5. Add some more text (bold or plain) to the source pad. This will save the source pad to
- // the database after the destination pad has had an opportunity to corrupt the source
- // pad.
- // 6. Export the source and destination pads. Make sure that doesn't appear in the
- // source pad's HTML, and that doesn't appear int he destination pad's HTML.
- // 7. Force the server to re-init the pads from the database.
- // 8. Repeat step 6.
- // If appears in the source pad, or appears in the destination pad, then shared
- // state between the two attribute pools caused corruption.
-
- const getHtml = async (padId:string) => {
- const res = await agent.get(`${endPoint('getHTML')}?padID=${padId}`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/);
- assert.equal(res.body.code, 0);
- return res.body.data.html;
- };
-
- const setBody = async (padId: string, bodyHtml: string) => {
- await agent.post(endPoint('setHTML'))
- .set("Authorization", (await common.generateJWTToken()))
- .send({padID: padId, html: `${bodyHtml}`})
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res: any) => assert.equal(res.body.code, 0));
- };
-
- const origHtml = await getHtml(sourcePadId);
- assert.doesNotMatch(origHtml, //);
- assert.doesNotMatch(origHtml, //);
- await agent.get(`${endPoint('copyPadWithoutHistory')}?sourceID=${sourcePadId}` +
- `&destinationID=${newPad}&force=false`)
- .set("Authorization", (await common.generateJWTToken()))
- .expect(200)
- .expect('Content-Type', /json/)
- .expect((res:any) => assert.equal(res.body.code, 0));
-
- const newBodySrc = 'bold';
- const newBodyDst = 'italic';
- await setBody(sourcePadId, newBodySrc);
- await setBody(newPad, newBodyDst);
- await setBody(sourcePadId, `${newBodySrc} foo`);
-
- let [srcHtml, dstHtml] = await Promise.all([getHtml(sourcePadId), getHtml(newPad)]);
- assert.match(srcHtml, new RegExp(newBodySrc));
- assert.match(dstHtml, new RegExp(newBodyDst));
-
- // Force the server to re-read the pads from the database. This rebuilds the attribute pool
- // objects from scratch, ensuring that an internally inconsistent attribute pool object did
- // not cause the above tests to accidentally pass.
- const reInitPad = async (padId:string) => {
- const pad = await padManager.getPad(padId);
- await pad.init();
- };
- await Promise.all([
- reInitPad(sourcePadId),
- reInitPad(newPad),
- ]);
-
- [srcHtml, dstHtml] = await Promise.all([getHtml(sourcePadId), getHtml(newPad)]);
- assert.match(srcHtml, new RegExp(newBodySrc));
- assert.match(dstHtml, new RegExp(newBodyDst));
- });
- });
+ describe("Tests", () => {
+ it("deletes a Pad that does not exist", async () => {
+ await agent
+ .get(`${endPoint("deletePad")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200) // @TODO: we shouldn't expect 200 here since the pad may not exist
+ .expect("Content-Type", /json/);
+ });
+
+ it("creates a new Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("createPad")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("gets revision count of Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("getRevisionsCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.equal(res.body.data.revisions, 0);
+ });
+
+ it("gets saved revisions count of Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("getSavedRevisionsCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.equal(res.body.data.savedRevisions, 0);
+ });
+
+ it("gets saved revision list of Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("listSavedRevisions")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.deepEqual(res.body.data.savedRevisions, []);
+ });
+
+ it("get the HTML of Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("getHTML")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert(res.body.data.html.length > 1);
+ });
+
+ it("list all pads", async () => {
+ const res = await agent
+ .get(endPoint("listAllPads"))
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert(res.body.data.padIDs.includes(testPadId));
+ });
+
+ it("deletes the Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("deletePad")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("list all pads again", async () => {
+ const res = await agent
+ .get(endPoint("listAllPads"))
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert(!res.body.data.padIDs.includes(testPadId));
+ });
+
+ it("get the HTML of a Pad -- Should return a failure", async () => {
+ const res = await agent
+ .get(`${endPoint("getHTML")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 1);
+ });
+
+ it("creates a new Pad with text", async () => {
+ const res = await agent
+ .get(`${endPoint("createPad")}?padID=${testPadId}&text=testText`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("gets the Pad text and expect it to be testText with trailing \\n", async () => {
+ const res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.text, "testText\n");
+ });
+
+ it("set text", async () => {
+ const res = await agent
+ .post(endPoint("setText"))
+ .set("Authorization", await common.generateJWTToken())
+ .send({
+ padID: testPadId,
+ text: "testTextTwo",
+ })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("gets the Pad text", async () => {
+ const res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.text, "testTextTwo\n");
+ });
+
+ it("gets Revision Count of a Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("getRevisionsCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.revisions, 1);
+ });
+
+ it("saves Revision", async () => {
+ const res = await agent
+ .get(`${endPoint("saveRevision")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("gets saved revisions count of Pad again", async () => {
+ const res = await agent
+ .get(`${endPoint("getSavedRevisionsCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.equal(res.body.data.savedRevisions, 1);
+ });
+
+ it("gets saved revision list of Pad again", async () => {
+ const res = await agent
+ .get(`${endPoint("listSavedRevisions")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.deepEqual(res.body.data.savedRevisions, [1]);
+ });
+
+ it("gets User Count of a Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("padUsersCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.padUsersCount, 0);
+ });
+
+ it("Gets the Read Only ID of a Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("getReadOnlyID")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert(res.body.data.readOnlyID);
+ });
+
+ it("Get Authors of the Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("listAuthorsOfPad")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.authorIDs.length, 0);
+ });
+
+ it("Get When Pad was left Edited", async () => {
+ const res = await agent
+ .get(`${endPoint("getLastEdited")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert(res.body.data.lastEdited);
+ lastEdited = res.body.data.lastEdited;
+ });
+
+ it("set text again", async () => {
+ const res = await agent
+ .post(endPoint("setText"))
+ .set("Authorization", await common.generateJWTToken())
+ .send({
+ padID: testPadId,
+ text: "testTextThree",
+ })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("Get When Pad was left Edited again", async () => {
+ const res = await agent
+ .get(`${endPoint("getLastEdited")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert(res.body.data.lastEdited > lastEdited);
+ });
+
+ it("gets User Count of a Pad again", async () => {
+ const res = await agent
+ .get(`${endPoint("padUsers")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.padUsers.length, 0);
+ });
+
+ it("deletes a Pad", async () => {
+ const res = await agent
+ .get(`${endPoint("deletePad")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("creates the Pad again", async () => {
+ const res = await agent
+ .get(`${endPoint("createPad")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("Sets text on a pad Id", async () => {
+ const res = await agent
+ .post(`${endPoint("setText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .field({ text })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("Gets text on a pad Id", async () => {
+ const res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.equal(res.body.data.text, `${text}\n`);
+ });
+
+ it("Sets text on a pad Id including an explicit newline", async () => {
+ const res = await agent
+ .post(`${endPoint("setText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .field({ text: `${text}\n` })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("Gets text on a pad Id and doesn't have an excess newline", async () => {
+ const res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.equal(res.body.data.text, `${text}\n`);
+ });
+
+ it("Gets when pad was last edited", async () => {
+ const res = await agent
+ .get(`${endPoint("getLastEdited")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.notEqual(res.body.lastEdited, 0);
+ });
+
+ it("Move a Pad to a different Pad ID", async () => {
+ const res = await agent
+ .get(
+ `${endPoint(
+ "movePad",
+ )}?sourceID=${testPadId}&destinationID=${newPadId}&force=true`,
+ )
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("Gets text from new pad", async () => {
+ const res = await agent
+ .get(`${endPoint("getText")}?padID=${newPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.text, `${text}\n`);
+ });
+
+ it("Move pad back to original ID", async () => {
+ const res = await agent
+ .get(
+ `${endPoint(
+ "movePad",
+ )}?sourceID=${newPadId}&destinationID=${testPadId}&force=false`,
+ )
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("Get text using original ID", async () => {
+ const res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.text, `${text}\n`);
+ });
+
+ it("Get last edit of original ID", async () => {
+ const res = await agent
+ .get(`${endPoint("getLastEdited")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.notEqual(res.body.lastEdited, 0);
+ });
+
+ it("Append text to a pad Id", async () => {
+ let res = await agent
+ .get(
+ `${endPoint("appendText", "1.2.13")}?padID=${testPadId}&text=hello`,
+ )
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.equal(res.body.data.text, `${text}hello\n`);
+ });
+
+ it("getText of old revision", async () => {
+ let res = await agent
+ .get(`${endPoint("getRevisionsCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ const rev = res.body.data.revisions;
+ assert(rev != null);
+ assert(Number.isInteger(rev));
+ assert(rev > 0);
+ res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}&rev=${rev - 1}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ assert.equal(res.body.data.text, `${text}\n`);
+ });
+
+ it("Sets the HTML of a Pad attempting to pass ugly HTML", async () => {
+ const html = "
Hello HTML
";
+ const res = await agent
+ .post(endPoint("setHTML"))
+ .set("Authorization", await common.generateJWTToken())
+ .send({
+ padID: testPadId,
+ html,
+ })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("Pad with complex nested lists of different types", async () => {
+ let res = await agent
+ .post(endPoint("setHTML"))
+ .set("Authorization", await common.generateJWTToken())
+ .send({
+ padID: testPadId,
+ html: ulHtml,
+ })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ res = await agent
+ .get(`${endPoint("getHTML")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ const receivedHtml = res.body.data.html
+ .replace(" ", "")
+ .toLowerCase();
+ assert.equal(receivedHtml, expectedHtml);
+ });
+
+ it("Pad with white space between list items", async () => {
+ let res = await agent
+ .get(`${endPoint("setHTML")}?padID=${testPadId}&html=${ulSpaceHtml}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ res = await agent
+ .get(`${endPoint("getHTML")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ const receivedHtml = res.body.data.html
+ .replace(" ", "")
+ .toLowerCase();
+ assert.equal(receivedHtml, expectedSpaceHtml);
+ });
+
+ it("errors if pad can be created", async () => {
+ await Promise.all(
+ ["/", "%23", "%3F", "%26"].map(async (badUrlChar) => {
+ const res = await agent
+ .get(`${endPoint("createPad")}?padID=${badUrlChar}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 1);
+ }),
+ );
+ });
+
+ it("copies the content of a existent pad", async () => {
+ const res = await agent
+ .get(
+ `${endPoint(
+ "copyPad",
+ )}?sourceID=${testPadId}&destinationID=${copiedPadId}&force=true`,
+ )
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ it("does not add an useless revision", async () => {
+ let res = await agent
+ .post(`${endPoint("setText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .field({ text: "identical text\n" })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+
+ res = await agent
+ .get(`${endPoint("getText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.text, "identical text\n");
+
+ res = await agent
+ .get(`${endPoint("getRevisionsCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ const revCount = res.body.data.revisions;
+
+ res = await agent
+ .post(`${endPoint("setText")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .field({ text: "identical text\n" })
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+
+ res = await agent
+ .get(`${endPoint("getRevisionsCount")}?padID=${testPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.data.revisions, revCount);
+ });
+
+ it("creates a new Pad with empty text", async () => {
+ await agent
+ .get(`${endPoint("createPad")}?padID=${anotherPadId}&text=`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ .expect((res: any) => {
+ assert.equal(res.body.code, 0, "Unable to create new Pad");
+ });
+ await agent
+ .get(`${endPoint("getText")}?padID=${anotherPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ .expect((res: any) => {
+ assert.equal(res.body.code, 0, "Unable to get pad text");
+ assert.equal(res.body.data.text, "\n", "Pad text is not empty");
+ });
+ });
+
+ it("deletes with empty text", async () => {
+ await agent
+ .get(`${endPoint("deletePad")}?padID=${anotherPadId}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect("Content-Type", /json/)
+ .expect(200)
+ .expect((res: any) => {
+ assert.equal(res.body.code, 0, "Unable to delete empty Pad");
+ });
+ });
+ });
+
+ describe("copyPadWithoutHistory", () => {
+ const sourcePadId = makeid();
+ let newPad: string;
+
+ before(async () => {
+ await createNewPadWithHtml(sourcePadId, ulHtml);
+ });
+
+ beforeEach(async () => {
+ newPad = makeid();
+ });
+
+ it("returns a successful response", async () => {
+ const res = await agent
+ .get(
+ `${endPoint("copyPadWithoutHistory")}?sourceID=${sourcePadId}` +
+ `&destinationID=${newPad}&force=false`,
+ )
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200)
+ .expect("Content-Type", /json/);
+ assert.equal(res.body.code, 0);
+ });
+
+ // this test validates if the source pad's text and attributes are kept
+ it("creates a new pad with the same content as the source pad", async () => {
+ let res = await agent
+ .get(
+ `${endPoint("copyPadWithoutHistory")}?sourceID=${sourcePadId}` +
+ `&destinationID=${newPad}&force=false`,
+ )
+ .set("Authorization", await common.generateJWTToken());
+ assert.equal(res.body.code, 0);
+ res = await agent
+ .get(`${endPoint("getHTML")}?padID=${newPad}`)
+ .set("Authorization", await common.generateJWTToken())
+ .expect(200);
+ const receivedHtml = res.body.data.html
+ .replace("
',
- wantAlines: [
- '*0*2*6+1+1',
- '*0*3*6+1+1',
- '*0*2*6+1+1',
- '+3',
- ],
- wantText: ['*a', '*b', '*a', 'foo'],
- },
- {
- description: 'Tests if ols properly get line numbers when in a normal OL',
- html: '
a
b
c
test
',
- wantAlines: [
- '*0*4*6*7+1+1',
- '*0*4*6*7+1+1',
- '*0*4*6*7+1+1',
- '+4',
- ],
- wantText: ['*a', '*b', '*c', 'test'],
- noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
- },
- {
- description: 'A single completely empty line break within an ol should reset count if OL is closed off..',
- html: '
should be 1
hello
should be 1
should be 2
',
- wantAlines: [
- '*0*4*6*7+1+b',
- '+5',
- '*0*4*6*8+1+b',
- '*0*4*6*8+1+b',
- '',
- ],
- wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''],
- noteToSelf: "Shouldn't include attribute marker in the
line",
- },
- {
- description: 'A single
should create a new line',
- html: '',
- wantAlines: ['', ''],
- wantText: ['', ''],
- noteToSelf: 'should create a line break but not break numbering',
- },
- {
- description: 'Tests if ols properly get line numbers when in a normal OL #2',
- html: 'a
b
c
notlist
foo
',
- wantAlines: [
- '+1',
- '*0*4*6*7+1+1',
- '*0*5*6*8+1+1',
- '+7',
- '+3',
- ],
- wantText: ['a', '*b', '*c', 'notlist', 'foo'],
- noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
- },
- {
- description: 'First item being an UL then subsequent being OL will fail',
- html: '
a
b
c
',
- wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'],
- wantText: ['a', '*b', '*c'],
- noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
- disabled: true,
- },
- {
- description: 'A single completely empty line break within an ol should NOT reset count',
- html: '
should be 1
should be 2
should be 3
',
- wantAlines: [],
- wantText: ['*should be 1', '*should be 2', '*should be 3'],
- noteToSelf: "should create a line break but not break numbering -- This is what I can't get working!",
- disabled: true,
- },
- {
- description: 'Content outside body should be ignored',
- html: 'titleempty ',
- wantAlines: ['+5'],
- wantText: ['empty'],
- },
- {
- description: 'Multiple spaces should be preserved',
- html: 'Text with more than one space. ',
- wantAlines: ['+10'],
- wantText: ['Text with more than one space.'],
- },
- {
- description: 'non-breaking and normal space should be preserved',
- html: 'Text with more than one space. ',
- wantAlines: ['+10'],
- wantText: ['Text with more than one space.'],
- },
- {
- description: 'Multiple nbsp should be preserved',
- html: ' ',
- wantAlines: ['+2'],
- wantText: [' '],
- },
- {
- description: 'Multiple nbsp between words ',
- html: ' word1 word2 word3 ',
- wantAlines: ['+m'],
- wantText: [' word1 word2 word3'],
- },
- {
- description: 'A non-breaking space preceded by a normal space',
- html: ' word1 word2 word3 ',
- wantAlines: ['+l'],
- wantText: [' word1 word2 word3'],
- },
- {
- description: 'A non-breaking space followed by a normal space',
- html: ' word1 word2 word3 ',
- wantAlines: ['+l'],
- wantText: [' word1 word2 word3'],
- },
- {
- description: 'Don\'t collapse spaces that follow a newline',
- html: 'something something ',
- wantAlines: ['+9', '+m'],
- wantText: ['something', ' something'],
- },
- {
- description: 'Don\'t collapse spaces that follow a empty paragraph',
- html: 'something something ',
- wantAlines: ['+9', '', '+m'],
- wantText: ['something', '', ' something'],
- },
- {
- description: 'Don\'t collapse spaces that preceed/follow a newline',
- html: 'something something ',
- wantAlines: ['+l', '+m'],
- wantText: ['something ', ' something'],
- },
- {
- description: 'Don\'t collapse spaces that preceed/follow a empty paragraph',
- html: 'something something ',
- wantAlines: ['+l', '', '+m'],
- wantText: ['something ', '', ' something'],
- },
- {
- description: 'Don\'t collapse non-breaking spaces that follow a newline',
- html: 'something something ',
- wantAlines: ['+9', '+c'],
- wantText: ['something', ' something'],
- },
- {
- description: 'Don\'t collapse non-breaking spaces that follow a paragraph',
- html: 'something something ',
- wantAlines: ['+9', '', '+c'],
- wantText: ['something', '', ' something'],
- },
- {
- description: 'Preserve all spaces when multiple are present',
- html: 'Need more space s ! ',
- wantAlines: ['+h*1+4+2'],
- wantText: ['Need more space s !'],
- },
- {
- description: 'Newlines and multiple spaces across newlines should be preserved',
- html: `
+ {
+ description: "Simple",
+ html: "
",
+ wantAlines: ["*0*2*6+1+1", "*0*3*6+1+1", "*0*2*6+1+1", "+3"],
+ wantText: ["*a", "*b", "*a", "foo"],
+ },
+ {
+ description: "Tests if ols properly get line numbers when in a normal OL",
+ html: "
a
b
c
test
",
+ wantAlines: ["*0*4*6*7+1+1", "*0*4*6*7+1+1", "*0*4*6*7+1+1", "+4"],
+ wantText: ["*a", "*b", "*c", "test"],
+ noteToSelf:
+ "Ensure empty P does not induce line attribute marker, wont this break the editor?",
+ },
+ {
+ description:
+ "A single completely empty line break within an ol should reset count if OL is closed off..",
+ html: "
should be 1
hello
should be 1
should be 2
",
+ wantAlines: ["*0*4*6*7+1+b", "+5", "*0*4*6*8+1+b", "*0*4*6*8+1+b", ""],
+ wantText: ["*should be 1", "hello", "*should be 1", "*should be 2", ""],
+ noteToSelf: "Shouldn't include attribute marker in the
line",
+ },
+ {
+ description: "A single
should create a new line",
+ html: "",
+ wantAlines: ["", ""],
+ wantText: ["", ""],
+ noteToSelf: "should create a line break but not break numbering",
+ },
+ {
+ description:
+ "Tests if ols properly get line numbers when in a normal OL #2",
+ html: "a
b
c
notlist
foo
",
+ wantAlines: ["+1", "*0*4*6*7+1+1", "*0*5*6*8+1+1", "+7", "+3"],
+ wantText: ["a", "*b", "*c", "notlist", "foo"],
+ noteToSelf:
+ "Ensure empty P does not induce line attribute marker, wont this break the editor?",
+ },
+ {
+ description: "First item being an UL then subsequent being OL will fail",
+ html: "
a
b
c
",
+ wantAlines: ["+1", "*0*1*2*3+1+1", "*0*4*2*5+1+1"],
+ wantText: ["a", "*b", "*c"],
+ noteToSelf:
+ "Ensure empty P does not induce line attribute marker, wont this break the editor?",
+ disabled: true,
+ },
+ {
+ description:
+ "A single completely empty line break within an ol should NOT reset count",
+ html: "
should be 1
should be 2
should be 3
",
+ wantAlines: [],
+ wantText: ["*should be 1", "*should be 2", "*should be 3"],
+ noteToSelf:
+ "should create a line break but not break numbering -- This is what I can't get working!",
+ disabled: true,
+ },
+ {
+ description: "Content outside body should be ignored",
+ html: "titleempty ",
+ wantAlines: ["+5"],
+ wantText: ["empty"],
+ },
+ {
+ description: "Multiple spaces should be preserved",
+ html: "Text with more than one space. ",
+ wantAlines: ["+10"],
+ wantText: ["Text with more than one space."],
+ },
+ {
+ description: "non-breaking and normal space should be preserved",
+ html: "Text with more than one space. ",
+ wantAlines: ["+10"],
+ wantText: ["Text with more than one space."],
+ },
+ {
+ description: "Multiple nbsp should be preserved",
+ html: " ",
+ wantAlines: ["+2"],
+ wantText: [" "],
+ },
+ {
+ description: "Multiple nbsp between words ",
+ html: " word1 word2 word3 ",
+ wantAlines: ["+m"],
+ wantText: [" word1 word2 word3"],
+ },
+ {
+ description: "A non-breaking space preceded by a normal space",
+ html: " word1 word2 word3 ",
+ wantAlines: ["+l"],
+ wantText: [" word1 word2 word3"],
+ },
+ {
+ description: "A non-breaking space followed by a normal space",
+ html: " word1 word2 word3 ",
+ wantAlines: ["+l"],
+ wantText: [" word1 word2 word3"],
+ },
+ {
+ description: "Don't collapse spaces that follow a newline",
+ html: "something something ",
+ wantAlines: ["+9", "+m"],
+ wantText: ["something", " something"],
+ },
+ {
+ description: "Don't collapse spaces that follow a empty paragraph",
+ html: "something something ",
+ wantAlines: ["+9", "", "+m"],
+ wantText: ["something", "", " something"],
+ },
+ {
+ description: "Don't collapse spaces that preceed/follow a newline",
+ html: "something something ",
+ wantAlines: ["+l", "+m"],
+ wantText: ["something ", " something"],
+ },
+ {
+ description: "Don't collapse spaces that preceed/follow a empty paragraph",
+ html: "something something ",
+ wantAlines: ["+l", "", "+m"],
+ wantText: ["something ", "", " something"],
+ },
+ {
+ description: "Don't collapse non-breaking spaces that follow a newline",
+ html: "something something ",
+ wantAlines: ["+9", "+c"],
+ wantText: ["something", " something"],
+ },
+ {
+ description: "Don't collapse non-breaking spaces that follow a paragraph",
+ html: "something something ",
+ wantAlines: ["+9", "", "+c"],
+ wantText: ["something", "", " something"],
+ },
+ {
+ description: "Preserve all spaces when multiple are present",
+ html: "Need more space s ! ",
+ wantAlines: ["+h*1+4+2"],
+ wantText: ["Need more space s !"],
+ },
+ {
+ description:
+ "Newlines and multiple spaces across newlines should be preserved",
+ html: `
Need
more
space
s
! `,
- wantAlines: ['+19*1+4+b'],
- wantText: ['Need more space s !'],
- },
- {
- description: 'Multiple new lines at the beginning should be preserved',
- html: '
first line
second line ',
- wantAlines: ['', '', '', '', '+a', '', '+b'],
- wantText: ['', '', '', '', 'first line', '', 'second line'],
- },
- {
- description: 'A paragraph with multiple lines should not loose spaces when lines are combined',
- html: `
+ wantAlines: ["+19*1+4+b"],
+ wantText: ["Need more space s !"],
+ },
+ {
+ description: "Multiple new lines at the beginning should be preserved",
+ html: "
first line
second line ",
+ wantAlines: ["", "", "", "", "+a", "", "+b"],
+ wantText: ["", "", "", "", "first line", "", "second line"],
+ },
+ {
+ description:
+ "A paragraph with multiple lines should not loose spaces when lines are combined",
+ html: `
а б в г ґ д е є ж з и і ї й к л м н о
п р с т у ф х ц ч ш щ ю я ь
`,
- wantAlines: ['+1t'],
- wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'],
- },
- {
- description: 'lines in preformatted text should be kept intact',
- html: `
+ wantAlines: ["+1t"],
+ wantText: [
+ "а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь",
+ ],
+ },
+ {
+ description: "lines in preformatted text should be kept intact",
+ html: `
а б в г ґ д е є ж з и і ї й к л м н о
multiple
lines
in
@@ -288,104 +264,108 @@ pre
п р с т у ф х ц ч ш щ ю я
ь
`,
- wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'],
- wantText: [
- 'а б в г ґ д е є ж з и і ї й к л м н о',
- 'multiple',
- 'lines',
- 'in',
- 'pre',
- 'п р с т у ф х ц ч ш щ ю я ь',
- ],
- },
- {
- description: 'pre should be on a new line not preceded by a space',
- html: `
+ wantAlines: ["+11", "+8", "+5", "+2", "+3", "+r"],
+ wantText: [
+ "а б в г ґ д е є ж з и і ї й к л м н о",
+ "multiple",
+ "lines",
+ "in",
+ "pre",
+ "п р с т у ф х ц ч ш щ ю я ь",
+ ],
+ },
+ {
+ description: "pre should be on a new line not preceded by a space",
+ html: `
1
preline
`,
- wantAlines: ['+6', '+7'],
- wantText: [' 1 ', 'preline'],
- },
- {
- description: 'Preserve spaces on the beginning and end of a element',
- html: 'Need more space s ! ',
- wantAlines: ['+f*1+3+1'],
- wantText: ['Need more space s !'],
- },
- {
- description: 'Preserve spaces outside elements',
- html: 'Need more space s ! ',
- wantAlines: ['+g*1+1+2'],
- wantText: ['Need more space s !'],
- },
- {
- description: 'Preserve spaces at the end of an element',
- html: 'Need more space s ! ',
- wantAlines: ['+g*1+2+1'],
- wantText: ['Need more space s !'],
- },
- {
- description: 'Preserve spaces at the start of an element',
- html: 'Need more space s ! ',
- wantAlines: ['+f*1+2+2'],
- wantText: ['Need more space s !'],
- },
+ wantAlines: ["+6", "+7"],
+ wantText: [" 1 ", "preline"],
+ },
+ {
+ description: "Preserve spaces on the beginning and end of a element",
+ html: "Need more space s ! ",
+ wantAlines: ["+f*1+3+1"],
+ wantText: ["Need more space s !"],
+ },
+ {
+ description: "Preserve spaces outside elements",
+ html: "Need more space s ! ",
+ wantAlines: ["+g*1+1+2"],
+ wantText: ["Need more space s !"],
+ },
+ {
+ description: "Preserve spaces at the end of an element",
+ html: "Need more space s ! ",
+ wantAlines: ["+g*1+2+1"],
+ wantText: ["Need more space s !"],
+ },
+ {
+ description: "Preserve spaces at the start of an element",
+ html: "Need more space s ! ",
+ wantAlines: ["+f*1+2+2"],
+ wantText: ["Need more space s !"],
+ },
];
-describe(__filename, function () {
- for (const tc of testCases) {
- describe(tc.description, function () {
- let apool: APool;
- let result: {
- lines: string[],
- lineAttribs: string[],
- };
+describe(__filename, () => {
+ for (const tc of testCases) {
+ describe(tc.description, () => {
+ let apool: APool;
+ let result: {
+ lines: string[];
+ lineAttribs: string[];
+ };
- before(async function () {
- if (tc.disabled) return this.skip();
- const {window: {document}} = new jsdom.JSDOM(tc.html);
- apool = new AttributePool();
- // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all
- // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute
- // numbers do not change if the attribute processing code changes.)
- for (const attrib of knownAttribs) apool.putAttrib(attrib);
- for (const aline of tc.wantAlines) {
- for (const op of Changeset.deserializeOps(aline)) {
- for (const n of attributes.decodeAttribString(op.attribs)) {
- assert(n < knownAttribs.length);
- }
- }
- }
- const cc = contentcollector.makeContentCollector(true, null, apool);
- cc.collectContent(document.body);
- result = cc.finish();
- });
+ before(async function () {
+ if (tc.disabled) return this.skip();
+ const {
+ window: { document },
+ } = new jsdom.JSDOM(tc.html);
+ apool = new AttributePool();
+ // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all
+ // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute
+ // numbers do not change if the attribute processing code changes.)
+ for (const attrib of knownAttribs) apool.putAttrib(attrib);
+ for (const aline of tc.wantAlines) {
+ for (const op of Changeset.deserializeOps(aline)) {
+ for (const n of attributes.decodeAttribString(op.attribs)) {
+ assert(n < knownAttribs.length);
+ }
+ }
+ }
+ const cc = contentcollector.makeContentCollector(true, null, apool);
+ cc.collectContent(document.body);
+ result = cc.finish();
+ });
- it('text matches', async function () {
- assert.deepEqual(result.lines, tc.wantText);
- });
+ it("text matches", async () => {
+ assert.deepEqual(result.lines, tc.wantText);
+ });
- it('alines match', async function () {
- assert.deepEqual(result.lineAttribs, tc.wantAlines);
- });
+ it("alines match", async () => {
+ assert.deepEqual(result.lineAttribs, tc.wantAlines);
+ });
- it('attributes are sorted in canonical order', async function () {
- const gotAttribs:string[][][] = [];
- const wantAttribs = [];
- for (const aline of result.lineAttribs) {
- const gotAlineAttribs:string[][] = [];
- gotAttribs.push(gotAlineAttribs);
- const wantAlineAttribs:string[] = [];
- wantAttribs.push(wantAlineAttribs);
- for (const op of Changeset.deserializeOps(aline)) {
- const gotOpAttribs:string[] = [...attributes.attribsFromString(op.attribs, apool)];
- gotAlineAttribs.push(gotOpAttribs);
- wantAlineAttribs.push(attributes.sort([...gotOpAttribs]));
- }
- }
- assert.deepEqual(gotAttribs, wantAttribs);
- });
- });
- }
+ it("attributes are sorted in canonical order", async () => {
+ const gotAttribs: string[][][] = [];
+ const wantAttribs = [];
+ for (const aline of result.lineAttribs) {
+ const gotAlineAttribs: string[][] = [];
+ gotAttribs.push(gotAlineAttribs);
+ const wantAlineAttribs: string[] = [];
+ wantAttribs.push(wantAlineAttribs);
+ for (const op of Changeset.deserializeOps(aline)) {
+ const gotOpAttribs: string[] = [
+ ...attributes.attribsFromString(op.attribs, apool),
+ ];
+ gotAlineAttribs.push(gotOpAttribs);
+ wantAlineAttribs.push(attributes.sort([...gotOpAttribs]));
+ }
+ }
+ assert.deepEqual(gotAttribs, wantAttribs);
+ });
+ });
+ }
});
diff --git a/src/tests/backend/specs/crypto.ts b/src/tests/backend/specs/crypto.ts
index 62d79f1b3..572c94754 100644
--- a/src/tests/backend/specs/crypto.ts
+++ b/src/tests/backend/specs/crypto.ts
@@ -1,10 +1,7 @@
-'use strict';
-
-
-import {Buffer} from 'buffer';
-import nodeCrypto from 'crypto';
-import util from 'util';
+import { Buffer } from "buffer";
+import nodeCrypto from "crypto";
+import util from "util";
const nodeHkdf = nodeCrypto.hkdf ? util.promisify(nodeCrypto.hkdf) : null;
-const ab2hex = (ab:string) => Buffer.from(ab).toString('hex');
+const ab2hex = (ab: string) => Buffer.from(ab).toString("hex");
diff --git a/src/tests/backend/specs/export.ts b/src/tests/backend/specs/export.ts
index de436f88c..df9d5d158 100644
--- a/src/tests/backend/specs/export.ts
+++ b/src/tests/backend/specs/export.ts
@@ -1,28 +1,25 @@
-'use strict';
+import type { MapArrayType } from "../../../node/types/MapType";
-import {MapArrayType} from "../../../node/types/MapType";
+const common = require("../common");
+const padManager = require("../../../node/db/PadManager");
+const settings = require("../../../node/utils/Settings");
-const common = require('../common');
-const padManager = require('../../../node/db/PadManager');
-const settings = require('../../../node/utils/Settings');
+describe(__filename, () => {
+ let agent: any;
+ const settingsBackup: MapArrayType = {};
-describe(__filename, function () {
- let agent:any;
- const settingsBackup:MapArrayType = {};
+ before(async () => {
+ agent = await common.init();
+ settingsBackup.soffice = settings.soffice;
+ await padManager.getPad("testExportPad", "test content");
+ });
- before(async function () {
- agent = await common.init();
- settingsBackup.soffice = settings.soffice;
- await padManager.getPad('testExportPad', 'test content');
- });
+ after(async () => {
+ Object.assign(settings, settingsBackup);
+ });
- after(async function () {
- Object.assign(settings, settingsBackup);
- });
-
- it('returns 500 on export error', async function () {
- settings.soffice = 'false'; // '/bin/false' doesn't work on Windows
- await agent.get('/p/testExportPad/export/doc')
- .expect(500);
- });
+ it("returns 500 on export error", async () => {
+ settings.soffice = "false"; // '/bin/false' doesn't work on Windows
+ await agent.get("/p/testExportPad/export/doc").expect(500);
+ });
});
diff --git a/src/tests/backend/specs/favicon.ts b/src/tests/backend/specs/favicon.ts
index 6b6230b4b..31cf8854f 100644
--- a/src/tests/backend/specs/favicon.ts
+++ b/src/tests/backend/specs/favicon.ts
@@ -1,99 +1,123 @@
-'use strict';
+import type { MapArrayType } from "../../../node/types/MapType";
-import {MapArrayType} from "../../../node/types/MapType";
-
-const assert = require('assert').strict;
-const common = require('../common');
-const fs = require('fs');
+const assert = require("assert").strict;
+const common = require("../common");
+const fs = require("fs");
const fsp = fs.promises;
-const path = require('path');
-const settings = require('../../../node/utils/Settings');
-const superagent = require('superagent');
+const path = require("path");
+const settings = require("../../../node/utils/Settings");
+const superagent = require("superagent");
-describe(__filename, function () {
- let agent:any;
- let backupSettings:MapArrayType;
- let skinDir: string;
- let wantCustomIcon: boolean;
- let wantDefaultIcon: boolean;
- let wantSkinIcon: boolean;
+describe(__filename, () => {
+ let agent: any;
+ let backupSettings: MapArrayType;
+ let skinDir: string;
+ let wantCustomIcon: boolean;
+ let wantDefaultIcon: boolean;
+ let wantSkinIcon: boolean;
- before(async function () {
- agent = await common.init();
- wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png'));
- wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico'));
- wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png'));
- });
+ before(async () => {
+ agent = await common.init();
+ wantCustomIcon = await fsp.readFile(
+ path.join(__dirname, "favicon-test-custom.png"),
+ );
+ wantDefaultIcon = await fsp.readFile(
+ path.join(settings.root, "src", "static", "favicon.ico"),
+ );
+ wantSkinIcon = await fsp.readFile(
+ path.join(__dirname, "favicon-test-skin.png"),
+ );
+ });
- beforeEach(async function () {
- backupSettings = {...settings};
- skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-'));
- settings.skinName = path.basename(skinDir);
- });
+ beforeEach(async () => {
+ backupSettings = { ...settings };
+ skinDir = await fsp.mkdtemp(
+ path.join(settings.root, "src", "static", "skins", "test-"),
+ );
+ settings.skinName = path.basename(skinDir);
+ });
- afterEach(async function () {
- delete settings.favicon;
- delete settings.skinName;
- Object.assign(settings, backupSettings);
- try {
- // TODO: The {recursive: true} option wasn't added to fsp.rmdir() until Node.js v12.10.0 so we
- // can't rely on it until support for Node.js v10 is dropped.
- await fsp.unlink(path.join(skinDir, 'favicon.ico'));
- await fsp.rmdir(skinDir, {recursive: true});
- } catch (err) { /* intentionally ignored */ }
- });
+ afterEach(async () => {
+ delete settings.favicon;
+ delete settings.skinName;
+ Object.assign(settings, backupSettings);
+ try {
+ // TODO: The {recursive: true} option wasn't added to fsp.rmdir() until Node.js v12.10.0 so we
+ // can't rely on it until support for Node.js v10 is dropped.
+ await fsp.unlink(path.join(skinDir, "favicon.ico"));
+ await fsp.rmdir(skinDir, { recursive: true });
+ } catch (err) {
+ /* intentionally ignored */
+ }
+ });
- it('uses custom favicon if set (relative pathname)', async function () {
- settings.favicon =
- path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png'));
- assert(!path.isAbsolute(settings.favicon));
- const {body: gotIcon} = await agent.get('/favicon.ico')
- .accept('png').buffer(true).parse(superagent.parse.image)
- .expect(200);
- assert(gotIcon.equals(wantCustomIcon));
- });
+ it("uses custom favicon if set (relative pathname)", async () => {
+ settings.favicon = path.relative(
+ settings.root,
+ path.join(__dirname, "favicon-test-custom.png"),
+ );
+ assert(!path.isAbsolute(settings.favicon));
+ const { body: gotIcon } = await agent
+ .get("/favicon.ico")
+ .accept("png")
+ .buffer(true)
+ .parse(superagent.parse.image)
+ .expect(200);
+ assert(gotIcon.equals(wantCustomIcon));
+ });
- it('uses custom favicon from url', async function () {
- settings.favicon = 'https://etherpad.org/favicon.ico';
- await agent.get('/favicon.ico')
- .expect(302);
- });
+ it("uses custom favicon from url", async () => {
+ settings.favicon = "https://etherpad.org/favicon.ico";
+ await agent.get("/favicon.ico").expect(302);
+ });
- it('uses custom favicon if set (absolute pathname)', async function () {
- settings.favicon = path.join(__dirname, 'favicon-test-custom.png');
- assert(path.isAbsolute(settings.favicon));
- const {body: gotIcon} = await agent.get('/favicon.ico')
- .accept('png').buffer(true).parse(superagent.parse.image)
- .expect(200);
- assert(gotIcon.equals(wantCustomIcon));
- });
+ it("uses custom favicon if set (absolute pathname)", async () => {
+ settings.favicon = path.join(__dirname, "favicon-test-custom.png");
+ assert(path.isAbsolute(settings.favicon));
+ const { body: gotIcon } = await agent
+ .get("/favicon.ico")
+ .accept("png")
+ .buffer(true)
+ .parse(superagent.parse.image)
+ .expect(200);
+ assert(gotIcon.equals(wantCustomIcon));
+ });
- it('falls back if custom favicon is missing', async function () {
- // The previous default for settings.favicon was 'favicon.ico', so many users will continue to
- // have that in their settings.json for a long time. There is unlikely to be a favicon at
- // path.resolve(settings.root, 'favicon.ico'), so this test ensures that 'favicon.ico' won't be
- // a problem for those users.
- settings.favicon = 'favicon.ico';
- const {body: gotIcon} = await agent.get('/favicon.ico')
- .accept('png').buffer(true).parse(superagent.parse.image)
- .expect(200);
- assert(gotIcon.equals(wantDefaultIcon));
- });
+ it("falls back if custom favicon is missing", async () => {
+ // The previous default for settings.favicon was 'favicon.ico', so many users will continue to
+ // have that in their settings.json for a long time. There is unlikely to be a favicon at
+ // path.resolve(settings.root, 'favicon.ico'), so this test ensures that 'favicon.ico' won't be
+ // a problem for those users.
+ settings.favicon = "favicon.ico";
+ const { body: gotIcon } = await agent
+ .get("/favicon.ico")
+ .accept("png")
+ .buffer(true)
+ .parse(superagent.parse.image)
+ .expect(200);
+ assert(gotIcon.equals(wantDefaultIcon));
+ });
- it('uses skin favicon if present', async function () {
- await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon);
- settings.favicon = null;
- const {body: gotIcon} = await agent.get('/favicon.ico')
- .accept('png').buffer(true).parse(superagent.parse.image)
- .expect(200);
- assert(gotIcon.equals(wantSkinIcon));
- });
+ it("uses skin favicon if present", async () => {
+ await fsp.writeFile(path.join(skinDir, "favicon.ico"), wantSkinIcon);
+ settings.favicon = null;
+ const { body: gotIcon } = await agent
+ .get("/favicon.ico")
+ .accept("png")
+ .buffer(true)
+ .parse(superagent.parse.image)
+ .expect(200);
+ assert(gotIcon.equals(wantSkinIcon));
+ });
- it('falls back to default favicon', async function () {
- settings.favicon = null;
- const {body: gotIcon} = await agent.get('/favicon.ico')
- .accept('png').buffer(true).parse(superagent.parse.image)
- .expect(200);
- assert(gotIcon.equals(wantDefaultIcon));
- });
+ it("falls back to default favicon", async () => {
+ settings.favicon = null;
+ const { body: gotIcon } = await agent
+ .get("/favicon.ico")
+ .accept("png")
+ .buffer(true)
+ .parse(superagent.parse.image)
+ .expect(200);
+ assert(gotIcon.equals(wantDefaultIcon));
+ });
});
diff --git a/src/tests/backend/specs/health.ts b/src/tests/backend/specs/health.ts
index 97364a7e5..d191f00eb 100644
--- a/src/tests/backend/specs/health.ts
+++ b/src/tests/backend/specs/health.ts
@@ -1,58 +1,58 @@
-'use strict';
+import type { MapArrayType } from "../../../node/types/MapType";
-import {MapArrayType} from "../../../node/types/MapType";
+const assert = require("assert").strict;
+const common = require("../common");
+const settings = require("../../../node/utils/Settings");
+const superagent = require("superagent");
-const assert = require('assert').strict;
-const common = require('../common');
-const settings = require('../../../node/utils/Settings');
-const superagent = require('superagent');
+describe(__filename, () => {
+ let agent: any;
+ const backup: MapArrayType = {};
-describe(__filename, function () {
- let agent:any;
- const backup:MapArrayType = {};
+ const getHealth = () =>
+ agent
+ .get("/health")
+ .accept("application/health+json")
+ .buffer(true)
+ .parse(superagent.parse["application/json"])
+ .expect(200)
+ .expect((res: any) => assert.equal(res.type, "application/health+json"));
- const getHealth = () => agent.get('/health')
- .accept('application/health+json')
- .buffer(true)
- .parse(superagent.parse['application/json'])
- .expect(200)
- .expect((res:any) => assert.equal(res.type, 'application/health+json'));
+ before(async () => {
+ agent = await common.init();
+ });
- before(async function () {
- agent = await common.init();
- });
+ beforeEach(async () => {
+ backup.settings = {};
+ for (const setting of ["requireAuthentication", "requireAuthorization"]) {
+ backup.settings[setting] = settings[setting];
+ }
+ });
- beforeEach(async function () {
- backup.settings = {};
- for (const setting of ['requireAuthentication', 'requireAuthorization']) {
- backup.settings[setting] = settings[setting];
- }
- });
+ afterEach(async () => {
+ Object.assign(settings, backup.settings);
+ });
- afterEach(async function () {
- Object.assign(settings, backup.settings);
- });
+ it("/health works", async () => {
+ const res = await getHealth();
+ assert.equal(res.body.status, "pass");
+ assert.equal(res.body.releaseId, settings.getEpVersion());
+ });
- it('/health works', async function () {
- const res = await getHealth();
- assert.equal(res.body.status, 'pass');
- assert.equal(res.body.releaseId, settings.getEpVersion());
- });
+ it("auth is not required", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ const res = await getHealth();
+ assert.equal(res.body.status, "pass");
+ });
- it('auth is not required', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- const res = await getHealth();
- assert.equal(res.body.status, 'pass');
- });
-
- // We actually want to test that no express-session state is created, but that is difficult to do
- // without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a
- // cookie means that no express-session state was created (how would express-session look up the
- // session state if no ID was returned to the client?).
- it('no cookie is returned', async function () {
- const res = await getHealth();
- const cookie = res.headers['set-cookie'];
- assert(cookie == null, `unexpected Set-Cookie: ${cookie}`);
- });
+ // We actually want to test that no express-session state is created, but that is difficult to do
+ // without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a
+ // cookie means that no express-session state was created (how would express-session look up the
+ // session state if no ID was returned to the client?).
+ it("no cookie is returned", async () => {
+ const res = await getHealth();
+ const cookie = res.headers["set-cookie"];
+ assert(cookie == null, `unexpected Set-Cookie: ${cookie}`);
+ });
});
diff --git a/src/tests/backend/specs/hooks.ts b/src/tests/backend/specs/hooks.ts
index 07c6e262e..69acf855a 100644
--- a/src/tests/backend/specs/hooks.ts
+++ b/src/tests/backend/specs/hooks.ts
@@ -1,1244 +1,1415 @@
-'use strict';
-
-import {strict as assert} from 'assert';
-const hooks = require('../../../static/js/pluginfw/hooks');
-const plugins = require('../../../static/js/pluginfw/plugin_defs');
-import sinon from 'sinon';
-import {MapArrayType} from "../../../node/types/MapType";
-
+import { strict as assert } from "assert";
+const hooks = require("../../../static/js/pluginfw/hooks");
+const plugins = require("../../../static/js/pluginfw/plugin_defs");
+import sinon from "sinon";
+import type { MapArrayType } from "../../../node/types/MapType";
interface ExtendedConsole extends Console {
- warn: {
- (message?: any, ...optionalParams: any[]): void;
- callCount: number;
- getCall: (i: number) => {args: any[]};
- };
- error: {
- (message?: any, ...optionalParams: any[]): void;
- callCount: number;
- getCall: (i: number) => {args: any[]};
- callsFake: (fn: Function) => void;
- getCalls: () => {args: any[]}[];
- };
+ warn: {
+ (message?: any, ...optionalParams: any[]): void;
+ callCount: number;
+ getCall: (i: number) => { args: any[] };
+ };
+ error: {
+ (message?: any, ...optionalParams: any[]): void;
+ callCount: number;
+ getCall: (i: number) => { args: any[] };
+ callsFake: (fn: Function) => void;
+ getCalls: () => { args: any[] }[];
+ };
}
declare var console: ExtendedConsole;
-describe(__filename, function () {
-
-
-
- const hookName = 'testHook';
- const hookFnName = 'testPluginFileName:testHookFunctionName';
- let testHooks; // Convenience shorthand for plugins.hooks[hookName].
- let hook: any; // Convenience shorthand for plugins.hooks[hookName][0].
-
- beforeEach(async function () {
- // Make sure these are not already set so that we don't accidentally step on someone else's
- // toes:
- assert(plugins.hooks[hookName] == null);
- assert(hooks.deprecationNotices[hookName] == null);
- assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null);
-
- // Many of the tests only need a single registered hook function. Set that up here to reduce
- // boilerplate.
- hook = makeHook();
- plugins.hooks[hookName] = [hook];
- testHooks = plugins.hooks[hookName];
- });
-
- afterEach(async function () {
- sinon.restore();
- delete plugins.hooks[hookName];
- delete hooks.deprecationNotices[hookName];
- delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName];
- });
-
- const makeHook = (ret?:any) => ({
- hook_name: hookName,
- // Many tests will likely want to change this. Unfortunately, we can't use a convenience
- // wrapper like `(...args) => hookFn(..args)` because the hooks look at Function.length and
- // change behavior depending on the number of parameters.
- hook_fn: (hn:Function, ctx:any, cb:Function) => cb(ret),
- hook_fn_name: hookFnName,
- part: {plugin: 'testPluginName'},
- });
-
- // Hook functions that should work for both synchronous and asynchronous hooks.
- const supportedSyncHookFunctions = [
- {
- name: 'return non-Promise value, with callback parameter',
- fn: (hn:Function, ctx:any, cb:Function) => 'val',
- want: 'val',
- syncOk: true,
- },
- {
- name: 'return non-Promise value, without callback parameter',
- fn: (hn:Function, ctx:any) => 'val',
- want: 'val',
- syncOk: true,
- },
- {
- name: 'return undefined, without callback parameter',
- fn: (hn:Function, ctx:any) => {},
- want: undefined,
- syncOk: true,
- },
- {
- name: 'pass non-Promise value to callback',
- fn: (hn:Function, ctx:any, cb:Function) => { cb('val'); },
- want: 'val',
- syncOk: true,
- },
- {
- name: 'pass undefined to callback',
- fn: (hn:Function, ctx:any, cb:Function) => { cb(); },
- want: undefined,
- syncOk: true,
- },
- {
- name: 'return the value returned from the callback',
- fn: (hn:Function, ctx:any, cb:Function) => cb('val'),
- want: 'val',
- syncOk: true,
- },
- {
- name: 'throw',
- fn: (hn:Function, ctx:any, cb:Function) => { throw new Error('test exception'); },
- wantErr: 'test exception',
- syncOk: true,
- },
- ];
-
- describe('callHookFnSync', function () {
- const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand.
-
- describe('basic behavior', function () {
- it('passes hook name', async function () {
- hook.hook_fn = (hn: string) => { assert.equal(hn, hookName); };
- callHookFnSync(hook);
- });
-
- it('passes context', async function () {
- for (const val of ['value', null, undefined]) {
- hook.hook_fn = (hn: string, ctx:string) => { assert.equal(ctx, val); };
- callHookFnSync(hook, val);
- }
- });
-
- it('returns the value provided to the callback', async function () {
- for (const val of ['value', null, undefined]) {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(ctx); };
- assert.equal(callHookFnSync(hook, val), val);
- }
- });
-
- it('returns the value returned by the hook function', async function () {
- for (const val of ['value', null, undefined]) {
- // Must not have the cb parameter otherwise returning undefined will error.
- hook.hook_fn = (hn: string, ctx: any) => ctx;
- assert.equal(callHookFnSync(hook, val), val);
- }
- });
-
- it('does not catch exceptions', async function () {
- hook.hook_fn = () => { throw new Error('test exception'); };
- assert.throws(() => callHookFnSync(hook), {message: 'test exception'});
- });
-
- it('callback returns undefined', async function () {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { assert.equal(cb('foo'), undefined); };
- callHookFnSync(hook);
- });
-
- it('checks for deprecation', async function () {
- sinon.stub(console, 'warn');
- hooks.deprecationNotices[hookName] = 'test deprecation';
- callHookFnSync(hook);
- assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true);
- // @ts-ignore
- assert.equal(console.warn.callCount, 1);
- // @ts-ignore
- assert.match(console.warn.getCall(0).args[0], /test deprecation/);
- });
- });
-
- describe('supported hook function styles', function () {
- for (const tc of supportedSyncHookFunctions) {
- it(tc.name, async function () {
- sinon.stub(console, 'warn');
- sinon.stub(console, 'error');
- hook.hook_fn = tc.fn;
- const call = () => callHookFnSync(hook);
- if (tc.wantErr) {
- assert.throws(call, {message: tc.wantErr});
- } else {
- assert.equal(call(), tc.want);
- }
- assert.equal(console.warn.callCount, 0);
- assert.equal(console.error.callCount, 0);
- });
- }
- });
-
- describe('bad hook function behavior (other than double settle)', function () {
- const promise1 = Promise.resolve('val1');
- const promise2 = Promise.resolve('val2');
-
- const testCases = [
- {
- name: 'never settles -> buggy hook detected',
- // Note that returning undefined without calling the callback is permitted if the function
- // has 2 or fewer parameters, so this test function must have 3 parameters.
- fn: (hn:Function, ctx:any, cb:Function) => {},
- wantVal: undefined,
- wantError: /UNSETTLED FUNCTION BUG/,
- },
- {
- name: 'returns a Promise -> buggy hook detected',
- fn: () => promise1,
- wantVal: promise1,
- wantError: /PROHIBITED PROMISE BUG/,
- },
- {
- name: 'passes a Promise to cb -> buggy hook detected',
- fn: (hn:Function, ctx:any, cb:Function) => cb(promise2),
- wantVal: promise2,
- wantError: /PROHIBITED PROMISE BUG/,
- },
- ];
-
- for (const tc of testCases) {
- it(tc.name, async function () {
- sinon.stub(console, 'error');
- hook.hook_fn = tc.fn;
- assert.equal(callHookFnSync(hook), tc.wantVal);
- assert.equal(console.error.callCount, tc.wantError ? 1 : 0);
- if (tc.wantError) assert.match(console.error.getCall(0).args[0], tc.wantError);
- });
- }
- });
-
- // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
- // time, or call the callback and then return a value.)
- describe('bad hook function behavior (double settle)', function () {
- beforeEach(async function () {
- sinon.stub(console, 'error');
- });
-
- // Each item in this array codifies a way to settle a synchronous hook function. Each of the
- // test cases below combines two of these behaviors in a single hook function and confirms
- // that callHookFnSync both (1) returns the result of the first settle attempt, and
- // (2) detects the second settle attempt.
- const behaviors = [
- {
- name: 'throw',
- fn: (cb: Function, err:any, val: string) => { throw err; },
- rejects: true,
- },
- {
- name: 'return value',
- fn: (cb: Function, err:any, val: string) => val,
- },
- {
- name: 'immediately call cb(value)',
- fn: (cb: Function, err:any, val: string) => cb(val),
- },
- {
- name: 'defer call to cb(value)',
- fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, val); },
- async: true,
- },
- ];
-
- for (const step1 of behaviors) {
- // There can't be a second step if the first step is to return or throw.
- if (step1.name.startsWith('return ') || step1.name === 'throw') continue;
- for (const step2 of behaviors) {
- // If step1 and step2 are both async then there would be three settle attempts (first an
- // erroneous unsettled return, then async step 1, then async step 2). Handling triple
- // settle would complicate the tests, and it is sufficient to test only double settles.
- if (step1.async && step2.async) continue;
-
- it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => {
- step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
- return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
- };
-
- // Temporarily remove unhandled error listeners so that the errors we expect to see
- // don't trigger a test failure (or terminate node).
- const events = ['uncaughtException', 'unhandledRejection'];
- const listenerBackups:MapArrayType = {};
- for (const event of events) {
- listenerBackups[event] = process.rawListeners(event);
- process.removeAllListeners(event);
- }
-
- // We should see an asynchronous error (either an unhandled Promise rejection or an
- // uncaught exception) if and only if one of the two steps was asynchronous or there was
- // a throw (in which case the double settle is deferred so that the caller sees the
- // original error).
- const wantAsyncErr = step1.async || step2.async || step2.rejects;
- let tempListener:Function;
- let asyncErr:Error|undefined;
- try {
- const seenErrPromise = new Promise((resolve) => {
- tempListener = (err:any) => {
- assert.equal(asyncErr, undefined);
- asyncErr = err;
- resolve();
- };
- if (!wantAsyncErr) resolve();
- });
- // @ts-ignore
- events.forEach((event) => process.on(event, tempListener));
- const call = () => callHookFnSync(hook, {ret1: 'val1', ret2: 'val2'});
- if (step2.rejects) {
- assert.throws(call, {message: 'val2'});
- } else if (!step1.async && !step2.async) {
- assert.throws(call, {message: /DOUBLE SETTLE BUG/});
- } else {
- assert.equal(call(), step1.async ? 'val2' : 'val1');
- }
- await seenErrPromise;
- } finally {
- // Restore the original listeners.
- for (const event of events) {
- // @ts-ignore
- process.off(event, tempListener);
- for (const listener of listenerBackups[event]) {
- process.on(event, listener);
- }
- }
- }
- assert.equal(console.error.callCount, 1);
- assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
- if (wantAsyncErr) {
- assert(asyncErr instanceof Error);
- assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
- }
- });
-
- // This next test is the same as the above test, except the second settle attempt is for
- // the same outcome. The two outcomes can't be the same if one step throws and the other
- // doesn't, so skip those cases.
- if (step1.rejects !== step2.rejects) continue;
-
- it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () {
- const err = new Error('val');
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => {
- step1.fn(cb, err, 'val');
- return step2.fn(cb, err, 'val');
- };
-
- const errorLogged = new Promise((resolve) => console.error.callsFake(resolve));
- const call = () => callHookFnSync(hook);
- if (step2.rejects) {
- assert.throws(call, {message: 'val'});
- } else {
- assert.equal(call(), 'val');
- }
- await errorLogged;
- assert.equal(console.error.callCount, 1);
- assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
- });
- }
- }
- });
- });
-
- describe('hooks.callAll', function () {
- describe('basic behavior', function () {
- it('calls all in order', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(1), makeHook(2), makeHook(3));
- assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]);
- });
-
- it('passes hook name', async function () {
- hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); };
- hooks.callAll(hookName);
- });
-
- it('undefined context -> {}', async function () {
- hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- hooks.callAll(hookName);
- });
-
- it('null context -> {}', async function () {
- hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- hooks.callAll(hookName, null);
- });
-
- it('context unmodified', async function () {
- const wantContext = {};
- hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); };
- hooks.callAll(hookName, wantContext);
- });
- });
-
- describe('result processing', function () {
- it('no registered hooks (undefined) -> []', async function () {
- delete plugins.hooks.testHook;
- assert.deepEqual(hooks.callAll(hookName), []);
- });
-
- it('no registered hooks (empty list) -> []', async function () {
- testHooks.length = 0;
- assert.deepEqual(hooks.callAll(hookName), []);
- });
-
- it('flattens one level', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
- assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]);
- });
-
- it('filters out undefined', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]));
- assert.deepEqual(hooks.callAll(hookName), [2, [3]]);
- });
-
- it('preserves null', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]]));
- assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]);
- });
-
- it('all undefined -> []', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook());
- assert.deepEqual(hooks.callAll(hookName), []);
- });
- });
- });
-
- describe('hooks.callFirst', function () {
- it('no registered hooks (undefined) -> []', async function () {
- delete plugins.hooks.testHook;
- assert.deepEqual(hooks.callFirst(hookName), []);
- });
-
- it('no registered hooks (empty list) -> []', async function () {
- testHooks.length = 0;
- assert.deepEqual(hooks.callFirst(hookName), []);
- });
-
- it('passes hook name => {}', async function () {
- hook.hook_fn = (hn: string) => { assert.equal(hn, hookName); };
- hooks.callFirst(hookName);
- });
-
- it('undefined context => {}', async function () {
- hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- hooks.callFirst(hookName);
- });
-
- it('null context => {}', async function () {
- hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- hooks.callFirst(hookName, null);
- });
-
- it('context unmodified', async function () {
- const wantContext = {};
- hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); };
- hooks.callFirst(hookName, wantContext);
- });
-
- it('predicate never satisfied -> calls all in order', async function () {
- const gotCalls:MapArrayType = [];
- testHooks.length = 0;
- for (let i = 0; i < 3; i++) {
- const hook = makeHook();
- hook.hook_fn = () => { gotCalls.push(i); };
- testHooks.push(hook);
- }
- assert.deepEqual(hooks.callFirst(hookName), []);
- assert.deepEqual(gotCalls, [0, 1, 2]);
- });
-
- it('stops when predicate is satisfied', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
- assert.deepEqual(hooks.callFirst(hookName), ['val1']);
- });
-
- it('skips values that do not satisfy predicate (undefined)', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook('val1'));
- assert.deepEqual(hooks.callFirst(hookName), ['val1']);
- });
-
- it('skips values that do not satisfy predicate (empty list)', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook([]), makeHook('val1'));
- assert.deepEqual(hooks.callFirst(hookName), ['val1']);
- });
-
- it('null satisifes the predicate', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(null), makeHook('val1'));
- assert.deepEqual(hooks.callFirst(hookName), [null]);
- });
-
- it('non-empty arrays are returned unmodified', async function () {
- const want = ['val1'];
- testHooks.length = 0;
- testHooks.push(makeHook(want), makeHook(['val2']));
- assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual!
- });
-
- it('value can be passed via callback', async function () {
- const want = {};
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(want); };
- const got = hooks.callFirst(hookName);
- assert.deepEqual(got, [want]);
- assert.equal(got[0], want); // Note: *NOT* deepEqual!
- });
- });
-
- describe('callHookFnAsync', function () {
- const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand.
-
- describe('basic behavior', function () {
- it('passes hook name', async function () {
- hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); };
- await callHookFnAsync(hook);
- });
-
- it('passes context', async function () {
- for (const val of ['value', null, undefined]) {
- hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, val); };
- await callHookFnAsync(hook, val);
- }
- });
-
- it('returns the value provided to the callback', async function () {
- for (const val of ['value', null, undefined]) {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(ctx); };
- assert.equal(await callHookFnAsync(hook, val), val);
- assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
- }
- });
-
- it('returns the value returned by the hook function', async function () {
- for (const val of ['value', null, undefined]) {
- // Must not have the cb parameter otherwise returning undefined will never resolve.
- hook.hook_fn = (hn: string, ctx: any) => ctx;
- assert.equal(await callHookFnAsync(hook, val), val);
- assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
- }
- });
-
- it('rejects if it throws an exception', async function () {
- hook.hook_fn = () => { throw new Error('test exception'); };
- await assert.rejects(callHookFnAsync(hook), {message: 'test exception'});
- });
-
- it('rejects if rejected Promise passed to callback', async function () {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => cb(Promise.reject(new Error('test exception')));
- await assert.rejects(callHookFnAsync(hook), {message: 'test exception'});
- });
-
- it('rejects if rejected Promise returned', async function () {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => Promise.reject(new Error('test exception'));
- await assert.rejects(callHookFnAsync(hook), {message: 'test exception'});
- });
-
- it('callback returns undefined', async function () {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { assert.equal(cb('foo'), undefined); };
- await callHookFnAsync(hook);
- });
-
- it('checks for deprecation', async function () {
- sinon.stub(console, 'warn');
- hooks.deprecationNotices[hookName] = 'test deprecation';
- await callHookFnAsync(hook);
- assert.equal(hooks.exportedForTestingOnly.deprecationWarned[hookFnName], true);
- assert.equal(console.warn.callCount, 1);
- assert.match(console.warn.getCall(0).args[0], /test deprecation/);
- });
- });
-
- describe('supported hook function styles', function () {
- // @ts-ignore
- const supportedHookFunctions = supportedSyncHookFunctions.concat([
- {
- name: 'legacy async cb',
- fn: (hn:Function, ctx:any, cb:Function) => { process.nextTick(cb, 'val'); },
- want: 'val',
- },
- // Already resolved Promises:
- {
- name: 'return resolved Promise, with callback parameter',
- fn: (hn:Function, ctx:any, cb:Function) => Promise.resolve('val'),
- want: 'val',
- },
- {
- name: 'return resolved Promise, without callback parameter',
- fn: (hn: string, ctx: any) => Promise.resolve('val'),
- want: 'val',
- },
- {
- name: 'pass resolved Promise to callback',
- fn: (hn:Function, ctx:any, cb:Function) => { cb(Promise.resolve('val')); },
- want: 'val',
- },
- // Not yet resolved Promises:
- {
- name: 'return unresolved Promise, with callback parameter',
- fn: (hn:Function, ctx:any, cb:Function) => new Promise((resolve) => process.nextTick(resolve, 'val')),
- want: 'val',
- },
- {
- name: 'return unresolved Promise, without callback parameter',
- fn: (hn: string, ctx: any) => new Promise((resolve) => process.nextTick(resolve, 'val')),
- want: 'val',
- },
- {
- name: 'pass unresolved Promise to callback',
- fn: (hn:Function, ctx:any, cb:Function) => { cb(new Promise((resolve) => process.nextTick(resolve, 'val'))); },
- want: 'val',
- },
- // Already rejected Promises:
- {
- name: 'return rejected Promise, with callback parameter',
- fn: (hn:Function, ctx:any, cb:Function) => Promise.reject(new Error('test rejection')),
- wantErr: 'test rejection',
- },
- {
- name: 'return rejected Promise, without callback parameter',
- fn: (hn: string, ctx: any) => Promise.reject(new Error('test rejection')),
- wantErr: 'test rejection',
- },
- {
- name: 'pass rejected Promise to callback',
- fn: (hn:Function, ctx:any, cb:Function) => { cb(Promise.reject(new Error('test rejection'))); },
- wantErr: 'test rejection',
- },
- // Not yet rejected Promises:
- {
- name: 'return unrejected Promise, with callback parameter',
- fn: (hn:Function, ctx:any, cb:Function) => new Promise((resolve, reject) => {
- process.nextTick(reject, new Error('test rejection'));
- }),
- wantErr: 'test rejection',
- },
- {
- name: 'return unrejected Promise, without callback parameter',
- fn: (hn: string, ctx: any) => new Promise((resolve, reject) => {
- process.nextTick(reject, new Error('test rejection'));
- }),
- wantErr: 'test rejection',
- },
- {
- name: 'pass unrejected Promise to callback',
- fn: (hn:Function, ctx:any, cb:Function) => {
- cb(new Promise((resolve, reject) => {
- process.nextTick(reject, new Error('test rejection'));
- }));
- },
- wantErr: 'test rejection',
- },
- ]);
-
- for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) {
- it(tc.name, async function () {
- sinon.stub(console, 'warn');
- sinon.stub(console, 'error');
- hook.hook_fn = tc.fn;
- const p = callHookFnAsync(hook);
- if (tc.wantErr) {
- await assert.rejects(p, {message: tc.wantErr});
- } else {
- assert.equal(await p, tc.want);
- }
- assert.equal(console.warn.callCount, 0);
- assert.equal(console.error.callCount, 0);
- });
- }
- });
-
- // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
- // time, or call the callback and then return a value.)
- describe('bad hook function behavior (double settle)', function () {
- beforeEach(async function () {
- sinon.stub(console, 'error');
- });
-
- // Each item in this array codifies a way to settle an asynchronous hook function. Each of the
- // test cases below combines two of these behaviors in a single hook function and confirms
- // that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2)
- // detects the second settle attempt.
- //
- // The 'when' property specifies the relative time that two behaviors will cause the hook
- // function to settle:
- // * If behavior1.when <= behavior2.when and behavior1 is called before behavior2 then
- // behavior1 will settle the hook function before behavior2.
- // * Otherwise, behavior2 will settle the hook function before behavior1.
- const behaviors = [
- {
- name: 'throw',
- fn: (cb: Function, err:any, val: string) => { throw err; },
- rejects: true,
- when: 0,
- },
- {
- name: 'return value',
- fn: (cb: Function, err:any, val: string) => val,
- // This behavior has a later relative settle time vs. the 'throw' behavior because 'throw'
- // immediately settles the hook function, whereas the 'return value' case is settled by a
- // .then() function attached to a Promise. EcmaScript guarantees that a .then() function
- // attached to a Promise is enqueued on the event loop (not executed immediately) when the
- // Promise settles.
- when: 1,
- },
- {
- name: 'immediately call cb(value)',
- fn: (cb: Function, err:any, val: string) => cb(val),
- // This behavior has the same relative time as the 'return value' case because it too is
- // settled by a .then() function attached to a Promise.
- when: 1,
- },
- {
- name: 'return resolvedPromise',
- fn: (cb: Function, err:any, val: string) => Promise.resolve(val),
- // This behavior has the same relative time as the 'return value' case because the return
- // value is wrapped in a Promise via Promise.resolve(). The EcmaScript standard guarantees
- // that Promise.resolve(Promise.resolve(value)) is equivalent to Promise.resolve(value),
- // so returning an already resolved Promise vs. returning a non-Promise value are
- // equivalent.
- when: 1,
- },
- {
- name: 'immediately call cb(resolvedPromise)',
- fn: (cb: Function, err:any, val: string) => cb(Promise.resolve(val)),
- when: 1,
- },
- {
- name: 'return rejectedPromise',
- fn: (cb: Function, err:any, val: string) => Promise.reject(err),
- rejects: true,
- when: 1,
- },
- {
- name: 'immediately call cb(rejectedPromise)',
- fn: (cb: Function, err:any, val: string) => cb(Promise.reject(err)),
- rejects: true,
- when: 1,
- },
- {
- name: 'return unresolvedPromise',
- fn: (cb: Function, err:any, val: string) => new Promise((resolve) => process.nextTick(resolve, val)),
- when: 2,
- },
- {
- name: 'immediately call cb(unresolvedPromise)',
- fn: (cb: Function, err:any, val: string) => cb(new Promise((resolve) => process.nextTick(resolve, val))),
- when: 2,
- },
- {
- name: 'return unrejectedPromise',
- fn: (cb: Function, err:any, val: string) => new Promise((resolve, reject) => process.nextTick(reject, err)),
- rejects: true,
- when: 2,
- },
- {
- name: 'immediately call cb(unrejectedPromise)',
- fn: (cb: Function, err:any, val: string) => cb(new Promise((resolve, reject) => process.nextTick(reject, err))),
- rejects: true,
- when: 2,
- },
- {
- name: 'defer call to cb(value)',
- fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, val); },
- when: 2,
- },
- {
- name: 'defer call to cb(resolvedPromise)',
- fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, Promise.resolve(val)); },
- when: 2,
- },
- {
- name: 'defer call to cb(rejectedPromise)',
- fn: (cb: Function, err:any, val: string) => { process.nextTick(cb, Promise.reject(err)); },
- rejects: true,
- when: 2,
- },
- {
- name: 'defer call to cb(unresolvedPromise)',
- fn: (cb: Function, err:any, val: string) => {
- process.nextTick(() => {
- cb(new Promise((resolve) => process.nextTick(resolve, val)));
- });
- },
- when: 3,
- },
- {
- name: 'defer call cb(unrejectedPromise)',
- fn: (cb: Function, err:any, val: string) => {
- process.nextTick(() => {
- cb(new Promise((resolve, reject) => process.nextTick(reject, err)));
- });
- },
- rejects: true,
- when: 3,
- },
- ];
-
- for (const step1 of behaviors) {
- // There can't be a second step if the first step is to return or throw.
- if (step1.name.startsWith('return ') || step1.name === 'throw') continue;
- for (const step2 of behaviors) {
- it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () {
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => {
- step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
- return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
- };
-
- // Temporarily remove unhandled Promise rejection listeners so that the unhandled
- // rejections we expect to see don't trigger a test failure (or terminate node).
- const event = 'unhandledRejection';
- const listenersBackup = process.rawListeners(event);
- process.removeAllListeners(event);
-
- let tempListener;
- let asyncErr: Error;
- try {
- const seenErrPromise = new Promise((resolve) => {
- tempListener = (err:any) => {
- assert.equal(asyncErr, undefined);
- asyncErr = err;
- resolve();
- };
- });
- process.on(event, tempListener!);
- const step1Wins = step1.when <= step2.when;
- const winningStep = step1Wins ? step1 : step2;
- const winningVal = step1Wins ? 'val1' : 'val2';
- const p = callHookFnAsync(hook, {ret1: 'val1', ret2: 'val2'});
- if (winningStep.rejects) {
- await assert.rejects(p, {message: winningVal});
- } else {
- assert.equal(await p, winningVal);
- }
- await seenErrPromise;
- } finally {
- // Restore the original listeners.
- process.off(event, tempListener!);
- for (const listener of listenersBackup) {
- process.on(event, listener as any);
- }
- }
- assert.equal(console.error.callCount, 1,
- `Got errors:\n${
- console.error.getCalls().map((call) => call.args[0]).join('\n')}`);
- assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
- // @ts-ignore
- assert(asyncErr instanceof Error);
- assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
- });
-
- // This next test is the same as the above test, except the second settle attempt is for
- // the same outcome. The two outcomes can't be the same if one step rejects and the other
- // doesn't, so skip those cases.
- if (step1.rejects !== step2.rejects) continue;
-
- it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () {
- const err = new Error('val');
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => {
- step1.fn(cb, err, 'val');
- return step2.fn(cb, err, 'val');
- };
- const winningStep = (step1.when <= step2.when) ? step1 : step2;
- const errorLogged = new Promise((resolve) => console.error.callsFake(resolve));
- const p = callHookFnAsync(hook);
- if (winningStep.rejects) {
- await assert.rejects(p, {message: 'val'});
- } else {
- assert.equal(await p, 'val');
- }
- await errorLogged;
- assert.equal(console.error.callCount, 1);
- assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
- });
- }
- }
- });
- });
-
- describe('hooks.aCallAll', function () {
- describe('basic behavior', function () {
- it('calls all asynchronously, returns values in order', async function () {
- testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it.
- let nextIndex = 0;
- const hookPromises: {
- promise?: Promise,
- resolve?: Function,
- } []
- = [];
- const hookStarted: boolean[] = [];
- const hookFinished :boolean[]= [];
- const makeHook = () => {
- const i = nextIndex++;
- const entry:{
- promise?: Promise,
- resolve?: Function,
- } = {};
- hookStarted[i] = false;
- hookFinished[i] = false;
- hookPromises[i] = entry;
- entry.promise = new Promise((resolve) => {
- entry.resolve = () => {
- hookFinished[i] = true;
- resolve(i);
- };
- });
- return {hook_fn: () => {
- hookStarted[i] = true;
- return entry.promise;
- }};
- };
- testHooks.push(makeHook(), makeHook());
- const p = hooks.aCallAll(hookName);
- assert.deepEqual(hookStarted, [true, true]);
- assert.deepEqual(hookFinished, [false, false]);
- hookPromises[1].resolve!();
- await hookPromises[1].promise;
- assert.deepEqual(hookFinished, [false, true]);
- hookPromises[0].resolve!();
- assert.deepEqual(await p, [0, 1]);
- });
-
- it('passes hook name', async function () {
- hook.hook_fn = async (hn:string) => { assert.equal(hn, hookName); };
- await hooks.aCallAll(hookName);
- });
-
- it('undefined context -> {}', async function () {
- hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- await hooks.aCallAll(hookName);
- });
-
- it('null context -> {}', async function () {
- hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- await hooks.aCallAll(hookName, null);
- });
-
- it('context unmodified', async function () {
- const wantContext = {};
- hook.hook_fn = async (hn: string, ctx: any) => { assert.equal(ctx, wantContext); };
- await hooks.aCallAll(hookName, wantContext);
- });
- });
-
- describe('aCallAll callback', function () {
- it('exception in callback rejects', async function () {
- const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); });
- await assert.rejects(p, {message: 'test exception'});
- });
-
- it('propagates error on exception', async function () {
- hook.hook_fn = () => { throw new Error('test exception'); };
- await hooks.aCallAll(hookName, {}, (err:any) => {
- assert(err instanceof Error);
- assert.equal(err.message, 'test exception');
- });
- });
-
- it('propagages null error on success', async function () {
- await hooks.aCallAll(hookName, {}, (err:any) => {
- assert(err == null, `got non-null error: ${err}`);
- });
- });
-
- it('propagages results on success', async function () {
- hook.hook_fn = () => 'val';
- await hooks.aCallAll(hookName, {}, (err:any, results:any) => {
- assert.deepEqual(results, ['val']);
- });
- });
-
- it('returns callback return value', async function () {
- assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val');
- });
- });
-
- describe('result processing', function () {
- it('no registered hooks (undefined) -> []', async function () {
- delete plugins.hooks[hookName];
- assert.deepEqual(await hooks.aCallAll(hookName), []);
- });
-
- it('no registered hooks (empty list) -> []', async function () {
- testHooks.length = 0;
- assert.deepEqual(await hooks.aCallAll(hookName), []);
- });
-
- it('flattens one level', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
- assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]);
- });
-
- it('filters out undefined', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
- assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]);
- });
-
- it('preserves null', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
- assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]);
- });
-
- it('all undefined -> []', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook(Promise.resolve()));
- assert.deepEqual(await hooks.aCallAll(hookName), []);
- });
- });
- });
-
- describe('hooks.callAllSerial', function () {
- describe('basic behavior', function () {
- it('calls all asynchronously, serially, in order', async function () {
- const gotCalls:number[] = [];
- testHooks.length = 0;
- for (let i = 0; i < 3; i++) {
- const hook = makeHook();
- hook.hook_fn = async () => {
- gotCalls.push(i);
- // Check gotCalls asynchronously to ensure that the next hook function does not start
- // executing before this hook function has resolved.
- return await new Promise((resolve) => {
- setImmediate(() => {
- assert.deepEqual(gotCalls, [...Array(i + 1).keys()]);
- resolve(i);
- });
- });
- };
- testHooks.push(hook);
- }
- assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]);
- assert.deepEqual(gotCalls, [0, 1, 2]);
- });
-
- it('passes hook name', async function () {
- hook.hook_fn = async (hn:string) => { assert.equal(hn, hookName); };
- await hooks.callAllSerial(hookName);
- });
-
- it('undefined context -> {}', async function () {
- hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- await hooks.callAllSerial(hookName);
- });
-
- it('null context -> {}', async function () {
- hook.hook_fn = async (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- await hooks.callAllSerial(hookName, null);
- });
-
- it('context unmodified', async function () {
- const wantContext = {};
- hook.hook_fn = async (hn: string, ctx: any) => { assert.equal(ctx, wantContext); };
- await hooks.callAllSerial(hookName, wantContext);
- });
- });
-
- describe('result processing', function () {
- it('no registered hooks (undefined) -> []', async function () {
- delete plugins.hooks[hookName];
- assert.deepEqual(await hooks.callAllSerial(hookName), []);
- });
-
- it('no registered hooks (empty list) -> []', async function () {
- testHooks.length = 0;
- assert.deepEqual(await hooks.callAllSerial(hookName), []);
- });
-
- it('flattens one level', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
- assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]);
- });
-
- it('filters out undefined', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
- assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]);
- });
-
- it('preserves null', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
- assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]);
- });
-
- it('all undefined -> []', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook(Promise.resolve()));
- assert.deepEqual(await hooks.callAllSerial(hookName), []);
- });
- });
- });
-
- describe('hooks.aCallFirst', function () {
- it('no registered hooks (undefined) -> []', async function () {
- delete plugins.hooks.testHook;
- assert.deepEqual(await hooks.aCallFirst(hookName), []);
- });
-
- it('no registered hooks (empty list) -> []', async function () {
- testHooks.length = 0;
- assert.deepEqual(await hooks.aCallFirst(hookName), []);
- });
-
- it('passes hook name => {}', async function () {
- hook.hook_fn = (hn:string) => { assert.equal(hn, hookName); };
- await hooks.aCallFirst(hookName);
- });
-
- it('undefined context => {}', async function () {
- hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- await hooks.aCallFirst(hookName);
- });
-
- it('null context => {}', async function () {
- hook.hook_fn = (hn: string, ctx: any) => { assert.deepEqual(ctx, {}); };
- await hooks.aCallFirst(hookName, null);
- });
-
- it('context unmodified', async function () {
- const wantContext = {};
- hook.hook_fn = (hn: string, ctx: any) => { assert.equal(ctx, wantContext); };
- await hooks.aCallFirst(hookName, wantContext);
- });
-
- it('default predicate: predicate never satisfied -> calls all in order', async function () {
- const gotCalls:number[] = [];
- testHooks.length = 0;
- for (let i = 0; i < 3; i++) {
- const hook = makeHook();
- hook.hook_fn = () => { gotCalls.push(i); };
- testHooks.push(hook);
- }
- assert.deepEqual(await hooks.aCallFirst(hookName), []);
- assert.deepEqual(gotCalls, [0, 1, 2]);
- });
-
- it('calls hook functions serially', async function () {
- const gotCalls: number[] = [];
- testHooks.length = 0;
- for (let i = 0; i < 3; i++) {
- const hook = makeHook();
- hook.hook_fn = async () => {
- gotCalls.push(i);
- // Check gotCalls asynchronously to ensure that the next hook function does not start
- // executing before this hook function has resolved.
- return await new Promise((resolve) => {
- setImmediate(() => {
- assert.deepEqual(gotCalls, [...Array(i + 1).keys()]);
- resolve();
- });
- });
- };
- testHooks.push(hook);
- }
- assert.deepEqual(await hooks.aCallFirst(hookName), []);
- assert.deepEqual(gotCalls, [0, 1, 2]);
- });
-
- it('default predicate: stops when satisfied', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
- assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
- });
-
- it('default predicate: skips values that do not satisfy (undefined)', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(), makeHook('val1'));
- assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
- });
-
- it('default predicate: skips values that do not satisfy (empty list)', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook([]), makeHook('val1'));
- assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
- });
-
- it('default predicate: null satisifes', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(null), makeHook('val1'));
- assert.deepEqual(await hooks.aCallFirst(hookName), [null]);
- });
-
- it('custom predicate: called for each hook function', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(0), makeHook(1), makeHook(2));
- let got = 0;
- await hooks.aCallFirst(hookName, null, null, (val:string) => { ++got; return false; });
- assert.equal(got, 3);
- });
-
- it('custom predicate: boolean false/true continues/stops iteration', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(1), makeHook(2), makeHook(3));
- let nCall = 0;
- const predicate = (val: number[]) => {
- assert.deepEqual(val, [++nCall]);
- return nCall === 2;
- };
- assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
- assert.equal(nCall, 2);
- });
-
- it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () {
- testHooks.length = 0;
- testHooks.push(makeHook(1), makeHook(2), makeHook(3));
- let nCall = 0;
- const predicate = (val: number[]) => {
- assert.deepEqual(val, [++nCall]);
- return nCall === 2 ? {} : null;
- };
- assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
- assert.equal(nCall, 2);
- });
-
- it('custom predicate: array value passed unmodified to predicate', async function () {
- const want = [0];
- hook.hook_fn = () => want;
- const predicate = (got: []) => { assert.equal(got, want); }; // Note: *NOT* deepEqual!
- await hooks.aCallFirst(hookName, null, null, predicate);
- });
-
- it('custom predicate: normalized value passed to predicate (undefined)', async function () {
- const predicate = (got: []) => { assert.deepEqual(got, []); };
- await hooks.aCallFirst(hookName, null, null, predicate);
- });
-
- it('custom predicate: normalized value passed to predicate (null)', async function () {
- hook.hook_fn = () => null;
- const predicate = (got: []) => { assert.deepEqual(got, [null]); };
- await hooks.aCallFirst(hookName, null, null, predicate);
- });
-
- it('non-empty arrays are returned unmodified', async function () {
- const want = ['val1'];
- testHooks.length = 0;
- testHooks.push(makeHook(want), makeHook(['val2']));
- assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual!
- });
-
- it('value can be passed via callback', async function () {
- const want = {};
- hook.hook_fn = (hn:Function, ctx:any, cb:Function) => { cb(want); };
- const got = await hooks.aCallFirst(hookName);
- assert.deepEqual(got, [want]);
- assert.equal(got[0], want); // Note: *NOT* deepEqual!
- });
- });
+describe(__filename, () => {
+ const hookName = "testHook";
+ const hookFnName = "testPluginFileName:testHookFunctionName";
+ let testHooks; // Convenience shorthand for plugins.hooks[hookName].
+ let hook: any; // Convenience shorthand for plugins.hooks[hookName][0].
+
+ beforeEach(async () => {
+ // Make sure these are not already set so that we don't accidentally step on someone else's
+ // toes:
+ assert(plugins.hooks[hookName] == null);
+ assert(hooks.deprecationNotices[hookName] == null);
+ assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null);
+
+ // Many of the tests only need a single registered hook function. Set that up here to reduce
+ // boilerplate.
+ hook = makeHook();
+ plugins.hooks[hookName] = [hook];
+ testHooks = plugins.hooks[hookName];
+ });
+
+ afterEach(async () => {
+ sinon.restore();
+ delete plugins.hooks[hookName];
+ delete hooks.deprecationNotices[hookName];
+ delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName];
+ });
+
+ const makeHook = (ret?: any) => ({
+ hook_name: hookName,
+ // Many tests will likely want to change this. Unfortunately, we can't use a convenience
+ // wrapper like `(...args) => hookFn(..args)` because the hooks look at Function.length and
+ // change behavior depending on the number of parameters.
+ hook_fn: (hn: Function, ctx: any, cb: Function) => cb(ret),
+ hook_fn_name: hookFnName,
+ part: { plugin: "testPluginName" },
+ });
+
+ // Hook functions that should work for both synchronous and asynchronous hooks.
+ const supportedSyncHookFunctions = [
+ {
+ name: "return non-Promise value, with callback parameter",
+ fn: (hn: Function, ctx: any, cb: Function) => "val",
+ want: "val",
+ syncOk: true,
+ },
+ {
+ name: "return non-Promise value, without callback parameter",
+ fn: (hn: Function, ctx: any) => "val",
+ want: "val",
+ syncOk: true,
+ },
+ {
+ name: "return undefined, without callback parameter",
+ fn: (hn: Function, ctx: any) => {},
+ want: undefined,
+ syncOk: true,
+ },
+ {
+ name: "pass non-Promise value to callback",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ cb("val");
+ },
+ want: "val",
+ syncOk: true,
+ },
+ {
+ name: "pass undefined to callback",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ cb();
+ },
+ want: undefined,
+ syncOk: true,
+ },
+ {
+ name: "return the value returned from the callback",
+ fn: (hn: Function, ctx: any, cb: Function) => cb("val"),
+ want: "val",
+ syncOk: true,
+ },
+ {
+ name: "throw",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ throw new Error("test exception");
+ },
+ wantErr: "test exception",
+ syncOk: true,
+ },
+ ];
+
+ describe("callHookFnSync", () => {
+ const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand.
+
+ describe("basic behavior", () => {
+ it("passes hook name", async () => {
+ hook.hook_fn = (hn: string) => {
+ assert.equal(hn, hookName);
+ };
+ callHookFnSync(hook);
+ });
+
+ it("passes context", async () => {
+ for (const val of ["value", null, undefined]) {
+ hook.hook_fn = (hn: string, ctx: string) => {
+ assert.equal(ctx, val);
+ };
+ callHookFnSync(hook, val);
+ }
+ });
+
+ it("returns the value provided to the callback", async () => {
+ for (const val of ["value", null, undefined]) {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ cb(ctx);
+ };
+ assert.equal(callHookFnSync(hook, val), val);
+ }
+ });
+
+ it("returns the value returned by the hook function", async () => {
+ for (const val of ["value", null, undefined]) {
+ // Must not have the cb parameter otherwise returning undefined will error.
+ hook.hook_fn = (hn: string, ctx: any) => ctx;
+ assert.equal(callHookFnSync(hook, val), val);
+ }
+ });
+
+ it("does not catch exceptions", async () => {
+ hook.hook_fn = () => {
+ throw new Error("test exception");
+ };
+ assert.throws(() => callHookFnSync(hook), {
+ message: "test exception",
+ });
+ });
+
+ it("callback returns undefined", async () => {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ assert.equal(cb("foo"), undefined);
+ };
+ callHookFnSync(hook);
+ });
+
+ it("checks for deprecation", async () => {
+ sinon.stub(console, "warn");
+ hooks.deprecationNotices[hookName] = "test deprecation";
+ callHookFnSync(hook);
+ assert.equal(
+ hooks.exportedForTestingOnly.deprecationWarned[hookFnName],
+ true,
+ );
+ // @ts-ignore
+ assert.equal(console.warn.callCount, 1);
+ // @ts-ignore
+ assert.match(console.warn.getCall(0).args[0], /test deprecation/);
+ });
+ });
+
+ describe("supported hook function styles", () => {
+ for (const tc of supportedSyncHookFunctions) {
+ it(tc.name, async () => {
+ sinon.stub(console, "warn");
+ sinon.stub(console, "error");
+ hook.hook_fn = tc.fn;
+ const call = () => callHookFnSync(hook);
+ if (tc.wantErr) {
+ assert.throws(call, { message: tc.wantErr });
+ } else {
+ assert.equal(call(), tc.want);
+ }
+ assert.equal(console.warn.callCount, 0);
+ assert.equal(console.error.callCount, 0);
+ });
+ }
+ });
+
+ describe("bad hook function behavior (other than double settle)", () => {
+ const promise1 = Promise.resolve("val1");
+ const promise2 = Promise.resolve("val2");
+
+ const testCases = [
+ {
+ name: "never settles -> buggy hook detected",
+ // Note that returning undefined without calling the callback is permitted if the function
+ // has 2 or fewer parameters, so this test function must have 3 parameters.
+ fn: (hn: Function, ctx: any, cb: Function) => {},
+ wantVal: undefined,
+ wantError: /UNSETTLED FUNCTION BUG/,
+ },
+ {
+ name: "returns a Promise -> buggy hook detected",
+ fn: () => promise1,
+ wantVal: promise1,
+ wantError: /PROHIBITED PROMISE BUG/,
+ },
+ {
+ name: "passes a Promise to cb -> buggy hook detected",
+ fn: (hn: Function, ctx: any, cb: Function) => cb(promise2),
+ wantVal: promise2,
+ wantError: /PROHIBITED PROMISE BUG/,
+ },
+ ];
+
+ for (const tc of testCases) {
+ it(tc.name, async () => {
+ sinon.stub(console, "error");
+ hook.hook_fn = tc.fn;
+ assert.equal(callHookFnSync(hook), tc.wantVal);
+ assert.equal(console.error.callCount, tc.wantError ? 1 : 0);
+ if (tc.wantError)
+ assert.match(console.error.getCall(0).args[0], tc.wantError);
+ });
+ }
+ });
+
+ // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
+ // time, or call the callback and then return a value.)
+ describe("bad hook function behavior (double settle)", () => {
+ beforeEach(async () => {
+ sinon.stub(console, "error");
+ });
+
+ // Each item in this array codifies a way to settle a synchronous hook function. Each of the
+ // test cases below combines two of these behaviors in a single hook function and confirms
+ // that callHookFnSync both (1) returns the result of the first settle attempt, and
+ // (2) detects the second settle attempt.
+ const behaviors = [
+ {
+ name: "throw",
+ fn: (cb: Function, err: any, val: string) => {
+ throw err;
+ },
+ rejects: true,
+ },
+ {
+ name: "return value",
+ fn: (cb: Function, err: any, val: string) => val,
+ },
+ {
+ name: "immediately call cb(value)",
+ fn: (cb: Function, err: any, val: string) => cb(val),
+ },
+ {
+ name: "defer call to cb(value)",
+ fn: (cb: Function, err: any, val: string) => {
+ process.nextTick(cb, val);
+ },
+ async: true,
+ },
+ ];
+
+ for (const step1 of behaviors) {
+ // There can't be a second step if the first step is to return or throw.
+ if (step1.name.startsWith("return ") || step1.name === "throw")
+ continue;
+ for (const step2 of behaviors) {
+ // If step1 and step2 are both async then there would be three settle attempts (first an
+ // erroneous unsettled return, then async step 1, then async step 2). Handling triple
+ // settle would complicate the tests, and it is sufficient to test only double settles.
+ if (step1.async && step2.async) continue;
+
+ it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async () => {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
+ return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
+ };
+
+ // Temporarily remove unhandled error listeners so that the errors we expect to see
+ // don't trigger a test failure (or terminate node).
+ const events = ["uncaughtException", "unhandledRejection"];
+ const listenerBackups: MapArrayType = {};
+ for (const event of events) {
+ listenerBackups[event] = process.rawListeners(event);
+ process.removeAllListeners(event);
+ }
+
+ // We should see an asynchronous error (either an unhandled Promise rejection or an
+ // uncaught exception) if and only if one of the two steps was asynchronous or there was
+ // a throw (in which case the double settle is deferred so that the caller sees the
+ // original error).
+ const wantAsyncErr = step1.async || step2.async || step2.rejects;
+ let tempListener: Function;
+ let asyncErr: Error | undefined;
+ try {
+ const seenErrPromise = new Promise((resolve) => {
+ tempListener = (err: any) => {
+ assert.equal(asyncErr, undefined);
+ asyncErr = err;
+ resolve();
+ };
+ if (!wantAsyncErr) resolve();
+ });
+ // @ts-ignore
+ events.forEach((event) => process.on(event, tempListener));
+ const call = () =>
+ callHookFnSync(hook, { ret1: "val1", ret2: "val2" });
+ if (step2.rejects) {
+ assert.throws(call, { message: "val2" });
+ } else if (!step1.async && !step2.async) {
+ assert.throws(call, { message: /DOUBLE SETTLE BUG/ });
+ } else {
+ assert.equal(call(), step1.async ? "val2" : "val1");
+ }
+ await seenErrPromise;
+ } finally {
+ // Restore the original listeners.
+ for (const event of events) {
+ // @ts-ignore
+ process.off(event, tempListener);
+ for (const listener of listenerBackups[event]) {
+ process.on(event, listener);
+ }
+ }
+ }
+ assert.equal(console.error.callCount, 1);
+ assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
+ if (wantAsyncErr) {
+ assert(asyncErr instanceof Error);
+ assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
+ }
+ });
+
+ // This next test is the same as the above test, except the second settle attempt is for
+ // the same outcome. The two outcomes can't be the same if one step throws and the other
+ // doesn't, so skip those cases.
+ if (step1.rejects !== step2.rejects) continue;
+
+ it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async () => {
+ const err = new Error("val");
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ step1.fn(cb, err, "val");
+ return step2.fn(cb, err, "val");
+ };
+
+ const errorLogged = new Promise((resolve) =>
+ console.error.callsFake(resolve),
+ );
+ const call = () => callHookFnSync(hook);
+ if (step2.rejects) {
+ assert.throws(call, { message: "val" });
+ } else {
+ assert.equal(call(), "val");
+ }
+ await errorLogged;
+ assert.equal(console.error.callCount, 1);
+ assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
+ });
+ }
+ }
+ });
+ });
+
+ describe("hooks.callAll", () => {
+ describe("basic behavior", () => {
+ it("calls all in order", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(1), makeHook(2), makeHook(3));
+ assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]);
+ });
+
+ it("passes hook name", async () => {
+ hook.hook_fn = (hn: string) => {
+ assert.equal(hn, hookName);
+ };
+ hooks.callAll(hookName);
+ });
+
+ it("undefined context -> {}", async () => {
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ hooks.callAll(hookName);
+ });
+
+ it("null context -> {}", async () => {
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ hooks.callAll(hookName, null);
+ });
+
+ it("context unmodified", async () => {
+ const wantContext = {};
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.equal(ctx, wantContext);
+ };
+ hooks.callAll(hookName, wantContext);
+ });
+ });
+
+ describe("result processing", () => {
+ it("no registered hooks (undefined) -> []", async () => {
+ delete plugins.hooks.testHook;
+ assert.deepEqual(hooks.callAll(hookName), []);
+ });
+
+ it("no registered hooks (empty list) -> []", async () => {
+ testHooks.length = 0;
+ assert.deepEqual(hooks.callAll(hookName), []);
+ });
+
+ it("flattens one level", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
+ assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]);
+ });
+
+ it("filters out undefined", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]));
+ assert.deepEqual(hooks.callAll(hookName), [2, [3]]);
+ });
+
+ it("preserves null", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]]));
+ assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]);
+ });
+
+ it("all undefined -> []", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook());
+ assert.deepEqual(hooks.callAll(hookName), []);
+ });
+ });
+ });
+
+ describe("hooks.callFirst", () => {
+ it("no registered hooks (undefined) -> []", async () => {
+ delete plugins.hooks.testHook;
+ assert.deepEqual(hooks.callFirst(hookName), []);
+ });
+
+ it("no registered hooks (empty list) -> []", async () => {
+ testHooks.length = 0;
+ assert.deepEqual(hooks.callFirst(hookName), []);
+ });
+
+ it("passes hook name => {}", async () => {
+ hook.hook_fn = (hn: string) => {
+ assert.equal(hn, hookName);
+ };
+ hooks.callFirst(hookName);
+ });
+
+ it("undefined context => {}", async () => {
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ hooks.callFirst(hookName);
+ });
+
+ it("null context => {}", async () => {
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ hooks.callFirst(hookName, null);
+ });
+
+ it("context unmodified", async () => {
+ const wantContext = {};
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.equal(ctx, wantContext);
+ };
+ hooks.callFirst(hookName, wantContext);
+ });
+
+ it("predicate never satisfied -> calls all in order", async () => {
+ const gotCalls: MapArrayType = [];
+ testHooks.length = 0;
+ for (let i = 0; i < 3; i++) {
+ const hook = makeHook();
+ hook.hook_fn = () => {
+ gotCalls.push(i);
+ };
+ testHooks.push(hook);
+ }
+ assert.deepEqual(hooks.callFirst(hookName), []);
+ assert.deepEqual(gotCalls, [0, 1, 2]);
+ });
+
+ it("stops when predicate is satisfied", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook("val1"), makeHook("val2"));
+ assert.deepEqual(hooks.callFirst(hookName), ["val1"]);
+ });
+
+ it("skips values that do not satisfy predicate (undefined)", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook("val1"));
+ assert.deepEqual(hooks.callFirst(hookName), ["val1"]);
+ });
+
+ it("skips values that do not satisfy predicate (empty list)", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook([]), makeHook("val1"));
+ assert.deepEqual(hooks.callFirst(hookName), ["val1"]);
+ });
+
+ it("null satisifes the predicate", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(null), makeHook("val1"));
+ assert.deepEqual(hooks.callFirst(hookName), [null]);
+ });
+
+ it("non-empty arrays are returned unmodified", async () => {
+ const want = ["val1"];
+ testHooks.length = 0;
+ testHooks.push(makeHook(want), makeHook(["val2"]));
+ assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual!
+ });
+
+ it("value can be passed via callback", async () => {
+ const want = {};
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ cb(want);
+ };
+ const got = hooks.callFirst(hookName);
+ assert.deepEqual(got, [want]);
+ assert.equal(got[0], want); // Note: *NOT* deepEqual!
+ });
+ });
+
+ describe("callHookFnAsync", () => {
+ const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand.
+
+ describe("basic behavior", () => {
+ it("passes hook name", async () => {
+ hook.hook_fn = (hn: string) => {
+ assert.equal(hn, hookName);
+ };
+ await callHookFnAsync(hook);
+ });
+
+ it("passes context", async () => {
+ for (const val of ["value", null, undefined]) {
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.equal(ctx, val);
+ };
+ await callHookFnAsync(hook, val);
+ }
+ });
+
+ it("returns the value provided to the callback", async () => {
+ for (const val of ["value", null, undefined]) {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ cb(ctx);
+ };
+ assert.equal(await callHookFnAsync(hook, val), val);
+ assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
+ }
+ });
+
+ it("returns the value returned by the hook function", async () => {
+ for (const val of ["value", null, undefined]) {
+ // Must not have the cb parameter otherwise returning undefined will never resolve.
+ hook.hook_fn = (hn: string, ctx: any) => ctx;
+ assert.equal(await callHookFnAsync(hook, val), val);
+ assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
+ }
+ });
+
+ it("rejects if it throws an exception", async () => {
+ hook.hook_fn = () => {
+ throw new Error("test exception");
+ };
+ await assert.rejects(callHookFnAsync(hook), {
+ message: "test exception",
+ });
+ });
+
+ it("rejects if rejected Promise passed to callback", async () => {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) =>
+ cb(Promise.reject(new Error("test exception")));
+ await assert.rejects(callHookFnAsync(hook), {
+ message: "test exception",
+ });
+ });
+
+ it("rejects if rejected Promise returned", async () => {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) =>
+ Promise.reject(new Error("test exception"));
+ await assert.rejects(callHookFnAsync(hook), {
+ message: "test exception",
+ });
+ });
+
+ it("callback returns undefined", async () => {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ assert.equal(cb("foo"), undefined);
+ };
+ await callHookFnAsync(hook);
+ });
+
+ it("checks for deprecation", async () => {
+ sinon.stub(console, "warn");
+ hooks.deprecationNotices[hookName] = "test deprecation";
+ await callHookFnAsync(hook);
+ assert.equal(
+ hooks.exportedForTestingOnly.deprecationWarned[hookFnName],
+ true,
+ );
+ assert.equal(console.warn.callCount, 1);
+ assert.match(console.warn.getCall(0).args[0], /test deprecation/);
+ });
+ });
+
+ describe("supported hook function styles", () => {
+ // @ts-ignore
+ const supportedHookFunctions = supportedSyncHookFunctions.concat([
+ {
+ name: "legacy async cb",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ process.nextTick(cb, "val");
+ },
+ want: "val",
+ },
+ // Already resolved Promises:
+ {
+ name: "return resolved Promise, with callback parameter",
+ fn: (hn: Function, ctx: any, cb: Function) => Promise.resolve("val"),
+ want: "val",
+ },
+ {
+ name: "return resolved Promise, without callback parameter",
+ fn: (hn: string, ctx: any) => Promise.resolve("val"),
+ want: "val",
+ },
+ {
+ name: "pass resolved Promise to callback",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ cb(Promise.resolve("val"));
+ },
+ want: "val",
+ },
+ // Not yet resolved Promises:
+ {
+ name: "return unresolved Promise, with callback parameter",
+ fn: (hn: Function, ctx: any, cb: Function) =>
+ new Promise((resolve) => process.nextTick(resolve, "val")),
+ want: "val",
+ },
+ {
+ name: "return unresolved Promise, without callback parameter",
+ fn: (hn: string, ctx: any) =>
+ new Promise((resolve) => process.nextTick(resolve, "val")),
+ want: "val",
+ },
+ {
+ name: "pass unresolved Promise to callback",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ cb(new Promise((resolve) => process.nextTick(resolve, "val")));
+ },
+ want: "val",
+ },
+ // Already rejected Promises:
+ {
+ name: "return rejected Promise, with callback parameter",
+ fn: (hn: Function, ctx: any, cb: Function) =>
+ Promise.reject(new Error("test rejection")),
+ wantErr: "test rejection",
+ },
+ {
+ name: "return rejected Promise, without callback parameter",
+ fn: (hn: string, ctx: any) =>
+ Promise.reject(new Error("test rejection")),
+ wantErr: "test rejection",
+ },
+ {
+ name: "pass rejected Promise to callback",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ cb(Promise.reject(new Error("test rejection")));
+ },
+ wantErr: "test rejection",
+ },
+ // Not yet rejected Promises:
+ {
+ name: "return unrejected Promise, with callback parameter",
+ fn: (hn: Function, ctx: any, cb: Function) =>
+ new Promise((resolve, reject) => {
+ process.nextTick(reject, new Error("test rejection"));
+ }),
+ wantErr: "test rejection",
+ },
+ {
+ name: "return unrejected Promise, without callback parameter",
+ fn: (hn: string, ctx: any) =>
+ new Promise((resolve, reject) => {
+ process.nextTick(reject, new Error("test rejection"));
+ }),
+ wantErr: "test rejection",
+ },
+ {
+ name: "pass unrejected Promise to callback",
+ fn: (hn: Function, ctx: any, cb: Function) => {
+ cb(
+ new Promise((resolve, reject) => {
+ process.nextTick(reject, new Error("test rejection"));
+ }),
+ );
+ },
+ wantErr: "test rejection",
+ },
+ ]);
+
+ for (const tc of supportedSyncHookFunctions.concat(
+ supportedHookFunctions,
+ )) {
+ it(tc.name, async () => {
+ sinon.stub(console, "warn");
+ sinon.stub(console, "error");
+ hook.hook_fn = tc.fn;
+ const p = callHookFnAsync(hook);
+ if (tc.wantErr) {
+ await assert.rejects(p, { message: tc.wantErr });
+ } else {
+ assert.equal(await p, tc.want);
+ }
+ assert.equal(console.warn.callCount, 0);
+ assert.equal(console.error.callCount, 0);
+ });
+ }
+ });
+
+ // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
+ // time, or call the callback and then return a value.)
+ describe("bad hook function behavior (double settle)", () => {
+ beforeEach(async () => {
+ sinon.stub(console, "error");
+ });
+
+ // Each item in this array codifies a way to settle an asynchronous hook function. Each of the
+ // test cases below combines two of these behaviors in a single hook function and confirms
+ // that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2)
+ // detects the second settle attempt.
+ //
+ // The 'when' property specifies the relative time that two behaviors will cause the hook
+ // function to settle:
+ // * If behavior1.when <= behavior2.when and behavior1 is called before behavior2 then
+ // behavior1 will settle the hook function before behavior2.
+ // * Otherwise, behavior2 will settle the hook function before behavior1.
+ const behaviors = [
+ {
+ name: "throw",
+ fn: (cb: Function, err: any, val: string) => {
+ throw err;
+ },
+ rejects: true,
+ when: 0,
+ },
+ {
+ name: "return value",
+ fn: (cb: Function, err: any, val: string) => val,
+ // This behavior has a later relative settle time vs. the 'throw' behavior because 'throw'
+ // immediately settles the hook function, whereas the 'return value' case is settled by a
+ // .then() function attached to a Promise. EcmaScript guarantees that a .then() function
+ // attached to a Promise is enqueued on the event loop (not executed immediately) when the
+ // Promise settles.
+ when: 1,
+ },
+ {
+ name: "immediately call cb(value)",
+ fn: (cb: Function, err: any, val: string) => cb(val),
+ // This behavior has the same relative time as the 'return value' case because it too is
+ // settled by a .then() function attached to a Promise.
+ when: 1,
+ },
+ {
+ name: "return resolvedPromise",
+ fn: (cb: Function, err: any, val: string) => Promise.resolve(val),
+ // This behavior has the same relative time as the 'return value' case because the return
+ // value is wrapped in a Promise via Promise.resolve(). The EcmaScript standard guarantees
+ // that Promise.resolve(Promise.resolve(value)) is equivalent to Promise.resolve(value),
+ // so returning an already resolved Promise vs. returning a non-Promise value are
+ // equivalent.
+ when: 1,
+ },
+ {
+ name: "immediately call cb(resolvedPromise)",
+ fn: (cb: Function, err: any, val: string) => cb(Promise.resolve(val)),
+ when: 1,
+ },
+ {
+ name: "return rejectedPromise",
+ fn: (cb: Function, err: any, val: string) => Promise.reject(err),
+ rejects: true,
+ when: 1,
+ },
+ {
+ name: "immediately call cb(rejectedPromise)",
+ fn: (cb: Function, err: any, val: string) => cb(Promise.reject(err)),
+ rejects: true,
+ when: 1,
+ },
+ {
+ name: "return unresolvedPromise",
+ fn: (cb: Function, err: any, val: string) =>
+ new Promise((resolve) => process.nextTick(resolve, val)),
+ when: 2,
+ },
+ {
+ name: "immediately call cb(unresolvedPromise)",
+ fn: (cb: Function, err: any, val: string) =>
+ cb(new Promise((resolve) => process.nextTick(resolve, val))),
+ when: 2,
+ },
+ {
+ name: "return unrejectedPromise",
+ fn: (cb: Function, err: any, val: string) =>
+ new Promise((resolve, reject) => process.nextTick(reject, err)),
+ rejects: true,
+ when: 2,
+ },
+ {
+ name: "immediately call cb(unrejectedPromise)",
+ fn: (cb: Function, err: any, val: string) =>
+ cb(new Promise((resolve, reject) => process.nextTick(reject, err))),
+ rejects: true,
+ when: 2,
+ },
+ {
+ name: "defer call to cb(value)",
+ fn: (cb: Function, err: any, val: string) => {
+ process.nextTick(cb, val);
+ },
+ when: 2,
+ },
+ {
+ name: "defer call to cb(resolvedPromise)",
+ fn: (cb: Function, err: any, val: string) => {
+ process.nextTick(cb, Promise.resolve(val));
+ },
+ when: 2,
+ },
+ {
+ name: "defer call to cb(rejectedPromise)",
+ fn: (cb: Function, err: any, val: string) => {
+ process.nextTick(cb, Promise.reject(err));
+ },
+ rejects: true,
+ when: 2,
+ },
+ {
+ name: "defer call to cb(unresolvedPromise)",
+ fn: (cb: Function, err: any, val: string) => {
+ process.nextTick(() => {
+ cb(new Promise((resolve) => process.nextTick(resolve, val)));
+ });
+ },
+ when: 3,
+ },
+ {
+ name: "defer call cb(unrejectedPromise)",
+ fn: (cb: Function, err: any, val: string) => {
+ process.nextTick(() => {
+ cb(
+ new Promise((resolve, reject) => process.nextTick(reject, err)),
+ );
+ });
+ },
+ rejects: true,
+ when: 3,
+ },
+ ];
+
+ for (const step1 of behaviors) {
+ // There can't be a second step if the first step is to return or throw.
+ if (step1.name.startsWith("return ") || step1.name === "throw")
+ continue;
+ for (const step2 of behaviors) {
+ it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async () => {
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
+ return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
+ };
+
+ // Temporarily remove unhandled Promise rejection listeners so that the unhandled
+ // rejections we expect to see don't trigger a test failure (or terminate node).
+ const event = "unhandledRejection";
+ const listenersBackup = process.rawListeners(event);
+ process.removeAllListeners(event);
+
+ let tempListener;
+ let asyncErr: Error;
+ try {
+ const seenErrPromise = new Promise((resolve) => {
+ tempListener = (err: any) => {
+ assert.equal(asyncErr, undefined);
+ asyncErr = err;
+ resolve();
+ };
+ });
+ process.on(event, tempListener!);
+ const step1Wins = step1.when <= step2.when;
+ const winningStep = step1Wins ? step1 : step2;
+ const winningVal = step1Wins ? "val1" : "val2";
+ const p = callHookFnAsync(hook, { ret1: "val1", ret2: "val2" });
+ if (winningStep.rejects) {
+ await assert.rejects(p, { message: winningVal });
+ } else {
+ assert.equal(await p, winningVal);
+ }
+ await seenErrPromise;
+ } finally {
+ // Restore the original listeners.
+ process.off(event, tempListener!);
+ for (const listener of listenersBackup) {
+ process.on(event, listener as any);
+ }
+ }
+ assert.equal(
+ console.error.callCount,
+ 1,
+ `Got errors:\n${console.error
+ .getCalls()
+ .map((call) => call.args[0])
+ .join("\n")}`,
+ );
+ assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
+ // @ts-ignore
+ assert(asyncErr instanceof Error);
+ assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
+ });
+
+ // This next test is the same as the above test, except the second settle attempt is for
+ // the same outcome. The two outcomes can't be the same if one step rejects and the other
+ // doesn't, so skip those cases.
+ if (step1.rejects !== step2.rejects) continue;
+
+ it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async () => {
+ const err = new Error("val");
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ step1.fn(cb, err, "val");
+ return step2.fn(cb, err, "val");
+ };
+ const winningStep = step1.when <= step2.when ? step1 : step2;
+ const errorLogged = new Promise((resolve) =>
+ console.error.callsFake(resolve),
+ );
+ const p = callHookFnAsync(hook);
+ if (winningStep.rejects) {
+ await assert.rejects(p, { message: "val" });
+ } else {
+ assert.equal(await p, "val");
+ }
+ await errorLogged;
+ assert.equal(console.error.callCount, 1);
+ assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
+ });
+ }
+ }
+ });
+ });
+
+ describe("hooks.aCallAll", () => {
+ describe("basic behavior", () => {
+ it("calls all asynchronously, returns values in order", async () => {
+ testHooks.length = 0; // Delete the boilerplate hook -- this test doesn't use it.
+ let nextIndex = 0;
+ const hookPromises: {
+ promise?: Promise;
+ resolve?: Function;
+ }[] = [];
+ const hookStarted: boolean[] = [];
+ const hookFinished: boolean[] = [];
+ const makeHook = () => {
+ const i = nextIndex++;
+ const entry: {
+ promise?: Promise;
+ resolve?: Function;
+ } = {};
+ hookStarted[i] = false;
+ hookFinished[i] = false;
+ hookPromises[i] = entry;
+ entry.promise = new Promise((resolve) => {
+ entry.resolve = () => {
+ hookFinished[i] = true;
+ resolve(i);
+ };
+ });
+ return {
+ hook_fn: () => {
+ hookStarted[i] = true;
+ return entry.promise;
+ },
+ };
+ };
+ testHooks.push(makeHook(), makeHook());
+ const p = hooks.aCallAll(hookName);
+ assert.deepEqual(hookStarted, [true, true]);
+ assert.deepEqual(hookFinished, [false, false]);
+ hookPromises[1].resolve!();
+ await hookPromises[1].promise;
+ assert.deepEqual(hookFinished, [false, true]);
+ hookPromises[0].resolve!();
+ assert.deepEqual(await p, [0, 1]);
+ });
+
+ it("passes hook name", async () => {
+ hook.hook_fn = async (hn: string) => {
+ assert.equal(hn, hookName);
+ };
+ await hooks.aCallAll(hookName);
+ });
+
+ it("undefined context -> {}", async () => {
+ hook.hook_fn = async (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ await hooks.aCallAll(hookName);
+ });
+
+ it("null context -> {}", async () => {
+ hook.hook_fn = async (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ await hooks.aCallAll(hookName, null);
+ });
+
+ it("context unmodified", async () => {
+ const wantContext = {};
+ hook.hook_fn = async (hn: string, ctx: any) => {
+ assert.equal(ctx, wantContext);
+ };
+ await hooks.aCallAll(hookName, wantContext);
+ });
+ });
+
+ describe("aCallAll callback", () => {
+ it("exception in callback rejects", async () => {
+ const p = hooks.aCallAll(hookName, {}, () => {
+ throw new Error("test exception");
+ });
+ await assert.rejects(p, { message: "test exception" });
+ });
+
+ it("propagates error on exception", async () => {
+ hook.hook_fn = () => {
+ throw new Error("test exception");
+ };
+ await hooks.aCallAll(hookName, {}, (err: any) => {
+ assert(err instanceof Error);
+ assert.equal(err.message, "test exception");
+ });
+ });
+
+ it("propagages null error on success", async () => {
+ await hooks.aCallAll(hookName, {}, (err: any) => {
+ assert(err == null, `got non-null error: ${err}`);
+ });
+ });
+
+ it("propagages results on success", async () => {
+ hook.hook_fn = () => "val";
+ await hooks.aCallAll(hookName, {}, (err: any, results: any) => {
+ assert.deepEqual(results, ["val"]);
+ });
+ });
+
+ it("returns callback return value", async () => {
+ assert.equal(await hooks.aCallAll(hookName, {}, () => "val"), "val");
+ });
+ });
+
+ describe("result processing", () => {
+ it("no registered hooks (undefined) -> []", async () => {
+ delete plugins.hooks[hookName];
+ assert.deepEqual(await hooks.aCallAll(hookName), []);
+ });
+
+ it("no registered hooks (empty list) -> []", async () => {
+ testHooks.length = 0;
+ assert.deepEqual(await hooks.aCallAll(hookName), []);
+ });
+
+ it("flattens one level", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
+ assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]);
+ });
+
+ it("filters out undefined", async () => {
+ testHooks.length = 0;
+ testHooks.push(
+ makeHook(),
+ makeHook([2]),
+ makeHook([[3]]),
+ makeHook(Promise.resolve()),
+ );
+ assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]);
+ });
+
+ it("preserves null", async () => {
+ testHooks.length = 0;
+ testHooks.push(
+ makeHook(null),
+ makeHook([2]),
+ makeHook(Promise.resolve(null)),
+ );
+ assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]);
+ });
+
+ it("all undefined -> []", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook(Promise.resolve()));
+ assert.deepEqual(await hooks.aCallAll(hookName), []);
+ });
+ });
+ });
+
+ describe("hooks.callAllSerial", () => {
+ describe("basic behavior", () => {
+ it("calls all asynchronously, serially, in order", async () => {
+ const gotCalls: number[] = [];
+ testHooks.length = 0;
+ for (let i = 0; i < 3; i++) {
+ const hook = makeHook();
+ hook.hook_fn = async () => {
+ gotCalls.push(i);
+ // Check gotCalls asynchronously to ensure that the next hook function does not start
+ // executing before this hook function has resolved.
+ return await new Promise((resolve) => {
+ setImmediate(() => {
+ assert.deepEqual(gotCalls, [...Array(i + 1).keys()]);
+ resolve(i);
+ });
+ });
+ };
+ testHooks.push(hook);
+ }
+ assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]);
+ assert.deepEqual(gotCalls, [0, 1, 2]);
+ });
+
+ it("passes hook name", async () => {
+ hook.hook_fn = async (hn: string) => {
+ assert.equal(hn, hookName);
+ };
+ await hooks.callAllSerial(hookName);
+ });
+
+ it("undefined context -> {}", async () => {
+ hook.hook_fn = async (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ await hooks.callAllSerial(hookName);
+ });
+
+ it("null context -> {}", async () => {
+ hook.hook_fn = async (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ await hooks.callAllSerial(hookName, null);
+ });
+
+ it("context unmodified", async () => {
+ const wantContext = {};
+ hook.hook_fn = async (hn: string, ctx: any) => {
+ assert.equal(ctx, wantContext);
+ };
+ await hooks.callAllSerial(hookName, wantContext);
+ });
+ });
+
+ describe("result processing", () => {
+ it("no registered hooks (undefined) -> []", async () => {
+ delete plugins.hooks[hookName];
+ assert.deepEqual(await hooks.callAllSerial(hookName), []);
+ });
+
+ it("no registered hooks (empty list) -> []", async () => {
+ testHooks.length = 0;
+ assert.deepEqual(await hooks.callAllSerial(hookName), []);
+ });
+
+ it("flattens one level", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
+ assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]);
+ });
+
+ it("filters out undefined", async () => {
+ testHooks.length = 0;
+ testHooks.push(
+ makeHook(),
+ makeHook([2]),
+ makeHook([[3]]),
+ makeHook(Promise.resolve()),
+ );
+ assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]);
+ });
+
+ it("preserves null", async () => {
+ testHooks.length = 0;
+ testHooks.push(
+ makeHook(null),
+ makeHook([2]),
+ makeHook(Promise.resolve(null)),
+ );
+ assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]);
+ });
+
+ it("all undefined -> []", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook(Promise.resolve()));
+ assert.deepEqual(await hooks.callAllSerial(hookName), []);
+ });
+ });
+ });
+
+ describe("hooks.aCallFirst", () => {
+ it("no registered hooks (undefined) -> []", async () => {
+ delete plugins.hooks.testHook;
+ assert.deepEqual(await hooks.aCallFirst(hookName), []);
+ });
+
+ it("no registered hooks (empty list) -> []", async () => {
+ testHooks.length = 0;
+ assert.deepEqual(await hooks.aCallFirst(hookName), []);
+ });
+
+ it("passes hook name => {}", async () => {
+ hook.hook_fn = (hn: string) => {
+ assert.equal(hn, hookName);
+ };
+ await hooks.aCallFirst(hookName);
+ });
+
+ it("undefined context => {}", async () => {
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ await hooks.aCallFirst(hookName);
+ });
+
+ it("null context => {}", async () => {
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.deepEqual(ctx, {});
+ };
+ await hooks.aCallFirst(hookName, null);
+ });
+
+ it("context unmodified", async () => {
+ const wantContext = {};
+ hook.hook_fn = (hn: string, ctx: any) => {
+ assert.equal(ctx, wantContext);
+ };
+ await hooks.aCallFirst(hookName, wantContext);
+ });
+
+ it("default predicate: predicate never satisfied -> calls all in order", async () => {
+ const gotCalls: number[] = [];
+ testHooks.length = 0;
+ for (let i = 0; i < 3; i++) {
+ const hook = makeHook();
+ hook.hook_fn = () => {
+ gotCalls.push(i);
+ };
+ testHooks.push(hook);
+ }
+ assert.deepEqual(await hooks.aCallFirst(hookName), []);
+ assert.deepEqual(gotCalls, [0, 1, 2]);
+ });
+
+ it("calls hook functions serially", async () => {
+ const gotCalls: number[] = [];
+ testHooks.length = 0;
+ for (let i = 0; i < 3; i++) {
+ const hook = makeHook();
+ hook.hook_fn = async () => {
+ gotCalls.push(i);
+ // Check gotCalls asynchronously to ensure that the next hook function does not start
+ // executing before this hook function has resolved.
+ return await new Promise((resolve) => {
+ setImmediate(() => {
+ assert.deepEqual(gotCalls, [...Array(i + 1).keys()]);
+ resolve();
+ });
+ });
+ };
+ testHooks.push(hook);
+ }
+ assert.deepEqual(await hooks.aCallFirst(hookName), []);
+ assert.deepEqual(gotCalls, [0, 1, 2]);
+ });
+
+ it("default predicate: stops when satisfied", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook("val1"), makeHook("val2"));
+ assert.deepEqual(await hooks.aCallFirst(hookName), ["val1"]);
+ });
+
+ it("default predicate: skips values that do not satisfy (undefined)", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(), makeHook("val1"));
+ assert.deepEqual(await hooks.aCallFirst(hookName), ["val1"]);
+ });
+
+ it("default predicate: skips values that do not satisfy (empty list)", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook([]), makeHook("val1"));
+ assert.deepEqual(await hooks.aCallFirst(hookName), ["val1"]);
+ });
+
+ it("default predicate: null satisifes", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(null), makeHook("val1"));
+ assert.deepEqual(await hooks.aCallFirst(hookName), [null]);
+ });
+
+ it("custom predicate: called for each hook function", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(0), makeHook(1), makeHook(2));
+ let got = 0;
+ await hooks.aCallFirst(hookName, null, null, (val: string) => {
+ ++got;
+ return false;
+ });
+ assert.equal(got, 3);
+ });
+
+ it("custom predicate: boolean false/true continues/stops iteration", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(1), makeHook(2), makeHook(3));
+ let nCall = 0;
+ const predicate = (val: number[]) => {
+ assert.deepEqual(val, [++nCall]);
+ return nCall === 2;
+ };
+ assert.deepEqual(
+ await hooks.aCallFirst(hookName, null, null, predicate),
+ [2],
+ );
+ assert.equal(nCall, 2);
+ });
+
+ it("custom predicate: non-boolean falsy/truthy continues/stops iteration", async () => {
+ testHooks.length = 0;
+ testHooks.push(makeHook(1), makeHook(2), makeHook(3));
+ let nCall = 0;
+ const predicate = (val: number[]) => {
+ assert.deepEqual(val, [++nCall]);
+ return nCall === 2 ? {} : null;
+ };
+ assert.deepEqual(
+ await hooks.aCallFirst(hookName, null, null, predicate),
+ [2],
+ );
+ assert.equal(nCall, 2);
+ });
+
+ it("custom predicate: array value passed unmodified to predicate", async () => {
+ const want = [0];
+ hook.hook_fn = () => want;
+ const predicate = (got: []) => {
+ assert.equal(got, want);
+ }; // Note: *NOT* deepEqual!
+ await hooks.aCallFirst(hookName, null, null, predicate);
+ });
+
+ it("custom predicate: normalized value passed to predicate (undefined)", async () => {
+ const predicate = (got: []) => {
+ assert.deepEqual(got, []);
+ };
+ await hooks.aCallFirst(hookName, null, null, predicate);
+ });
+
+ it("custom predicate: normalized value passed to predicate (null)", async () => {
+ hook.hook_fn = () => null;
+ const predicate = (got: []) => {
+ assert.deepEqual(got, [null]);
+ };
+ await hooks.aCallFirst(hookName, null, null, predicate);
+ });
+
+ it("non-empty arrays are returned unmodified", async () => {
+ const want = ["val1"];
+ testHooks.length = 0;
+ testHooks.push(makeHook(want), makeHook(["val2"]));
+ assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual!
+ });
+
+ it("value can be passed via callback", async () => {
+ const want = {};
+ hook.hook_fn = (hn: Function, ctx: any, cb: Function) => {
+ cb(want);
+ };
+ const got = await hooks.aCallFirst(hookName);
+ assert.deepEqual(got, [want]);
+ assert.equal(got[0], want); // Note: *NOT* deepEqual!
+ });
+ });
});
diff --git a/src/tests/backend/specs/lowerCasePadIds.ts b/src/tests/backend/specs/lowerCasePadIds.ts
index 359f85d2c..c090e9bcf 100644
--- a/src/tests/backend/specs/lowerCasePadIds.ts
+++ b/src/tests/backend/specs/lowerCasePadIds.ts
@@ -1,90 +1,107 @@
-'use strict';
+const assert = require("assert").strict;
+const common = require("../common");
+const padManager = require("../../../node/db/PadManager");
+const settings = require("../../../node/utils/Settings");
-const assert = require('assert').strict;
-const common = require('../common');
-const padManager = require('../../../node/db/PadManager');
-const settings = require('../../../node/utils/Settings');
+describe(__filename, () => {
+ let agent: any;
+ const cleanUpPads = async () => {
+ const { padIDs } = await padManager.listAllPads();
+ await Promise.all(
+ padIDs.map(async (padId: string) => {
+ if (await padManager.doesPadExist(padId)) {
+ const pad = await padManager.getPad(padId);
+ await pad.remove();
+ }
+ }),
+ );
+ };
+ let backup: any;
-describe(__filename, function () {
- let agent:any;
- const cleanUpPads = async () => {
- const {padIDs} = await padManager.listAllPads();
- await Promise.all(padIDs.map(async (padId: string) => {
- if (await padManager.doesPadExist(padId)) {
- const pad = await padManager.getPad(padId);
- await pad.remove();
- }
- }));
- };
- let backup:any;
+ before(async () => {
+ backup = settings.lowerCasePadIds;
+ agent = await common.init();
+ });
+ beforeEach(async () => {
+ await cleanUpPads();
+ });
+ afterEach(async () => {
+ await cleanUpPads();
+ });
+ after(async () => {
+ settings.lowerCasePadIds = backup;
+ });
- before(async function () {
- backup = settings.lowerCasePadIds;
- agent = await common.init();
- });
- beforeEach(async function () {
- await cleanUpPads();
- });
- afterEach(async function () {
- await cleanUpPads();
- });
- after(async function () {
- settings.lowerCasePadIds = backup;
- });
+ describe("not activated", () => {
+ beforeEach(async () => {
+ settings.lowerCasePadIds = false;
+ });
- describe('not activated', function () {
- beforeEach(async function () {
- settings.lowerCasePadIds = false;
- });
+ it("do nothing", async () => {
+ await agent.get("/p/UPPERCASEpad").expect(200);
+ });
+ });
+ describe("activated", () => {
+ beforeEach(async () => {
+ settings.lowerCasePadIds = true;
+ });
+ it("lowercase pad ids", async () => {
+ await agent
+ .get("/p/UPPERCASEpad")
+ .expect(302)
+ .expect("location", "uppercasepad");
+ });
- it('do nothing', async function () {
- await agent.get('/p/UPPERCASEpad')
- .expect(200);
- });
- });
+ it("keeps old pads accessible", async () => {
+ Object.assign(settings, {
+ lowerCasePadIds: false,
+ });
+ await padManager.getPad("ALREADYexistingPad", "oldpad");
+ await padManager.getPad("alreadyexistingpad", "newpad");
+ Object.assign(settings, {
+ lowerCasePadIds: true,
+ });
- describe('activated', function () {
- beforeEach(async function () {
- settings.lowerCasePadIds = true;
- });
- it('lowercase pad ids', async function () {
- await agent.get('/p/UPPERCASEpad')
- .expect(302)
- .expect('location', 'uppercasepad');
- });
+ const oldPad = await agent.get("/p/ALREADYexistingPad").expect(200);
+ const oldPadSocket = await common.connect(oldPad);
+ const oldPadHandshake = await common.handshake(
+ oldPadSocket,
+ "ALREADYexistingPad",
+ );
+ assert.equal(oldPadHandshake.data.padId, "ALREADYexistingPad");
+ assert.equal(
+ oldPadHandshake.data.collab_client_vars.initialAttributedText.text,
+ "oldpad\n",
+ );
- it('keeps old pads accessible', async function () {
- Object.assign(settings, {
- lowerCasePadIds: false,
- });
- await padManager.getPad('ALREADYexistingPad', 'oldpad');
- await padManager.getPad('alreadyexistingpad', 'newpad');
- Object.assign(settings, {
- lowerCasePadIds: true,
- });
+ const newPad = await agent.get("/p/alreadyexistingpad").expect(200);
+ const newPadSocket = await common.connect(newPad);
+ const newPadHandshake = await common.handshake(
+ newPadSocket,
+ "alreadyexistingpad",
+ );
+ assert.equal(newPadHandshake.data.padId, "alreadyexistingpad");
+ assert.equal(
+ newPadHandshake.data.collab_client_vars.initialAttributedText.text,
+ "newpad\n",
+ );
+ });
- const oldPad = await agent.get('/p/ALREADYexistingPad').expect(200);
- const oldPadSocket = await common.connect(oldPad);
- const oldPadHandshake = await common.handshake(oldPadSocket, 'ALREADYexistingPad');
- assert.equal(oldPadHandshake.data.padId, 'ALREADYexistingPad');
- assert.equal(oldPadHandshake.data.collab_client_vars.initialAttributedText.text, 'oldpad\n');
+ it("disallow creation of different case pad-name via socket connection", async () => {
+ await padManager.getPad("maliciousattempt", "attempt");
- const newPad = await agent.get('/p/alreadyexistingpad').expect(200);
- const newPadSocket = await common.connect(newPad);
- const newPadHandshake = await common.handshake(newPadSocket, 'alreadyexistingpad');
- assert.equal(newPadHandshake.data.padId, 'alreadyexistingpad');
- assert.equal(newPadHandshake.data.collab_client_vars.initialAttributedText.text, 'newpad\n');
- });
+ const newPad = await agent.get("/p/maliciousattempt").expect(200);
+ const newPadSocket = await common.connect(newPad);
+ const newPadHandshake = await common.handshake(
+ newPadSocket,
+ "MaliciousAttempt",
+ );
- it('disallow creation of different case pad-name via socket connection', async function () {
- await padManager.getPad('maliciousattempt', 'attempt');
-
- const newPad = await agent.get('/p/maliciousattempt').expect(200);
- const newPadSocket = await common.connect(newPad);
- const newPadHandshake = await common.handshake(newPadSocket, 'MaliciousAttempt');
-
- assert.equal(newPadHandshake.data.collab_client_vars.initialAttributedText.text, 'attempt\n');
- });
- });
+ assert.equal(
+ newPadHandshake.data.collab_client_vars.initialAttributedText.text,
+ "attempt\n",
+ );
+ });
+ });
});
diff --git a/src/tests/backend/specs/messages.ts b/src/tests/backend/specs/messages.ts
index 9d91b2342..646b2ee5b 100644
--- a/src/tests/backend/specs/messages.ts
+++ b/src/tests/backend/specs/messages.ts
@@ -1,258 +1,257 @@
-'use strict';
+import type { MapArrayType } from "../../../node/types/MapType";
+import type { PadType } from "../../../node/types/PadType";
-import {PadType} from "../../../node/types/PadType";
-import {MapArrayType} from "../../../node/types/MapType";
+const assert = require("assert").strict;
+const common = require("../common");
+const padManager = require("../../../node/db/PadManager");
+const plugins = require("../../../static/js/pluginfw/plugin_defs");
+const readOnlyManager = require("../../../node/db/ReadOnlyManager");
-const assert = require('assert').strict;
-const common = require('../common');
-const padManager = require('../../../node/db/PadManager');
-const plugins = require('../../../static/js/pluginfw/plugin_defs');
-const readOnlyManager = require('../../../node/db/ReadOnlyManager');
+describe(__filename, () => {
+ let agent: any;
+ let pad: PadType | null;
+ let padId: string;
+ let roPadId: string;
+ let rev: number;
+ let socket: any;
+ let roSocket: any;
+ const backups: MapArrayType = {};
-describe(__filename, function () {
- let agent:any;
- let pad:PadType|null;
- let padId: string;
- let roPadId: string;
- let rev: number;
- let socket: any;
- let roSocket: any;
- const backups:MapArrayType = {};
+ before(async () => {
+ agent = await common.init();
+ });
- before(async function () {
- agent = await common.init();
- });
+ beforeEach(async () => {
+ backups.hooks = {
+ handleMessageSecurity: plugins.hooks.handleMessageSecurity,
+ };
+ plugins.hooks.handleMessageSecurity = [];
+ padId = common.randomString();
+ assert(!(await padManager.doesPadExist(padId)));
+ pad = await padManager.getPad(padId, "dummy text\n");
+ await pad!.setText("\n"); // Make sure the pad is created.
+ assert.equal(pad!.text(), "\n");
+ let res = await agent.get(`/p/${padId}`).expect(200);
+ socket = await common.connect(res);
+ const { type, data: clientVars } = await common.handshake(socket, padId);
+ assert.equal(type, "CLIENT_VARS");
+ rev = clientVars.collab_client_vars.rev;
- beforeEach(async function () {
- backups.hooks = {handleMessageSecurity: plugins.hooks.handleMessageSecurity};
- plugins.hooks.handleMessageSecurity = [];
- padId = common.randomString();
- assert(!await padManager.doesPadExist(padId));
- pad = await padManager.getPad(padId, 'dummy text\n');
- await pad!.setText('\n'); // Make sure the pad is created.
- assert.equal(pad!.text(), '\n');
- let res = await agent.get(`/p/${padId}`).expect(200);
- socket = await common.connect(res);
- const {type, data: clientVars} = await common.handshake(socket, padId);
- assert.equal(type, 'CLIENT_VARS');
- rev = clientVars.collab_client_vars.rev;
+ roPadId = await readOnlyManager.getReadOnlyId(padId);
+ res = await agent.get(`/p/${roPadId}`).expect(200);
+ roSocket = await common.connect(res);
+ await common.handshake(roSocket, roPadId);
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ });
- roPadId = await readOnlyManager.getReadOnlyId(padId);
- res = await agent.get(`/p/${roPadId}`).expect(200);
- roSocket = await common.connect(res);
- await common.handshake(roSocket, roPadId);
- await new Promise(resolve => setTimeout(resolve, 1000));
- });
+ afterEach(async () => {
+ Object.assign(plugins.hooks, backups.hooks);
+ if (socket != null) socket.close();
+ socket = null;
+ if (roSocket != null) roSocket.close();
+ roSocket = null;
+ if (pad != null) await pad.remove();
+ pad = null;
+ });
- afterEach(async function () {
- Object.assign(plugins.hooks, backups.hooks);
- if (socket != null) socket.close();
- socket = null;
- if (roSocket != null) roSocket.close();
- roSocket = null;
- if (pad != null) await pad.remove();
- pad = null;
- });
+ describe("CHANGESET_REQ", () => {
+ it("users are unable to read changesets from other pads", async () => {
+ const otherPadId = `${padId}other`;
+ assert(!(await padManager.doesPadExist(otherPadId)));
+ const otherPad = await padManager.getPad(otherPadId, "other text\n");
+ try {
+ await otherPad.setText("other text\n");
+ const resP = common.waitForSocketEvent(roSocket, "message");
+ await common.sendMessage(roSocket, {
+ component: "pad",
+ padId: otherPadId, // The server should ignore this.
+ type: "CHANGESET_REQ",
+ data: {
+ granularity: 1,
+ start: 0,
+ requestID: "requestId",
+ },
+ });
+ const res = await resP;
+ assert.equal(res.type, "CHANGESET_REQ");
+ assert.equal(res.data.requestID, "requestId");
+ // Should match padId's text, not otherPadId's text.
+ assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
+ } finally {
+ await otherPad.remove();
+ }
+ });
- describe('CHANGESET_REQ', function () {
- it('users are unable to read changesets from other pads', async function () {
- const otherPadId = `${padId}other`;
- assert(!await padManager.doesPadExist(otherPadId));
- const otherPad = await padManager.getPad(otherPadId, 'other text\n');
- try {
- await otherPad.setText('other text\n');
- const resP = common.waitForSocketEvent(roSocket, 'message');
- await common.sendMessage(roSocket, {
- component: 'pad',
- padId: otherPadId, // The server should ignore this.
- type: 'CHANGESET_REQ',
- data: {
- granularity: 1,
- start: 0,
- requestID: 'requestId',
- },
- });
- const res = await resP;
- assert.equal(res.type, 'CHANGESET_REQ');
- assert.equal(res.data.requestID, 'requestId');
- // Should match padId's text, not otherPadId's text.
- assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
- } finally {
- await otherPad.remove();
- }
- });
+ it("CHANGESET_REQ: verify revNum is a number (regression)", async () => {
+ const otherPadId = `${padId}other`;
+ assert(!(await padManager.doesPadExist(otherPadId)));
+ const otherPad = await padManager.getPad(otherPadId, "other text\n");
+ let errorCatched = 0;
+ try {
+ await otherPad.setText("other text\n");
+ await common.sendMessage(roSocket, {
+ component: "pad",
+ padId: otherPadId, // The server should ignore this.
+ type: "CHANGESET_REQ",
+ data: {
+ granularity: 1,
+ start: "test123",
+ requestID: "requestId",
+ },
+ });
+ assert.equal("This code should never run", 1);
+ } catch (e: any) {
+ assert.match(e.message, /rev is not a number/);
+ errorCatched = 1;
+ } finally {
+ await otherPad.remove();
+ assert.equal(errorCatched, 1);
+ }
+ });
- it('CHANGESET_REQ: verify revNum is a number (regression)', async function () {
- const otherPadId = `${padId}other`;
- assert(!await padManager.doesPadExist(otherPadId));
- const otherPad = await padManager.getPad(otherPadId, 'other text\n');
- let errorCatched = 0;
- try {
- await otherPad.setText('other text\n');
- await common.sendMessage(roSocket, {
- component: 'pad',
- padId: otherPadId, // The server should ignore this.
- type: 'CHANGESET_REQ',
- data: {
- granularity: 1,
- start: 'test123',
- requestID: 'requestId',
- },
- });
- assert.equal('This code should never run', 1);
- }
- catch(e:any) {
- assert.match(e.message, /rev is not a number/);
- errorCatched = 1;
- }
- finally {
- await otherPad.remove();
- assert.equal(errorCatched, 1);
- }
- });
+ it("CHANGESET_REQ: revNum is converted to number if possible (regression)", async () => {
+ const otherPadId = `${padId}other`;
+ assert(!(await padManager.doesPadExist(otherPadId)));
+ const otherPad = await padManager.getPad(otherPadId, "other text\n");
+ try {
+ await otherPad.setText("other text\n");
+ const resP = common.waitForSocketEvent(roSocket, "message");
+ await common.sendMessage(roSocket, {
+ component: "pad",
+ padId: otherPadId, // The server should ignore this.
+ type: "CHANGESET_REQ",
+ data: {
+ granularity: 1,
+ start: "1test123",
+ requestID: "requestId",
+ },
+ });
+ const res = await resP;
+ assert.equal(res.type, "CHANGESET_REQ");
+ assert.equal(res.data.requestID, "requestId");
+ assert.equal(res.data.start, 1);
+ } finally {
+ await otherPad.remove();
+ }
+ });
- it('CHANGESET_REQ: revNum is converted to number if possible (regression)', async function () {
- const otherPadId = `${padId}other`;
- assert(!await padManager.doesPadExist(otherPadId));
- const otherPad = await padManager.getPad(otherPadId, 'other text\n');
- try {
- await otherPad.setText('other text\n');
- const resP = common.waitForSocketEvent(roSocket, 'message');
- await common.sendMessage(roSocket, {
- component: 'pad',
- padId: otherPadId, // The server should ignore this.
- type: 'CHANGESET_REQ',
- data: {
- granularity: 1,
- start: '1test123',
- requestID: 'requestId',
- },
- });
- const res = await resP;
- assert.equal(res.type, 'CHANGESET_REQ');
- assert.equal(res.data.requestID, 'requestId');
- assert.equal(res.data.start, 1);
- }
- finally {
- await otherPad.remove();
- }
- });
+ it("CHANGESET_REQ: revNum 2 is converted to head rev 1 (regression)", async () => {
+ const otherPadId = `${padId}other`;
+ assert(!(await padManager.doesPadExist(otherPadId)));
+ const otherPad = await padManager.getPad(otherPadId, "other text\n");
+ try {
+ await otherPad.setText("other text\n");
+ const resP = common.waitForSocketEvent(roSocket, "message");
+ await common.sendMessage(roSocket, {
+ component: "pad",
+ padId: otherPadId, // The server should ignore this.
+ type: "CHANGESET_REQ",
+ data: {
+ granularity: 1,
+ start: "2",
+ requestID: "requestId",
+ },
+ });
+ const res = await resP;
+ assert.equal(res.type, "CHANGESET_REQ");
+ assert.equal(res.data.requestID, "requestId");
+ assert.equal(res.data.start, 1);
+ } finally {
+ await otherPad.remove();
+ }
+ });
+ });
- it('CHANGESET_REQ: revNum 2 is converted to head rev 1 (regression)', async function () {
- const otherPadId = `${padId}other`;
- assert(!await padManager.doesPadExist(otherPadId));
- const otherPad = await padManager.getPad(otherPadId, 'other text\n');
- try {
- await otherPad.setText('other text\n');
- const resP = common.waitForSocketEvent(roSocket, 'message');
- await common.sendMessage(roSocket, {
- component: 'pad',
- padId: otherPadId, // The server should ignore this.
- type: 'CHANGESET_REQ',
- data: {
- granularity: 1,
- start: '2',
- requestID: 'requestId',
- },
- });
- const res = await resP;
- assert.equal(res.type, 'CHANGESET_REQ');
- assert.equal(res.data.requestID, 'requestId');
- assert.equal(res.data.start, 1);
- }
- finally {
- await otherPad.remove();
- }
- });
- });
+ describe("USER_CHANGES", () => {
+ const sendUserChanges = async (socket: any, cs: any) =>
+ await common.sendUserChanges(socket, { baseRev: rev, changeset: cs });
+ const assertAccepted = async (socket: any, wantRev: number) => {
+ await common.waitForAcceptCommit(socket, wantRev);
+ rev = wantRev;
+ };
+ const assertRejected = async (socket: any) => {
+ const msg = await common.waitForSocketEvent(socket, "message");
+ assert.deepEqual(msg, { disconnect: "badChangeset" });
+ };
- describe('USER_CHANGES', function () {
- const sendUserChanges =
- async (socket:any, cs:any) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs});
- const assertAccepted = async (socket:any, wantRev: number) => {
- await common.waitForAcceptCommit(socket, wantRev);
- rev = wantRev;
- };
- const assertRejected = async (socket:any) => {
- const msg = await common.waitForSocketEvent(socket, 'message');
- assert.deepEqual(msg, {disconnect: 'badChangeset'});
- };
+ it("changes are applied", async () => {
+ await Promise.all([
+ assertAccepted(socket, rev + 1),
+ sendUserChanges(socket, "Z:1>5+5$hello"),
+ ]);
+ assert.equal(pad!.text(), "hello\n");
+ });
- it('changes are applied', async function () {
- await Promise.all([
- assertAccepted(socket, rev + 1),
- sendUserChanges(socket, 'Z:1>5+5$hello'),
- ]);
- assert.equal(pad!.text(), 'hello\n');
- });
+ it("bad changeset is rejected", async () => {
+ await Promise.all([
+ assertRejected(socket),
+ sendUserChanges(socket, "this is not a valid changeset"),
+ ]);
+ });
- it('bad changeset is rejected', async function () {
- await Promise.all([
- assertRejected(socket),
- sendUserChanges(socket, 'this is not a valid changeset'),
- ]);
- });
+ it("retransmission is accepted, has no effect", async () => {
+ const cs = "Z:1>5+5$hello";
+ await Promise.all([
+ assertAccepted(socket, rev + 1),
+ sendUserChanges(socket, cs),
+ ]);
+ --rev;
+ await Promise.all([
+ assertAccepted(socket, rev + 1),
+ sendUserChanges(socket, cs),
+ ]);
+ assert.equal(pad!.text(), "hello\n");
+ });
- it('retransmission is accepted, has no effect', async function () {
- const cs = 'Z:1>5+5$hello';
- await Promise.all([
- assertAccepted(socket, rev + 1),
- sendUserChanges(socket, cs),
- ]);
- --rev;
- await Promise.all([
- assertAccepted(socket, rev + 1),
- sendUserChanges(socket, cs),
- ]);
- assert.equal(pad!.text(), 'hello\n');
- });
+ it("identity changeset is accepted, has no effect", async () => {
+ await Promise.all([
+ assertAccepted(socket, rev + 1),
+ sendUserChanges(socket, "Z:1>5+5$hello"),
+ ]);
+ await Promise.all([
+ assertAccepted(socket, rev),
+ sendUserChanges(socket, "Z:6>0$"),
+ ]);
+ assert.equal(pad!.text(), "hello\n");
+ });
- it('identity changeset is accepted, has no effect', async function () {
- await Promise.all([
- assertAccepted(socket, rev + 1),
- sendUserChanges(socket, 'Z:1>5+5$hello'),
- ]);
- await Promise.all([
- assertAccepted(socket, rev),
- sendUserChanges(socket, 'Z:6>0$'),
- ]);
- assert.equal(pad!.text(), 'hello\n');
- });
+ it("non-identity changeset with no net change is accepted, has no effect", async () => {
+ await Promise.all([
+ assertAccepted(socket, rev + 1),
+ sendUserChanges(socket, "Z:1>5+5$hello"),
+ ]);
+ await Promise.all([
+ assertAccepted(socket, rev),
+ sendUserChanges(socket, "Z:6>0-5+5$hello"),
+ ]);
+ assert.equal(pad!.text(), "hello\n");
+ });
- it('non-identity changeset with no net change is accepted, has no effect', async function () {
- await Promise.all([
- assertAccepted(socket, rev + 1),
- sendUserChanges(socket, 'Z:1>5+5$hello'),
- ]);
- await Promise.all([
- assertAccepted(socket, rev),
- sendUserChanges(socket, 'Z:6>0-5+5$hello'),
- ]);
- assert.equal(pad!.text(), 'hello\n');
- });
+ it("handleMessageSecurity can grant one-time write access", async () => {
+ const cs = "Z:1>5+5$hello";
+ const errRegEx = /write attempt on read-only pad/;
+ // First try to send a change and verify that it was dropped.
+ await assert.rejects(sendUserChanges(roSocket, cs), errRegEx);
+ // sendUserChanges() waits for message ack, so if the message was accepted then head should
+ // have already incremented by the time we get here.
+ assert.equal(pad!.head, rev); // Not incremented.
- it('handleMessageSecurity can grant one-time write access', async function () {
- const cs = 'Z:1>5+5$hello';
- const errRegEx = /write attempt on read-only pad/;
- // First try to send a change and verify that it was dropped.
- await assert.rejects(sendUserChanges(roSocket, cs), errRegEx);
- // sendUserChanges() waits for message ack, so if the message was accepted then head should
- // have already incremented by the time we get here.
- assert.equal(pad!.head, rev); // Not incremented.
+ // Now allow the change.
+ plugins.hooks.handleMessageSecurity.push({ hook_fn: () => "permitOnce" });
+ await Promise.all([
+ assertAccepted(roSocket, rev + 1),
+ sendUserChanges(roSocket, cs),
+ ]);
+ assert.equal(pad!.text(), "hello\n");
- // Now allow the change.
- plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'});
- await Promise.all([
- assertAccepted(roSocket, rev + 1),
- sendUserChanges(roSocket, cs),
- ]);
- assert.equal(pad!.text(), 'hello\n');
-
- // The next change should be dropped.
- plugins.hooks.handleMessageSecurity = [];
- await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx);
- assert.equal(pad!.head, rev); // Not incremented.
- assert.equal(pad!.text(), 'hello\n');
- });
- });
+ // The next change should be dropped.
+ plugins.hooks.handleMessageSecurity = [];
+ await assert.rejects(
+ sendUserChanges(roSocket, "Z:6>6=5+6$ world"),
+ errRegEx,
+ );
+ assert.equal(pad!.head, rev); // Not incremented.
+ assert.equal(pad!.text(), "hello\n");
+ });
+ });
});
diff --git a/src/tests/backend/specs/pad_utils.ts b/src/tests/backend/specs/pad_utils.ts
index 3ca3c0858..95384b443 100644
--- a/src/tests/backend/specs/pad_utils.ts
+++ b/src/tests/backend/specs/pad_utils.ts
@@ -1,45 +1,49 @@
-'use strict';
+import type { MapArrayType } from "../../../node/types/MapType";
-import {MapArrayType} from "../../../node/types/MapType";
+import { strict as assert } from "assert";
+const { padutils } = require("../../../static/js/pad_utils");
-import {strict as assert} from "assert";
-const {padutils} = require('../../../static/js/pad_utils');
+describe(__filename, () => {
+ describe("warnDeprecated", () => {
+ const { warnDeprecated } = padutils;
+ const backups: MapArrayType = {};
-describe(__filename, function () {
- describe('warnDeprecated', function () {
- const {warnDeprecated} = padutils;
- const backups:MapArrayType = {};
+ before(async () => {
+ backups.logger = warnDeprecated.logger;
+ });
- before(async function () {
- backups.logger = warnDeprecated.logger;
- });
+ afterEach(async () => {
+ warnDeprecated.logger = backups.logger;
+ delete warnDeprecated._rl; // Reset internal rate limiter state.
+ });
- afterEach(async function () {
- warnDeprecated.logger = backups.logger;
- delete warnDeprecated._rl; // Reset internal rate limiter state.
- });
-
- /*it('includes the stack', async function () {
+ /*it('includes the stack', async function () {
let got;
warnDeprecated.logger = {warn: (stack: any) => got = stack};
warnDeprecated();
assert(got!.includes(__filename));
});*/
- it('rate limited', async function () {
- let got = 0;
- warnDeprecated.logger = {warn: () => ++got};
- warnDeprecated(); // Initialize internal rate limiter state.
- const {period} = warnDeprecated._rl;
- got = 0;
- const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]];
- for (const [now, want] of testCases) { // In a loop so that the stack trace is the same.
- warnDeprecated._rl.now = () => now;
- warnDeprecated();
- assert.equal(got, want);
- }
- warnDeprecated(); // Should have a different stack trace.
- assert.equal(got, testCases[testCases.length - 1][1] + 1);
- });
- });
+ it("rate limited", async () => {
+ let got = 0;
+ warnDeprecated.logger = { warn: () => ++got };
+ warnDeprecated(); // Initialize internal rate limiter state.
+ const { period } = warnDeprecated._rl;
+ got = 0;
+ const testCases = [
+ [0, 1],
+ [0, 1],
+ [period - 1, 1],
+ [period, 2],
+ ];
+ for (const [now, want] of testCases) {
+ // In a loop so that the stack trace is the same.
+ warnDeprecated._rl.now = () => now;
+ warnDeprecated();
+ assert.equal(got, want);
+ }
+ warnDeprecated(); // Should have a different stack trace.
+ assert.equal(got, testCases[testCases.length - 1][1] + 1);
+ });
+ });
});
diff --git a/src/tests/backend/specs/pads-with-spaces.ts b/src/tests/backend/specs/pads-with-spaces.ts
index cfadca1b9..a15d42e89 100644
--- a/src/tests/backend/specs/pads-with-spaces.ts
+++ b/src/tests/backend/specs/pads-with-spaces.ts
@@ -1,23 +1,23 @@
-'use strict';
+const common = require("../common");
-const common = require('../common');
+let agent: any;
-let agent:any;
+describe(__filename, () => {
+ before(async () => {
+ agent = await common.init();
+ });
-describe(__filename, function () {
- before(async function () {
- agent = await common.init();
- });
+ it("supports pads with spaces, regression test for #4883", async () => {
+ await agent
+ .get("/p/pads with spaces")
+ .expect(302)
+ .expect("location", "pads_with_spaces");
+ });
- it('supports pads with spaces, regression test for #4883', async function () {
- await agent.get('/p/pads with spaces')
- .expect(302)
- .expect('location', 'pads_with_spaces');
- });
-
- it('supports pads with spaces and query, regression test for #4883', async function () {
- await agent.get('/p/pads with spaces?showChat=true&noColors=false')
- .expect(302)
- .expect('location', 'pads_with_spaces?showChat=true&noColors=false');
- });
+ it("supports pads with spaces and query, regression test for #4883", async () => {
+ await agent
+ .get("/p/pads with spaces?showChat=true&noColors=false")
+ .expect(302)
+ .expect("location", "pads_with_spaces?showChat=true&noColors=false");
+ });
});
diff --git a/src/tests/backend/specs/promises.ts b/src/tests/backend/specs/promises.ts
index 66be23562..c39f0e017 100644
--- a/src/tests/backend/specs/promises.ts
+++ b/src/tests/backend/specs/promises.ts
@@ -1,92 +1,103 @@
-import {MapArrayType} from "../../../node/types/MapType";
+import { MapArrayType } from "../../../node/types/MapType";
-const assert = require('assert').strict;
-const promises = require('../../../node/utils/promises');
+const assert = require("assert").strict;
+const promises = require("../../../node/utils/promises");
-describe(__filename, function () {
- describe('promises.timesLimit', function () {
- let wantIndex = 0;
+describe(__filename, () => {
+ describe("promises.timesLimit", () => {
+ let wantIndex = 0;
- type TestPromise = {
- promise?: Promise,
- resolve?: () => void,
- }
+ type TestPromise = {
+ promise?: Promise;
+ resolve?: () => void;
+ };
- const testPromises: TestPromise[] = [];
- const makePromise = (index: number) => {
- // Make sure index increases by one each time.
- assert.equal(index, wantIndex++);
- // Save the resolve callback (so the test can trigger resolution)
- // and the promise itself (to wait for resolve to take effect).
- const p:TestPromise = {};
- p.promise = new Promise((resolve) => {
- p.resolve = resolve;
- });
- testPromises.push(p);
- return p.promise;
- };
+ const testPromises: TestPromise[] = [];
+ const makePromise = (index: number) => {
+ // Make sure index increases by one each time.
+ assert.equal(index, wantIndex++);
+ // Save the resolve callback (so the test can trigger resolution)
+ // and the promise itself (to wait for resolve to take effect).
+ const p: TestPromise = {};
+ p.promise = new Promise((resolve) => {
+ p.resolve = resolve;
+ });
+ testPromises.push(p);
+ return p.promise;
+ };
- const total = 11;
- const concurrency = 7;
- const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
+ const total = 11;
+ const concurrency = 7;
+ const timesLimitPromise = promises.timesLimit(
+ total,
+ concurrency,
+ makePromise,
+ );
- it('honors concurrency', async function () {
- assert.equal(wantIndex, concurrency);
- });
+ it("honors concurrency", async () => {
+ assert.equal(wantIndex, concurrency);
+ });
- it('creates another when one completes', async function () {
- const {promise, resolve} = testPromises.shift()!;
- resolve!();
- await promise;
- assert.equal(wantIndex, concurrency + 1);
- });
+ it("creates another when one completes", async () => {
+ const { promise, resolve } = testPromises.shift()!;
+ resolve!();
+ await promise;
+ assert.equal(wantIndex, concurrency + 1);
+ });
- it('creates the expected total number of promises', async function () {
- while (testPromises.length > 0) {
- // Resolve them in random order to ensure that the resolution order doesn't matter.
- const i = Math.floor(Math.random() * Math.floor(testPromises.length));
- const {promise, resolve} = testPromises.splice(i, 1)[0];
- resolve!();
- await promise;
- }
- assert.equal(wantIndex, total);
- });
+ it("creates the expected total number of promises", async () => {
+ while (testPromises.length > 0) {
+ // Resolve them in random order to ensure that the resolution order doesn't matter.
+ const i = Math.floor(Math.random() * Math.floor(testPromises.length));
+ const { promise, resolve } = testPromises.splice(i, 1)[0];
+ resolve!();
+ await promise;
+ }
+ assert.equal(wantIndex, total);
+ });
- it('resolves', async function () {
- await timesLimitPromise;
- });
+ it("resolves", async () => {
+ await timesLimitPromise;
+ });
- it('does not create too many promises if total < concurrency', async function () {
- wantIndex = 0;
- assert.equal(testPromises.length, 0);
- const total = 7;
- const concurrency = 11;
- const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
- while (testPromises.length > 0) {
- const {promise, resolve} = testPromises.pop()!;
- resolve!();
- await promise;
- }
- await timesLimitPromise;
- assert.equal(wantIndex, total);
- });
+ it("does not create too many promises if total < concurrency", async () => {
+ wantIndex = 0;
+ assert.equal(testPromises.length, 0);
+ const total = 7;
+ const concurrency = 11;
+ const timesLimitPromise = promises.timesLimit(
+ total,
+ concurrency,
+ makePromise,
+ );
+ while (testPromises.length > 0) {
+ const { promise, resolve } = testPromises.pop()!;
+ resolve!();
+ await promise;
+ }
+ await timesLimitPromise;
+ assert.equal(wantIndex, total);
+ });
- it('accepts total === 0, concurrency > 0', async function () {
- wantIndex = 0;
- assert.equal(testPromises.length, 0);
- await promises.timesLimit(0, concurrency, makePromise);
- assert.equal(wantIndex, 0);
- });
+ it("accepts total === 0, concurrency > 0", async () => {
+ wantIndex = 0;
+ assert.equal(testPromises.length, 0);
+ await promises.timesLimit(0, concurrency, makePromise);
+ assert.equal(wantIndex, 0);
+ });
- it('accepts total === 0, concurrency === 0', async function () {
- wantIndex = 0;
- assert.equal(testPromises.length, 0);
- await promises.timesLimit(0, 0, makePromise);
- assert.equal(wantIndex, 0);
- });
+ it("accepts total === 0, concurrency === 0", async () => {
+ wantIndex = 0;
+ assert.equal(testPromises.length, 0);
+ await promises.timesLimit(0, 0, makePromise);
+ assert.equal(wantIndex, 0);
+ });
- it('rejects total > 0, concurrency === 0', async function () {
- await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError);
- });
- });
+ it("rejects total > 0, concurrency === 0", async () => {
+ await assert.rejects(
+ promises.timesLimit(total, 0, makePromise),
+ RangeError,
+ );
+ });
+ });
});
diff --git a/src/tests/backend/specs/regression-db.ts b/src/tests/backend/specs/regression-db.ts
index ba50e5240..654f2dc6b 100644
--- a/src/tests/backend/specs/regression-db.ts
+++ b/src/tests/backend/specs/regression-db.ts
@@ -1,30 +1,30 @@
-'use strict';
+const AuthorManager = require("../../../node/db/AuthorManager");
+import { strict as assert } from "assert";
+const common = require("../common");
+const db = require("../../../node/db/DB");
-const AuthorManager = require('../../../node/db/AuthorManager');
-import {strict as assert} from "assert";
-const common = require('../common');
-const db = require('../../../node/db/DB');
+describe(__filename, () => {
+ let setBackup: Function;
-describe(__filename, function () {
- let setBackup: Function;
+ before(async () => {
+ await common.init();
+ setBackup = db.set;
- before(async function () {
- await common.init();
- setBackup = db.set;
+ db.set = async (...args: any) => {
+ // delay db.set
+ await new Promise((resolve) => {
+ setTimeout(() => resolve(), 500);
+ });
+ return await setBackup.call(db, ...args);
+ };
+ });
- db.set = async (...args:any) => {
- // delay db.set
- await new Promise((resolve) => { setTimeout(() => resolve(), 500); });
- return await setBackup.call(db, ...args);
- };
- });
+ after(async () => {
+ db.set = setBackup;
+ });
- after(async function () {
- db.set = setBackup;
- });
-
- it('regression test for missing await in createAuthor (#5000)', async function () {
- const {authorID} = await AuthorManager.createAuthor(); // Should block until db.set() finishes.
- assert(await AuthorManager.doesAuthorExist(authorID));
- });
+ it("regression test for missing await in createAuthor (#5000)", async () => {
+ const { authorID } = await AuthorManager.createAuthor(); // Should block until db.set() finishes.
+ assert(await AuthorManager.doesAuthorExist(authorID));
+ });
});
diff --git a/src/tests/backend/specs/sanitizePathname.ts b/src/tests/backend/specs/sanitizePathname.ts
index fd3cbb2e7..3e52501d0 100644
--- a/src/tests/backend/specs/sanitizePathname.ts
+++ b/src/tests/backend/specs/sanitizePathname.ts
@@ -1,99 +1,101 @@
-'use strict';
+import { strict as assert } from "assert";
+import path from "path";
+const sanitizePathname = require("../../../node/utils/sanitizePathname");
-import {strict as assert} from "assert";
-import path from 'path';
-const sanitizePathname = require('../../../node/utils/sanitizePathname');
+describe(__filename, () => {
+ describe("absolute paths rejected", () => {
+ const testCases = [
+ ["posix", "/"],
+ ["posix", "/foo"],
+ ["win32", "/"],
+ ["win32", "\\"],
+ ["win32", "C:/foo"],
+ ["win32", "C:\\foo"],
+ ["win32", "c:/foo"],
+ ["win32", "c:\\foo"],
+ ["win32", "/foo"],
+ ["win32", "\\foo"],
+ ];
+ for (const [platform, p] of testCases) {
+ it(`${platform} ${p}`, async () => {
+ // @ts-ignore
+ assert.throws(() => sanitizePathname(p, path[platform]), {
+ message: /absolute path/,
+ });
+ });
+ }
+ });
+ describe("directory traversal rejected", () => {
+ const testCases = [
+ ["posix", ".."],
+ ["posix", "../"],
+ ["posix", "../foo"],
+ ["posix", "foo/../.."],
+ ["win32", ".."],
+ ["win32", "../"],
+ ["win32", "..\\"],
+ ["win32", "../foo"],
+ ["win32", "..\\foo"],
+ ["win32", "foo/../.."],
+ ["win32", "foo\\..\\.."],
+ ];
+ for (const [platform, p] of testCases) {
+ it(`${platform} ${p}`, async () => {
+ // @ts-ignore
+ assert.throws(() => sanitizePathname(p, path[platform]), {
+ message: /travers/,
+ });
+ });
+ }
+ });
-describe(__filename, function () {
- describe('absolute paths rejected', function () {
- const testCases = [
- ['posix', '/'],
- ['posix', '/foo'],
- ['win32', '/'],
- ['win32', '\\'],
- ['win32', 'C:/foo'],
- ['win32', 'C:\\foo'],
- ['win32', 'c:/foo'],
- ['win32', 'c:\\foo'],
- ['win32', '/foo'],
- ['win32', '\\foo'],
- ];
- for (const [platform, p] of testCases) {
- it(`${platform} ${p}`, async function () {
- // @ts-ignore
- assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/});
- });
- }
- });
- describe('directory traversal rejected', function () {
- const testCases = [
- ['posix', '..'],
- ['posix', '../'],
- ['posix', '../foo'],
- ['posix', 'foo/../..'],
- ['win32', '..'],
- ['win32', '../'],
- ['win32', '..\\'],
- ['win32', '../foo'],
- ['win32', '..\\foo'],
- ['win32', 'foo/../..'],
- ['win32', 'foo\\..\\..'],
- ];
- for (const [platform, p] of testCases) {
- it(`${platform} ${p}`, async function () {
- // @ts-ignore
- assert.throws(() => sanitizePathname(p, path[platform]), {message: /travers/});
- });
- }
- });
+ describe("accepted paths", () => {
+ const testCases = [
+ ["posix", "", "."],
+ ["posix", "."],
+ ["posix", "./"],
+ ["posix", "foo"],
+ ["posix", "foo/"],
+ ["posix", "foo/bar/..", "foo"],
+ ["posix", "foo/bar/../", "foo/"],
+ ["posix", "./foo", "foo"],
+ ["posix", "foo/bar"],
+ ["posix", "foo\\bar"],
+ ["posix", "\\foo"],
+ ["posix", "..\\foo"],
+ ["posix", "foo/../bar", "bar"],
+ ["posix", "C:/foo"],
+ ["posix", "C:\\foo"],
+ ["win32", "", "."],
+ ["win32", "."],
+ ["win32", "./"],
+ ["win32", ".\\", "./"],
+ ["win32", "foo"],
+ ["win32", "foo/"],
+ ["win32", "foo\\", "foo/"],
+ ["win32", "foo/bar/..", "foo"],
+ ["win32", "foo\\bar\\..", "foo"],
+ ["win32", "foo/bar/../", "foo/"],
+ ["win32", "foo\\bar\\..\\", "foo/"],
+ ["win32", "./foo", "foo"],
+ ["win32", ".\\foo", "foo"],
+ ["win32", "foo/bar"],
+ ["win32", "foo\\bar", "foo/bar"],
+ ["win32", "foo/../bar", "bar"],
+ ["win32", "foo\\..\\bar", "bar"],
+ ["win32", "foo/..\\bar", "bar"],
+ ["win32", "foo\\../bar", "bar"],
+ ];
+ for (const [platform, p, tcWant] of testCases) {
+ const want = tcWant == null ? p : tcWant;
+ it(`${platform} ${p || ""} -> ${want}`, async () => {
+ // @ts-ignore
+ assert.equal(sanitizePathname(p, path[platform]), want);
+ });
+ }
+ });
- describe('accepted paths', function () {
- const testCases = [
- ['posix', '', '.'],
- ['posix', '.'],
- ['posix', './'],
- ['posix', 'foo'],
- ['posix', 'foo/'],
- ['posix', 'foo/bar/..', 'foo'],
- ['posix', 'foo/bar/../', 'foo/'],
- ['posix', './foo', 'foo'],
- ['posix', 'foo/bar'],
- ['posix', 'foo\\bar'],
- ['posix', '\\foo'],
- ['posix', '..\\foo'],
- ['posix', 'foo/../bar', 'bar'],
- ['posix', 'C:/foo'],
- ['posix', 'C:\\foo'],
- ['win32', '', '.'],
- ['win32', '.'],
- ['win32', './'],
- ['win32', '.\\', './'],
- ['win32', 'foo'],
- ['win32', 'foo/'],
- ['win32', 'foo\\', 'foo/'],
- ['win32', 'foo/bar/..', 'foo'],
- ['win32', 'foo\\bar\\..', 'foo'],
- ['win32', 'foo/bar/../', 'foo/'],
- ['win32', 'foo\\bar\\..\\', 'foo/'],
- ['win32', './foo', 'foo'],
- ['win32', '.\\foo', 'foo'],
- ['win32', 'foo/bar'],
- ['win32', 'foo\\bar', 'foo/bar'],
- ['win32', 'foo/../bar', 'bar'],
- ['win32', 'foo\\..\\bar', 'bar'],
- ['win32', 'foo/..\\bar', 'bar'],
- ['win32', 'foo\\../bar', 'bar'],
- ];
- for (const [platform, p, tcWant] of testCases) {
- const want = tcWant == null ? p : tcWant;
- it(`${platform} ${p || ''} -> ${want}`, async function () {
- // @ts-ignore
- assert.equal(sanitizePathname(p, path[platform]), want);
- });
- }
- });
-
- it('default path API', async function () {
- assert.equal(sanitizePathname('foo'), 'foo');
- });
+ it("default path API", async () => {
+ assert.equal(sanitizePathname("foo"), "foo");
+ });
});
diff --git a/src/tests/backend/specs/settings.ts b/src/tests/backend/specs/settings.ts
index d9bfe4f6d..a02140f2f 100644
--- a/src/tests/backend/specs/settings.ts
+++ b/src/tests/backend/specs/settings.ts
@@ -1,92 +1,106 @@
-'use strict';
+const assert = require("assert").strict;
+const { parseSettings } =
+ require("../../../node/utils/Settings").exportedForTestingOnly;
+import path from "path";
+import process from "process";
-const assert = require('assert').strict;
-const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly;
-import path from 'path';
-import process from 'process';
+describe(__filename, () => {
+ describe("parseSettings", () => {
+ let settings: any;
+ const envVarSubstTestCases = [
+ { name: "true", val: "true", var: "SET_VAR_TRUE", want: true },
+ { name: "false", val: "false", var: "SET_VAR_FALSE", want: false },
+ { name: "null", val: "null", var: "SET_VAR_NULL", want: null },
+ {
+ name: "undefined",
+ val: "undefined",
+ var: "SET_VAR_UNDEFINED",
+ want: undefined,
+ },
+ { name: "number", val: "123", var: "SET_VAR_NUMBER", want: 123 },
+ { name: "string", val: "foo", var: "SET_VAR_STRING", want: "foo" },
+ { name: "empty string", val: "", var: "SET_VAR_EMPTY_STRING", want: "" },
+ ];
-describe(__filename, function () {
- describe('parseSettings', function () {
- let settings: any;
- const envVarSubstTestCases = [
- {name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true},
- {name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false},
- {name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null},
- {name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined},
- {name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123},
- {name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'},
- {name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''},
- ];
+ before(async () => {
+ for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val;
+ delete process.env.UNSET_VAR;
+ settings = parseSettings(path.join(__dirname, "settings.json"), true);
+ assert(settings != null);
+ });
- before(async function () {
- for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val;
- delete process.env.UNSET_VAR;
- settings = parseSettings(path.join(__dirname, 'settings.json'), true);
- assert(settings != null);
- });
+ describe("environment variable substitution", () => {
+ describe("set", () => {
+ for (const tc of envVarSubstTestCases) {
+ it(tc.name, async () => {
+ const obj = settings["environment variable substitution"].set;
+ if (tc.name === "undefined") {
+ assert(!(tc.name in obj));
+ } else {
+ assert.equal(obj[tc.name], tc.want);
+ }
+ });
+ }
+ });
- describe('environment variable substitution', function () {
- describe('set', function () {
- for (const tc of envVarSubstTestCases) {
- it(tc.name, async function () {
- const obj = settings['environment variable substitution'].set;
- if (tc.name === 'undefined') {
- assert(!(tc.name in obj));
- } else {
- assert.equal(obj[tc.name], tc.want);
- }
- });
- }
- });
+ describe("unset", () => {
+ it("no default", async () => {
+ const obj = settings["environment variable substitution"].unset;
+ assert.equal(obj["no default"], null);
+ });
- describe('unset', function () {
- it('no default', async function () {
- const obj = settings['environment variable substitution'].unset;
- assert.equal(obj['no default'], null);
- });
+ for (const tc of envVarSubstTestCases) {
+ it(tc.name, async () => {
+ const obj = settings["environment variable substitution"].unset;
+ if (tc.name === "undefined") {
+ assert(!(tc.name in obj));
+ } else {
+ assert.equal(obj[tc.name], tc.want);
+ }
+ });
+ }
+ });
+ });
+ });
- for (const tc of envVarSubstTestCases) {
- it(tc.name, async function () {
- const obj = settings['environment variable substitution'].unset;
- if (tc.name === 'undefined') {
- assert(!(tc.name in obj));
- } else {
- assert.equal(obj[tc.name], tc.want);
- }
- });
- }
- });
- });
- });
+ describe("Parse plugin settings", () => {
+ before(async () => {
+ process.env["EP__ADMIN__PASSWORD"] = "test";
+ });
+ it("should parse plugin settings", async () => {
+ const settings = parseSettings(
+ path.join(__dirname, "settings.json"),
+ true,
+ );
+ assert.equal(settings.ADMIN.PASSWORD, "test");
+ });
- describe("Parse plugin settings", function () {
+ it("should bundle settings with same path", async () => {
+ process.env["EP__ADMIN__USERNAME"] = "test";
+ const settings = parseSettings(
+ path.join(__dirname, "settings.json"),
+ true,
+ );
+ assert.deepEqual(settings.ADMIN, { PASSWORD: "test", USERNAME: "test" });
+ });
- before(async function () {
- process.env["EP__ADMIN__PASSWORD"] = "test"
- })
+ it("Can set the ep themes", async () => {
+ process.env["EP__ep_themes__default_theme"] = "hacker";
+ const settings = parseSettings(
+ path.join(__dirname, "settings.json"),
+ true,
+ );
+ assert.deepEqual(settings.ep_themes, { default_theme: "hacker" });
+ });
- it('should parse plugin settings', async function () {
- let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
- assert.equal(settings.ADMIN.PASSWORD, "test");
- })
-
- it('should bundle settings with same path', async function () {
- process.env["EP__ADMIN__USERNAME"] = "test"
- let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
- assert.deepEqual(settings.ADMIN, {PASSWORD: "test", USERNAME: "test"});
- })
-
- it("Can set the ep themes", async function () {
- process.env["EP__ep_themes__default_theme"] = "hacker"
- let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
- assert.deepEqual(settings.ep_themes, {"default_theme": "hacker"});
- })
-
- it("can set the ep_webrtc settings", async function () {
- process.env["EP__ep_webrtc__enabled"] = "true"
- let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
- assert.deepEqual(settings.ep_webrtc, {"enabled": true});
- })
- })
+ it("can set the ep_webrtc settings", async () => {
+ process.env["EP__ep_webrtc__enabled"] = "true";
+ const settings = parseSettings(
+ path.join(__dirname, "settings.json"),
+ true,
+ );
+ assert.deepEqual(settings.ep_webrtc, { enabled: true });
+ });
+ });
});
diff --git a/src/tests/backend/specs/socketio.ts b/src/tests/backend/specs/socketio.ts
index cde554e5e..844378523 100644
--- a/src/tests/backend/specs/socketio.ts
+++ b/src/tests/backend/specs/socketio.ts
@@ -1,434 +1,568 @@
-'use strict';
+import type { MapArrayType } from "../../../node/types/MapType";
-import {MapArrayType} from "../../../node/types/MapType";
-
-const assert = require('assert').strict;
-const common = require('../common');
-const padManager = require('../../../node/db/PadManager');
-const plugins = require('../../../static/js/pluginfw/plugin_defs');
-const readOnlyManager = require('../../../node/db/ReadOnlyManager');
-const settings = require('../../../node/utils/Settings');
-const socketIoRouter = require('../../../node/handler/SocketIORouter');
+const assert = require("assert").strict;
+const common = require("../common");
+const padManager = require("../../../node/db/PadManager");
+const plugins = require("../../../static/js/pluginfw/plugin_defs");
+const readOnlyManager = require("../../../node/db/ReadOnlyManager");
+const settings = require("../../../node/utils/Settings");
+const socketIoRouter = require("../../../node/handler/SocketIORouter");
describe(__filename, function () {
- this.timeout(30000);
- let agent: any;
- let authorize:Function;
- const backups:MapArrayType = {};
- const cleanUpPads = async () => {
- const padIds = ['pad', 'other-pad', 'päd'];
- await Promise.all(padIds.map(async (padId) => {
- if (await padManager.doesPadExist(padId)) {
- const pad = await padManager.getPad(padId);
- await pad.remove();
- }
- }));
- };
- let socket:any;
+ this.timeout(30000);
+ let agent: any;
+ let authorize: Function;
+ const backups: MapArrayType = {};
+ const cleanUpPads = async () => {
+ const padIds = ["pad", "other-pad", "päd"];
+ await Promise.all(
+ padIds.map(async (padId) => {
+ if (await padManager.doesPadExist(padId)) {
+ const pad = await padManager.getPad(padId);
+ await pad.remove();
+ }
+ }),
+ );
+ };
+ let socket: any;
- before(async function () { agent = await common.init(); });
- beforeEach(async function () {
- backups.hooks = {};
- for (const hookName of ['preAuthorize', 'authenticate', 'authorize']) {
- backups.hooks[hookName] = plugins.hooks[hookName];
- plugins.hooks[hookName] = [];
- }
- backups.settings = {};
- for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users']) {
- backups.settings[setting] = settings[setting];
- }
- settings.editOnly = false;
- settings.requireAuthentication = false;
- settings.requireAuthorization = false;
- settings.users = {
- admin: {password: 'admin-password', is_admin: true},
- user: {password: 'user-password'},
- };
- assert(socket == null);
- authorize = () => true;
- plugins.hooks.authorize = [{hook_fn: (hookName: string, {req}:any, cb:Function) => cb([authorize(req)])}];
- await cleanUpPads();
- });
- afterEach(async function () {
- if (socket) socket.close();
- socket = null;
- await cleanUpPads();
- Object.assign(plugins.hooks, backups.hooks);
- Object.assign(settings, backups.settings);
- });
+ before(async () => {
+ agent = await common.init();
+ });
+ beforeEach(async () => {
+ backups.hooks = {};
+ for (const hookName of ["preAuthorize", "authenticate", "authorize"]) {
+ backups.hooks[hookName] = plugins.hooks[hookName];
+ plugins.hooks[hookName] = [];
+ }
+ backups.settings = {};
+ for (const setting of [
+ "editOnly",
+ "requireAuthentication",
+ "requireAuthorization",
+ "users",
+ ]) {
+ backups.settings[setting] = settings[setting];
+ }
+ settings.editOnly = false;
+ settings.requireAuthentication = false;
+ settings.requireAuthorization = false;
+ settings.users = {
+ admin: { password: "admin-password", is_admin: true },
+ user: { password: "user-password" },
+ };
+ assert(socket == null);
+ authorize = () => true;
+ plugins.hooks.authorize = [
+ {
+ hook_fn: (hookName: string, { req }: any, cb: Function) =>
+ cb([authorize(req)]),
+ },
+ ];
+ await cleanUpPads();
+ });
+ afterEach(async () => {
+ if (socket) socket.close();
+ socket = null;
+ await cleanUpPads();
+ Object.assign(plugins.hooks, backups.hooks);
+ Object.assign(settings, backups.settings);
+ });
- describe('Normal accesses', function () {
- it('!authn anonymous cookie /p/pad -> 200, ok', async function () {
- const res = await agent.get('/p/pad').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- });
- it('!authn !cookie -> ok', async function () {
- socket = await common.connect(null);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- });
- it('!authn user /p/pad -> 200, ok', async function () {
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- });
- it('authn user /p/pad -> 200, ok', async function () {
- settings.requireAuthentication = true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- });
+ describe("Normal accesses", () => {
+ it("!authn anonymous cookie /p/pad -> 200, ok", async () => {
+ const res = await agent.get("/p/pad").expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ });
+ it("!authn !cookie -> ok", async () => {
+ socket = await common.connect(null);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ });
+ it("!authn user /p/pad -> 200, ok", async () => {
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ });
+ it("authn user /p/pad -> 200, ok", async () => {
+ settings.requireAuthentication = true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ });
- for (const authn of [false, true]) {
- const desc = authn ? 'authn user' : '!authn anonymous';
- it(`${desc} read-only /p/pad -> 200, ok`, async function () {
- const get = (ep: string) => {
- let res = agent.get(ep);
- if (authn) res = res.auth('user', 'user-password');
- return res.expect(200);
- };
- settings.requireAuthentication = authn;
- let res = await get('/p/pad');
- socket = await common.connect(res);
- let clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, false);
- const readOnlyId = clientVars.data.readOnlyId;
- assert(readOnlyManager.isReadOnlyId(readOnlyId));
- socket.close();
- res = await get(`/p/${readOnlyId}`);
- socket = await common.connect(res);
- clientVars = await common.handshake(socket, readOnlyId);
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, true);
- });
- }
+ for (const authn of [false, true]) {
+ const desc = authn ? "authn user" : "!authn anonymous";
+ it(`${desc} read-only /p/pad -> 200, ok`, async () => {
+ const get = (ep: string) => {
+ let res = agent.get(ep);
+ if (authn) res = res.auth("user", "user-password");
+ return res.expect(200);
+ };
+ settings.requireAuthentication = authn;
+ let res = await get("/p/pad");
+ socket = await common.connect(res);
+ let clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, false);
+ const readOnlyId = clientVars.data.readOnlyId;
+ assert(readOnlyManager.isReadOnlyId(readOnlyId));
+ socket.close();
+ res = await get(`/p/${readOnlyId}`);
+ socket = await common.connect(res);
+ clientVars = await common.handshake(socket, readOnlyId);
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, true);
+ });
+ }
- it('authz user /p/pad -> 200, ok', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- });
- it('supports pad names with characters that must be percent-encoded', async function () {
- settings.requireAuthentication = true;
- // requireAuthorization is set to true here to guarantee that the user's padAuthorizations
- // object is populated. Technically this isn't necessary because the user's padAuthorizations
- // is currently populated even if requireAuthorization is false, but setting this to true
- // ensures the test remains useful if the implementation ever changes.
- settings.requireAuthorization = true;
- const encodedPadId = encodeURIComponent('päd');
- const res = await agent.get(`/p/${encodedPadId}`).auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'päd');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- });
- });
+ it("authz user /p/pad -> 200, ok", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ });
+ it("supports pad names with characters that must be percent-encoded", async () => {
+ settings.requireAuthentication = true;
+ // requireAuthorization is set to true here to guarantee that the user's padAuthorizations
+ // object is populated. Technically this isn't necessary because the user's padAuthorizations
+ // is currently populated even if requireAuthorization is false, but setting this to true
+ // ensures the test remains useful if the implementation ever changes.
+ settings.requireAuthorization = true;
+ const encodedPadId = encodeURIComponent("päd");
+ const res = await agent
+ .get(`/p/${encodedPadId}`)
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "päd");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ });
+ });
- describe('Abnormal access attempts', function () {
- it('authn anonymous /p/pad -> 401, error', async function () {
- settings.requireAuthentication = true;
- const res = await agent.get('/p/pad').expect(401);
- // Despite the 401, try to create the pad via a socket.io connection anyway.
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
+ describe("Abnormal access attempts", () => {
+ it("authn anonymous /p/pad -> 401, error", async () => {
+ settings.requireAuthentication = true;
+ const res = await agent.get("/p/pad").expect(401);
+ // Despite the 401, try to create the pad via a socket.io connection anyway.
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
- it('authn anonymous read-only /p/pad -> 401, error', async function () {
- settings.requireAuthentication = true;
- let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- const readOnlyId = clientVars.data.readOnlyId;
- assert(readOnlyManager.isReadOnlyId(readOnlyId));
- socket.close();
- res = await agent.get(`/p/${readOnlyId}`).expect(401);
- // Despite the 401, try to read the pad via a socket.io connection anyway.
- socket = await common.connect(res);
- const message = await common.handshake(socket, readOnlyId);
- assert.equal(message.accessStatus, 'deny');
- });
+ it("authn anonymous read-only /p/pad -> 401, error", async () => {
+ settings.requireAuthentication = true;
+ let res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ const readOnlyId = clientVars.data.readOnlyId;
+ assert(readOnlyManager.isReadOnlyId(readOnlyId));
+ socket.close();
+ res = await agent.get(`/p/${readOnlyId}`).expect(401);
+ // Despite the 401, try to read the pad via a socket.io connection anyway.
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, readOnlyId);
+ assert.equal(message.accessStatus, "deny");
+ });
- it('authn !cookie -> error', async function () {
- settings.requireAuthentication = true;
- socket = await common.connect(null);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- it('authorization bypass attempt -> error', async function () {
- // Only allowed to access /p/pad.
- authorize = (req:{
- path: string,
- }) => req.path === '/p/pad';
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- // First authenticate and establish a session.
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
- const message = await common.handshake(socket, 'other-pad');
- assert.equal(message.accessStatus, 'deny');
- });
- });
+ it("authn !cookie -> error", async () => {
+ settings.requireAuthentication = true;
+ socket = await common.connect(null);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ it("authorization bypass attempt -> error", async () => {
+ // Only allowed to access /p/pad.
+ authorize = (req: {
+ path: string;
+ }) => req.path === "/p/pad";
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ // First authenticate and establish a session.
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ // Accessing /p/other-pad should fail, despite the successful fetch of /p/pad.
+ const message = await common.handshake(socket, "other-pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ });
- describe('Authorization levels via authorize hook', function () {
- beforeEach(async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- });
+ describe("Authorization levels via authorize hook", () => {
+ beforeEach(async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ });
- it("level='create' -> can create", async function () {
- authorize = () => 'create';
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, false);
- });
- it('level=true -> can create', async function () {
- authorize = () => true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, false);
- });
- it("level='modify' -> can modify", async function () {
- await padManager.getPad('pad'); // Create the pad.
- authorize = () => 'modify';
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, false);
- });
- it("level='create' settings.editOnly=true -> unable to create", async function () {
- authorize = () => 'create';
- settings.editOnly = true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- it("level='modify' settings.editOnly=false -> unable to create", async function () {
- authorize = () => 'modify';
- settings.editOnly = false;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- it("level='readOnly' -> unable to create", async function () {
- authorize = () => 'readOnly';
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- it("level='readOnly' -> unable to modify", async function () {
- await padManager.getPad('pad'); // Create the pad.
- authorize = () => 'readOnly';
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, true);
- });
- });
+ it("level='create' -> can create", async () => {
+ authorize = () => "create";
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, false);
+ });
+ it("level=true -> can create", async () => {
+ authorize = () => true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, false);
+ });
+ it("level='modify' -> can modify", async () => {
+ await padManager.getPad("pad"); // Create the pad.
+ authorize = () => "modify";
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, false);
+ });
+ it("level='create' settings.editOnly=true -> unable to create", async () => {
+ authorize = () => "create";
+ settings.editOnly = true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ it("level='modify' settings.editOnly=false -> unable to create", async () => {
+ authorize = () => "modify";
+ settings.editOnly = false;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ it("level='readOnly' -> unable to create", async () => {
+ authorize = () => "readOnly";
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ it("level='readOnly' -> unable to modify", async () => {
+ await padManager.getPad("pad"); // Create the pad.
+ authorize = () => "readOnly";
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, true);
+ });
+ });
- describe('Authorization levels via user settings', function () {
- beforeEach(async function () {
- settings.requireAuthentication = true;
- });
+ describe("Authorization levels via user settings", () => {
+ beforeEach(async () => {
+ settings.requireAuthentication = true;
+ });
- it('user.canCreate = true -> can create and modify', async function () {
- settings.users.user.canCreate = true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, false);
- });
- it('user.canCreate = false -> unable to create', async function () {
- settings.users.user.canCreate = false;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- it('user.readOnly = true -> unable to create', async function () {
- settings.users.user.readOnly = true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- it('user.readOnly = true -> unable to modify', async function () {
- await padManager.getPad('pad'); // Create the pad.
- settings.users.user.readOnly = true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, true);
- });
- it('user.readOnly = false -> can create and modify', async function () {
- settings.users.user.readOnly = false;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const clientVars = await common.handshake(socket, 'pad');
- assert.equal(clientVars.type, 'CLIENT_VARS');
- assert.equal(clientVars.data.readonly, false);
- });
- it('user.readOnly = true, user.canCreate = true -> unable to create', async function () {
- settings.users.user.canCreate = true;
- settings.users.user.readOnly = true;
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- });
+ it("user.canCreate = true -> can create and modify", async () => {
+ settings.users.user.canCreate = true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, false);
+ });
+ it("user.canCreate = false -> unable to create", async () => {
+ settings.users.user.canCreate = false;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ it("user.readOnly = true -> unable to create", async () => {
+ settings.users.user.readOnly = true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ it("user.readOnly = true -> unable to modify", async () => {
+ await padManager.getPad("pad"); // Create the pad.
+ settings.users.user.readOnly = true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, true);
+ });
+ it("user.readOnly = false -> can create and modify", async () => {
+ settings.users.user.readOnly = false;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const clientVars = await common.handshake(socket, "pad");
+ assert.equal(clientVars.type, "CLIENT_VARS");
+ assert.equal(clientVars.data.readonly, false);
+ });
+ it("user.readOnly = true, user.canCreate = true -> unable to create", async () => {
+ settings.users.user.canCreate = true;
+ settings.users.user.readOnly = true;
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ });
- describe('Authorization level interaction between authorize hook and user settings', function () {
- beforeEach(async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- });
+ describe("Authorization level interaction between authorize hook and user settings", () => {
+ beforeEach(async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ });
- it('authorize hook does not elevate level from user settings', async function () {
- settings.users.user.readOnly = true;
- authorize = () => 'create';
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- it('user settings does not elevate level from authorize hook', async function () {
- settings.users.user.readOnly = false;
- settings.users.user.canCreate = true;
- authorize = () => 'readOnly';
- const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
- socket = await common.connect(res);
- const message = await common.handshake(socket, 'pad');
- assert.equal(message.accessStatus, 'deny');
- });
- });
+ it("authorize hook does not elevate level from user settings", async () => {
+ settings.users.user.readOnly = true;
+ authorize = () => "create";
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ it("user settings does not elevate level from authorize hook", async () => {
+ settings.users.user.readOnly = false;
+ settings.users.user.canCreate = true;
+ authorize = () => "readOnly";
+ const res = await agent
+ .get("/p/pad")
+ .auth("user", "user-password")
+ .expect(200);
+ socket = await common.connect(res);
+ const message = await common.handshake(socket, "pad");
+ assert.equal(message.accessStatus, "deny");
+ });
+ });
- describe('SocketIORouter.js', function () {
- const Module = class {
- setSocketIO(io:any) {}
- handleConnect(socket:any) {}
- handleDisconnect(socket:any) {}
- handleMessage(socket:any, message:string) {}
- };
+ describe("SocketIORouter.js", () => {
+ const Module = class {
+ setSocketIO(io: any) {}
+ handleConnect(socket: any) {}
+ handleDisconnect(socket: any) {}
+ handleMessage(socket: any, message: string) {}
+ };
- afterEach(async function () {
- socketIoRouter.deleteComponent(this.test!.fullTitle());
- socketIoRouter.deleteComponent(`${this.test!.fullTitle()} #2`);
- });
+ afterEach(async function () {
+ socketIoRouter.deleteComponent(this.test!.fullTitle());
+ socketIoRouter.deleteComponent(`${this.test!.fullTitle()} #2`);
+ });
- it('setSocketIO', async function () {
- let ioServer;
- socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module {
- setSocketIO(io:any) { ioServer = io; }
- }());
- assert(ioServer != null);
- });
+ it("setSocketIO", async function () {
+ let ioServer;
+ socketIoRouter.addComponent(
+ this.test!.fullTitle(),
+ new (class extends Module {
+ setSocketIO(io: any) {
+ ioServer = io;
+ }
+ })(),
+ );
+ assert(ioServer != null);
+ });
- it('handleConnect', async function () {
- let serverSocket;
- socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module {
- handleConnect(socket:any) { serverSocket = socket; }
- }());
- socket = await common.connect();
- assert(serverSocket != null);
- });
+ it("handleConnect", async function () {
+ let serverSocket;
+ socketIoRouter.addComponent(
+ this.test!.fullTitle(),
+ new (class extends Module {
+ handleConnect(socket: any) {
+ serverSocket = socket;
+ }
+ })(),
+ );
+ socket = await common.connect();
+ assert(serverSocket != null);
+ });
- it('handleDisconnect', async function () {
- let resolveConnected: (value: void | PromiseLike) => void ;
- const connected = new Promise((resolve) => resolveConnected = resolve);
- let resolveDisconnected: (value: void | PromiseLike) => void ;
- const disconnected = new Promise((resolve) => resolveDisconnected = resolve);
- socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module {
- private _socket: any;
- handleConnect(socket:any) {
- this._socket = socket;
- resolveConnected();
- }
- handleDisconnect(socket:any) {
- assert(socket != null);
- // There might be lingering disconnect events from sockets created by other tests.
- if (this._socket == null || socket.id !== this._socket.id) return;
- assert.equal(socket, this._socket);
- resolveDisconnected();
- }
- }());
- socket = await common.connect();
- await connected;
- socket.close();
- socket = null;
- await disconnected;
- });
+ it("handleDisconnect", async function () {
+ let resolveConnected: (value: void | PromiseLike) => void;
+ const connected = new Promise((resolve) => (resolveConnected = resolve));
+ let resolveDisconnected: (value: void | PromiseLike) => void;
+ const disconnected = new Promise(
+ (resolve) => (resolveDisconnected = resolve),
+ );
+ socketIoRouter.addComponent(
+ this.test!.fullTitle(),
+ new (class extends Module {
+ private _socket: any;
+ handleConnect(socket: any) {
+ this._socket = socket;
+ resolveConnected();
+ }
+ handleDisconnect(socket: any) {
+ assert(socket != null);
+ // There might be lingering disconnect events from sockets created by other tests.
+ if (this._socket == null || socket.id !== this._socket.id) return;
+ assert.equal(socket, this._socket);
+ resolveDisconnected();
+ }
+ })(),
+ );
+ socket = await common.connect();
+ await connected;
+ socket.close();
+ socket = null;
+ await disconnected;
+ });
- it('handleMessage (success)', async function () {
- let serverSocket:any;
- const want = {
- component: this.test!.fullTitle(),
- foo: {bar: 'asdf'},
- };
- let rx:Function;
- const got = new Promise((resolve) => { rx = resolve; });
- socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module {
- handleConnect(socket:any) { serverSocket = socket; }
- handleMessage(socket:any, message:string) { assert.equal(socket, serverSocket); rx(message); }
- }());
- socketIoRouter.addComponent(`${this.test!.fullTitle()} #2`, new class extends Module {
- handleMessage(socket:any, message:any) { assert.fail('wrong handler called'); }
- }());
- socket = await common.connect();
- socket.emit('message', want);
- assert.deepEqual(await got, want);
- });
+ it("handleMessage (success)", async function () {
+ let serverSocket: any;
+ const want = {
+ component: this.test!.fullTitle(),
+ foo: { bar: "asdf" },
+ };
+ let rx: Function;
+ const got = new Promise((resolve) => {
+ rx = resolve;
+ });
+ socketIoRouter.addComponent(
+ this.test!.fullTitle(),
+ new (class extends Module {
+ handleConnect(socket: any) {
+ serverSocket = socket;
+ }
+ handleMessage(socket: any, message: string) {
+ assert.equal(socket, serverSocket);
+ rx(message);
+ }
+ })(),
+ );
+ socketIoRouter.addComponent(
+ `${this.test!.fullTitle()} #2`,
+ new (class extends Module {
+ handleMessage(socket: any, message: any) {
+ assert.fail("wrong handler called");
+ }
+ })(),
+ );
+ socket = await common.connect();
+ socket.emit("message", want);
+ assert.deepEqual(await got, want);
+ });
- const tx = async (socket:any, message = {}) => await new Promise((resolve, reject) => {
- const AckErr = class extends Error {
- constructor(name: string, ...args:any) { super(...args); this.name = name; }
- };
- socket.emit('message', message,
- (errj: {
- message: string,
- name: string,
- }, val: any) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val));
- });
+ const tx = async (socket: any, message = {}) =>
+ await new Promise((resolve, reject) => {
+ const AckErr = class extends Error {
+ constructor(name: string, ...args: any) {
+ super(...args);
+ this.name = name;
+ }
+ };
+ socket.emit(
+ "message",
+ message,
+ (
+ errj: {
+ message: string;
+ name: string;
+ },
+ val: any,
+ ) =>
+ errj != null
+ ? reject(new AckErr(errj.name, errj.message))
+ : resolve(val),
+ );
+ });
- it('handleMessage with ack (success)', async function () {
- const want = 'value';
- socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module {
- handleMessage(socket:any, msg:any) { return want; }
- }());
- socket = await common.connect();
- const got = await tx(socket, {component: this.test!.fullTitle()});
- assert.equal(got, want);
- });
+ it("handleMessage with ack (success)", async function () {
+ const want = "value";
+ socketIoRouter.addComponent(
+ this.test!.fullTitle(),
+ new (class extends Module {
+ handleMessage(socket: any, msg: any) {
+ return want;
+ }
+ })(),
+ );
+ socket = await common.connect();
+ const got = await tx(socket, { component: this.test!.fullTitle() });
+ assert.equal(got, want);
+ });
- it('handleMessage with ack (error)', async function () {
- const InjectedError = class extends Error {
- constructor() { super('injected test error'); this.name = 'InjectedError'; }
- };
- socketIoRouter.addComponent(this.test!.fullTitle(), new class extends Module {
- handleMessage(socket:any, msg:any) { throw new InjectedError(); }
- }());
- socket = await common.connect();
- await assert.rejects(tx(socket, {component: this.test!.fullTitle()}), new InjectedError());
- });
- });
+ it("handleMessage with ack (error)", async function () {
+ const InjectedError = class extends Error {
+ constructor() {
+ super("injected test error");
+ this.name = "InjectedError";
+ }
+ };
+ socketIoRouter.addComponent(
+ this.test!.fullTitle(),
+ new (class extends Module {
+ handleMessage(socket: any, msg: any) {
+ throw new InjectedError();
+ }
+ })(),
+ );
+ socket = await common.connect();
+ await assert.rejects(
+ tx(socket, { component: this.test!.fullTitle() }),
+ new InjectedError(),
+ );
+ });
+ });
});
diff --git a/src/tests/backend/specs/specialpages.ts b/src/tests/backend/specs/specialpages.ts
index fbb446c49..0fb4ab007 100644
--- a/src/tests/backend/specs/specialpages.ts
+++ b/src/tests/backend/specs/specialpages.ts
@@ -1,32 +1,30 @@
-'use strict';
-
-import {MapArrayType} from "../../../node/types/MapType";
-
-const common = require('../common');
-const settings = require('../../../node/utils/Settings');
-
+import type { MapArrayType } from "../../../node/types/MapType";
+const common = require("../common");
+const settings = require("../../../node/utils/Settings");
describe(__filename, function () {
- this.timeout(30000);
- let agent:any;
- const backups:MapArrayType = {};
- before(async function () { agent = await common.init(); });
- beforeEach(async function () {
- backups.settings = {};
- for (const setting of ['requireAuthentication', 'requireAuthorization']) {
- backups.settings[setting] = settings[setting];
- }
- settings.requireAuthentication = false;
- settings.requireAuthorization = false;
- });
- afterEach(async function () {
- Object.assign(settings, backups.settings);
- });
+ this.timeout(30000);
+ let agent: any;
+ const backups: MapArrayType = {};
+ before(async () => {
+ agent = await common.init();
+ });
+ beforeEach(async () => {
+ backups.settings = {};
+ for (const setting of ["requireAuthentication", "requireAuthorization"]) {
+ backups.settings[setting] = settings[setting];
+ }
+ settings.requireAuthentication = false;
+ settings.requireAuthorization = false;
+ });
+ afterEach(async () => {
+ Object.assign(settings, backups.settings);
+ });
- describe('/javascript', function () {
- it('/javascript -> 200', async function () {
- await agent.get('/javascript').expect(200);
- });
- });
+ describe("/javascript", () => {
+ it("/javascript -> 200", async () => {
+ await agent.get("/javascript").expect(200);
+ });
+ });
});
diff --git a/src/tests/backend/specs/webaccess.ts b/src/tests/backend/specs/webaccess.ts
index 96c2265fc..1527b9a64 100644
--- a/src/tests/backend/specs/webaccess.ts
+++ b/src/tests/backend/specs/webaccess.ts
@@ -1,562 +1,675 @@
-'use strict';
+import { Func } from "mocha";
+import type { MapArrayType } from "../../../node/types/MapType";
+import type { SettingsUser } from "../../../node/types/SettingsUser";
-import {MapArrayType} from "../../../node/types/MapType";
-import {Func} from "mocha";
-import {SettingsUser} from "../../../node/types/SettingsUser";
-
-const assert = require('assert').strict;
-const common = require('../common');
-const plugins = require('../../../static/js/pluginfw/plugin_defs');
-const settings = require('../../../node/utils/Settings');
+const assert = require("assert").strict;
+const common = require("../common");
+const plugins = require("../../../static/js/pluginfw/plugin_defs");
+const settings = require("../../../node/utils/Settings");
describe(__filename, function () {
- this.timeout(30000);
- let agent:any;
- const backups:MapArrayType = {};
- const authHookNames = ['preAuthorize', 'authenticate', 'authorize'];
- const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure'];
- const makeHook = (hookName: string, hookFn:Function) => ({
- hook_fn: hookFn,
- hook_fn_name: `fake_plugin/${hookName}`,
- hook_name: hookName,
- part: {plugin: 'fake_plugin'},
- });
+ this.timeout(30000);
+ let agent: any;
+ const backups: MapArrayType = {};
+ const authHookNames = ["preAuthorize", "authenticate", "authorize"];
+ const failHookNames = [
+ "preAuthzFailure",
+ "authnFailure",
+ "authzFailure",
+ "authFailure",
+ ];
+ const makeHook = (hookName: string, hookFn: Function) => ({
+ hook_fn: hookFn,
+ hook_fn_name: `fake_plugin/${hookName}`,
+ hook_name: hookName,
+ part: { plugin: "fake_plugin" },
+ });
- before(async function () { agent = await common.init(); });
+ before(async () => {
+ agent = await common.init();
+ });
- beforeEach(async function () {
- backups.hooks = {};
- for (const hookName of authHookNames.concat(failHookNames)) {
- backups.hooks[hookName] = plugins.hooks[hookName];
- plugins.hooks[hookName] = [];
- }
- backups.settings = {};
- for (const setting of ['requireAuthentication', 'requireAuthorization', 'users']) {
- backups.settings[setting] = settings[setting];
- }
- settings.requireAuthentication = false;
- settings.requireAuthorization = false;
- settings.users = {
- admin: {password: 'admin-password', is_admin: true},
- user: {password: 'user-password'},
- } satisfies SettingsUser;
- });
+ beforeEach(async () => {
+ backups.hooks = {};
+ for (const hookName of authHookNames.concat(failHookNames)) {
+ backups.hooks[hookName] = plugins.hooks[hookName];
+ plugins.hooks[hookName] = [];
+ }
+ backups.settings = {};
+ for (const setting of [
+ "requireAuthentication",
+ "requireAuthorization",
+ "users",
+ ]) {
+ backups.settings[setting] = settings[setting];
+ }
+ settings.requireAuthentication = false;
+ settings.requireAuthorization = false;
+ settings.users = {
+ admin: { password: "admin-password", is_admin: true },
+ user: { password: "user-password" },
+ } satisfies SettingsUser;
+ });
- afterEach(async function () {
- Object.assign(plugins.hooks, backups.hooks);
- Object.assign(settings, backups.settings);
- });
+ afterEach(async () => {
+ Object.assign(plugins.hooks, backups.hooks);
+ Object.assign(settings, backups.settings);
+ });
- describe('webaccess: without plugins', function () {
- it('!authn !authz anonymous / -> 200', async function () {
- settings.requireAuthentication = false;
- settings.requireAuthorization = false;
- await agent.get('/').expect(200);
- });
+ describe("webaccess: without plugins", () => {
+ it("!authn !authz anonymous / -> 200", async () => {
+ settings.requireAuthentication = false;
+ settings.requireAuthorization = false;
+ await agent.get("/").expect(200);
+ });
- it('!authn !authz anonymous /admin-auth// -> 401', async function () {
- settings.requireAuthentication = false;
- settings.requireAuthorization = false;
- await agent.get('/admin-auth/').expect(401);
- });
+ it("!authn !authz anonymous /admin-auth// -> 401", async () => {
+ settings.requireAuthentication = false;
+ settings.requireAuthorization = false;
+ await agent.get("/admin-auth/").expect(401);
+ });
- it('authn !authz anonymous / -> 401', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = false;
- await agent.get('/').expect(401);
- });
+ it("authn !authz anonymous / -> 401", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = false;
+ await agent.get("/").expect(401);
+ });
- it('authn !authz user / -> 200', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = false;
- await agent.get('/').auth('user', 'user-password').expect(200);
- });
+ it("authn !authz user / -> 200", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = false;
+ await agent.get("/").auth("user", "user-password").expect(200);
+ });
- it('authn !authz user //admin-auth// -> 403', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = false;
- await agent.get('/admin-auth//').auth('user', 'user-password').expect(403);
- });
+ it("authn !authz user //admin-auth// -> 403", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = false;
+ await agent
+ .get("/admin-auth//")
+ .auth("user", "user-password")
+ .expect(403);
+ });
- it('authn !authz admin / -> 200', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = false;
- await agent.get('/').auth('admin', 'admin-password').expect(200);
- });
+ it("authn !authz admin / -> 200", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = false;
+ await agent.get("/").auth("admin", "admin-password").expect(200);
+ });
- it('authn !authz admin /admin-auth/ -> 200', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = false;
- await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200);
- });
+ it("authn !authz admin /admin-auth/ -> 200", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = false;
+ await agent
+ .get("/admin-auth/")
+ .auth("admin", "admin-password")
+ .expect(200);
+ });
- it('authn authz anonymous /robots.txt -> 200', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- await agent.get('/robots.txt').expect(200);
- });
+ it("authn authz anonymous /robots.txt -> 200", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ await agent.get("/robots.txt").expect(200);
+ });
- it('authn authz user / -> 403', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- await agent.get('/').auth('user', 'user-password').expect(403);
- });
+ it("authn authz user / -> 403", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ await agent.get("/").auth("user", "user-password").expect(403);
+ });
- it('authn authz user //admin-auth// -> 403', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- await agent.get('/admin-auth//').auth('user', 'user-password').expect(403);
- });
+ it("authn authz user //admin-auth// -> 403", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ await agent
+ .get("/admin-auth//")
+ .auth("user", "user-password")
+ .expect(403);
+ });
- it('authn authz admin / -> 200', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- await agent.get('/').auth('admin', 'admin-password').expect(200);
- });
+ it("authn authz admin / -> 200", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ await agent.get("/").auth("admin", "admin-password").expect(200);
+ });
- it('authn authz admin /admin-auth/ -> 200', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200);
- });
+ it("authn authz admin /admin-auth/ -> 200", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ await agent
+ .get("/admin-auth/")
+ .auth("admin", "admin-password")
+ .expect(200);
+ });
- describe('login fails if password is nullish', function () {
- for (const adminPassword of [undefined, null]) {
- // https://tools.ietf.org/html/rfc7617 says that the username and password are sent as
- // base64(username + ':' + password), but there's nothing stopping a malicious user from
- // sending just base64(username) (no colon). The lack of colon could throw off credential
- // parsing, resulting in successful comparisons against a null or undefined password.
- for (const creds of ['admin', 'admin:']) {
- it(`admin password: ${adminPassword} credentials: ${creds}`, async function () {
- settings.users.admin.password = adminPassword;
- const encCreds = Buffer.from(creds).toString('base64');
- await agent.get('/admin-auth/').set('Authorization', `Basic ${encCreds}`).expect(401);
- });
- }
- }
- });
- });
+ describe("login fails if password is nullish", () => {
+ for (const adminPassword of [undefined, null]) {
+ // https://tools.ietf.org/html/rfc7617 says that the username and password are sent as
+ // base64(username + ':' + password), but there's nothing stopping a malicious user from
+ // sending just base64(username) (no colon). The lack of colon could throw off credential
+ // parsing, resulting in successful comparisons against a null or undefined password.
+ for (const creds of ["admin", "admin:"]) {
+ it(`admin password: ${adminPassword} credentials: ${creds}`, async () => {
+ settings.users.admin.password = adminPassword;
+ const encCreds = Buffer.from(creds).toString("base64");
+ await agent
+ .get("/admin-auth/")
+ .set("Authorization", `Basic ${encCreds}`)
+ .expect(401);
+ });
+ }
+ }
+ });
+ });
- describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () {
- let callOrder:string[];
- const Handler = class {
- private called: boolean;
- private readonly hookName: string;
- private readonly innerHandle: Function;
- private readonly id: string;
- private readonly checkContext: Function;
- constructor(hookName:string, suffix: string) {
- this.called = false;
- this.hookName = hookName;
- this.innerHandle = () => [];
- this.id = hookName + suffix;
- this.checkContext = () => {};
- }
- handle(hookName: string, context: any, cb:Function) {
- assert.equal(hookName, this.hookName);
- assert(context != null);
- assert(context.req != null);
- assert(context.res != null);
- assert(context.next != null);
- this.checkContext(context);
- assert(!this.called);
- this.called = true;
- callOrder.push(this.id);
- return cb(this.innerHandle(context));
- }
- };
- const handlers:MapArrayType = {};
+ describe("webaccess: preAuthorize, authenticate, and authorize hooks", () => {
+ let callOrder: string[];
+ const Handler = class {
+ private called: boolean;
+ private readonly hookName: string;
+ private readonly innerHandle: Function;
+ private readonly id: string;
+ private readonly checkContext: Function;
+ constructor(hookName: string, suffix: string) {
+ this.called = false;
+ this.hookName = hookName;
+ this.innerHandle = () => [];
+ this.id = hookName + suffix;
+ this.checkContext = () => {};
+ }
+ handle(hookName: string, context: any, cb: Function) {
+ assert.equal(hookName, this.hookName);
+ assert(context != null);
+ assert(context.req != null);
+ assert(context.res != null);
+ assert(context.next != null);
+ this.checkContext(context);
+ assert(!this.called);
+ this.called = true;
+ callOrder.push(this.id);
+ return cb(this.innerHandle(context));
+ }
+ };
+ const handlers: MapArrayType = {};
- beforeEach(async function () {
- callOrder = [];
- for (const hookName of authHookNames) {
- // Create two handlers for each hook to test deferral to the next function.
- const h0 = new Handler(hookName, '_0');
- const h1 = new Handler(hookName, '_1');
- handlers[hookName] = [h0, h1];
- plugins.hooks[hookName] = [
- makeHook(hookName, h0.handle.bind(h0)),
- makeHook(hookName, h1.handle.bind(h1)),
- ];
- }
- });
+ beforeEach(async () => {
+ callOrder = [];
+ for (const hookName of authHookNames) {
+ // Create two handlers for each hook to test deferral to the next function.
+ const h0 = new Handler(hookName, "_0");
+ const h1 = new Handler(hookName, "_1");
+ handlers[hookName] = [h0, h1];
+ plugins.hooks[hookName] = [
+ makeHook(hookName, h0.handle.bind(h0)),
+ makeHook(hookName, h1.handle.bind(h1)),
+ ];
+ }
+ });
- describe('preAuthorize', function () {
- beforeEach(async function () {
- settings.requireAuthentication = false;
- settings.requireAuthorization = false;
- });
+ describe("preAuthorize", () => {
+ beforeEach(async () => {
+ settings.requireAuthentication = false;
+ settings.requireAuthorization = false;
+ });
- it('defers if it returns []', async function () {
- await agent.get('/').expect(200);
- // Note: The preAuthorize hook always runs even if requireAuthorization is false.
- assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
- });
+ it("defers if it returns []", async () => {
+ await agent.get("/").expect(200);
+ // Note: The preAuthorize hook always runs even if requireAuthorization is false.
+ assert.deepEqual(callOrder, ["preAuthorize_0", "preAuthorize_1"]);
+ });
- it('bypasses authenticate and authorize hooks when true is returned', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- handlers.preAuthorize[0].innerHandle = () => [true];
- await agent.get('/').expect(200);
- assert.deepEqual(callOrder, ['preAuthorize_0']);
- });
+ it("bypasses authenticate and authorize hooks when true is returned", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ handlers.preAuthorize[0].innerHandle = () => [true];
+ await agent.get("/").expect(200);
+ assert.deepEqual(callOrder, ["preAuthorize_0"]);
+ });
- it('bypasses authenticate and authorize hooks when false is returned', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- handlers.preAuthorize[0].innerHandle = () => [false];
- await agent.get('/').expect(403);
- assert.deepEqual(callOrder, ['preAuthorize_0']);
- });
+ it("bypasses authenticate and authorize hooks when false is returned", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ handlers.preAuthorize[0].innerHandle = () => [false];
+ await agent.get("/").expect(403);
+ assert.deepEqual(callOrder, ["preAuthorize_0"]);
+ });
- it('bypasses authenticate and authorize hooks when next is called', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- handlers.preAuthorize[0].innerHandle = ({next}:{
- next: Function
- }) => next();
- await agent.get('/').expect(200);
- assert.deepEqual(callOrder, ['preAuthorize_0']);
- });
+ it("bypasses authenticate and authorize hooks when next is called", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ handlers.preAuthorize[0].innerHandle = ({
+ next,
+ }: {
+ next: Function;
+ }) => next();
+ await agent.get("/").expect(200);
+ assert.deepEqual(callOrder, ["preAuthorize_0"]);
+ });
- it('static content (expressPreSession) bypasses all auth checks', async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- await agent.get('/static/robots.txt').expect(200);
- assert.deepEqual(callOrder, []);
- });
+ it("static content (expressPreSession) bypasses all auth checks", async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ await agent.get("/static/robots.txt").expect(200);
+ assert.deepEqual(callOrder, []);
+ });
- it('cannot grant access to /admin', async function () {
- handlers.preAuthorize[0].innerHandle = () => [true];
- await agent.get('/admin-auth/').expect(401);
- // Notes:
- // * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because
- // 'true' entries are ignored for /admin-auth//* requests.
- // * The authenticate hook always runs for /admin-auth//* requests even if
- // settings.requireAuthentication is false.
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("cannot grant access to /admin", async () => {
+ handlers.preAuthorize[0].innerHandle = () => [true];
+ await agent.get("/admin-auth/").expect(401);
+ // Notes:
+ // * preAuthorize[1] is called despite preAuthorize[0] returning a non-empty list because
+ // 'true' entries are ignored for /admin-auth//* requests.
+ // * The authenticate hook always runs for /admin-auth//* requests even if
+ // settings.requireAuthentication is false.
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('can deny access to /admin-auth/', async function () {
- handlers.preAuthorize[0].innerHandle = () => [false];
- await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(403);
- assert.deepEqual(callOrder, ['preAuthorize_0']);
- });
+ it("can deny access to /admin-auth/", async () => {
+ handlers.preAuthorize[0].innerHandle = () => [false];
+ await agent
+ .get("/admin-auth/")
+ .auth("admin", "admin-password")
+ .expect(403);
+ assert.deepEqual(callOrder, ["preAuthorize_0"]);
+ });
- it('runs preAuthzFailure hook when access is denied', async function () {
- handlers.preAuthorize[0].innerHandle = () => [false];
- let called = false;
- plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName: string, {req, res}:any, cb:Function) => {
- assert.equal(hookName, 'preAuthzFailure');
- assert(req != null);
- assert(res != null);
- assert(!called);
- called = true;
- res.status(200).send('injected');
- return cb([true]);
- })];
- await agent.get('/admin-auth//').auth('admin', 'admin-password').expect(200, 'injected');
- assert(called);
- });
+ it("runs preAuthzFailure hook when access is denied", async () => {
+ handlers.preAuthorize[0].innerHandle = () => [false];
+ let called = false;
+ plugins.hooks.preAuthzFailure = [
+ makeHook(
+ "preAuthzFailure",
+ (hookName: string, { req, res }: any, cb: Function) => {
+ assert.equal(hookName, "preAuthzFailure");
+ assert(req != null);
+ assert(res != null);
+ assert(!called);
+ called = true;
+ res.status(200).send("injected");
+ return cb([true]);
+ },
+ ),
+ ];
+ await agent
+ .get("/admin-auth//")
+ .auth("admin", "admin-password")
+ .expect(200, "injected");
+ assert(called);
+ });
- it('returns 500 if an exception is thrown', async function () {
- handlers.preAuthorize[0].innerHandle = () => { throw new Error('exception test'); };
- await agent.get('/').expect(500);
- });
- });
+ it("returns 500 if an exception is thrown", async () => {
+ handlers.preAuthorize[0].innerHandle = () => {
+ throw new Error("exception test");
+ };
+ await agent.get("/").expect(500);
+ });
+ });
- describe('authenticate', function () {
- beforeEach(async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = false;
- });
+ describe("authenticate", () => {
+ beforeEach(async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = false;
+ });
- it('is not called if !requireAuthentication and not /admin-auth/*', async function () {
- settings.requireAuthentication = false;
- await agent.get('/').expect(200);
- assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1']);
- });
+ it("is not called if !requireAuthentication and not /admin-auth/*", async () => {
+ settings.requireAuthentication = false;
+ await agent.get("/").expect(200);
+ assert.deepEqual(callOrder, ["preAuthorize_0", "preAuthorize_1"]);
+ });
- it('is called if !requireAuthentication and /admin-auth//*', async function () {
- settings.requireAuthentication = false;
- await agent.get('/admin-auth/').expect(401);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("is called if !requireAuthentication and /admin-auth//*", async () => {
+ settings.requireAuthentication = false;
+ await agent.get("/admin-auth/").expect(401);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('defers if empty list returned', async function () {
- await agent.get('/').expect(401);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("defers if empty list returned", async () => {
+ await agent.get("/").expect(401);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('does not defer if return [true], 200', async function () {
- handlers.authenticate[0].innerHandle = ({req}:any) => { req.session.user = {}; return [true]; };
- await agent.get('/').expect(200);
- // Note: authenticate_1 was not called because authenticate_0 handled it.
- assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
- });
+ it("does not defer if return [true], 200", async () => {
+ handlers.authenticate[0].innerHandle = ({ req }: any) => {
+ req.session.user = {};
+ return [true];
+ };
+ await agent.get("/").expect(200);
+ // Note: authenticate_1 was not called because authenticate_0 handled it.
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ ]);
+ });
- it('does not defer if return [false], 401', async function () {
- handlers.authenticate[0].innerHandle = () => [false];
- await agent.get('/').expect(401);
- // Note: authenticate_1 was not called because authenticate_0 handled it.
- assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
- });
+ it("does not defer if return [false], 401", async () => {
+ handlers.authenticate[0].innerHandle = () => [false];
+ await agent.get("/").expect(401);
+ // Note: authenticate_1 was not called because authenticate_0 handled it.
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ ]);
+ });
- it('falls back to HTTP basic auth', async function () {
- await agent.get('/').auth('user', 'user-password').expect(200);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("falls back to HTTP basic auth", async () => {
+ await agent.get("/").auth("user", "user-password").expect(200);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('passes settings.users in context', async function () {
- handlers.authenticate[0].checkContext = ({users}:{
- users: SettingsUser
- }) => {
- assert.equal(users, settings.users);
- };
- await agent.get('/').expect(401);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("passes settings.users in context", async () => {
+ handlers.authenticate[0].checkContext = ({
+ users,
+ }: {
+ users: SettingsUser;
+ }) => {
+ assert.equal(users, settings.users);
+ };
+ await agent.get("/").expect(401);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('passes user, password in context if provided', async function () {
- handlers.authenticate[0].checkContext = ({username, password}:{
- username: string,
- password: string
+ it("passes user, password in context if provided", async () => {
+ handlers.authenticate[0].checkContext = ({
+ username,
+ password,
+ }: {
+ username: string;
+ password: string;
+ }) => {
+ assert.equal(username, "user");
+ assert.equal(password, "user-password");
+ };
+ await agent.get("/").auth("user", "user-password").expect(200);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- }) => {
- assert.equal(username, 'user');
- assert.equal(password, 'user-password');
- };
- await agent.get('/').auth('user', 'user-password').expect(200);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("does not pass user, password in context if not provided", async () => {
+ handlers.authenticate[0].checkContext = ({
+ username,
+ password,
+ }: {
+ username: string;
+ password: string;
+ }) => {
+ assert(username == null);
+ assert(password == null);
+ };
+ await agent.get("/").expect(401);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('does not pass user, password in context if not provided', async function () {
- handlers.authenticate[0].checkContext = ({username, password}:{
- username: string,
- password: string
- }) => {
- assert(username == null);
- assert(password == null);
- };
- await agent.get('/').expect(401);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("errors if req.session.user is not created", async () => {
+ handlers.authenticate[0].innerHandle = () => [true];
+ await agent.get("/").expect(500);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ ]);
+ });
- it('errors if req.session.user is not created', async function () {
- handlers.authenticate[0].innerHandle = () => [true];
- await agent.get('/').expect(500);
- assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
- });
+ it("returns 500 if an exception is thrown", async () => {
+ handlers.authenticate[0].innerHandle = () => {
+ throw new Error("exception test");
+ };
+ await agent.get("/").expect(500);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ ]);
+ });
+ });
- it('returns 500 if an exception is thrown', async function () {
- handlers.authenticate[0].innerHandle = () => { throw new Error('exception test'); };
- await agent.get('/').expect(500);
- assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
- });
- });
+ describe("authorize", () => {
+ beforeEach(async () => {
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ });
- describe('authorize', function () {
- beforeEach(async function () {
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- });
+ it("is not called if !requireAuthorization (non-/admin)", async () => {
+ settings.requireAuthorization = false;
+ await agent.get("/").auth("user", "user-password").expect(200);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('is not called if !requireAuthorization (non-/admin)', async function () {
- settings.requireAuthorization = false;
- await agent.get('/').auth('user', 'user-password').expect(200);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("is not called if !requireAuthorization (/admin)", async () => {
+ settings.requireAuthorization = false;
+ await agent
+ .get("/admin-auth/")
+ .auth("admin", "admin-password")
+ .expect(200);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ ]);
+ });
- it('is not called if !requireAuthorization (/admin)', async function () {
- settings.requireAuthorization = false;
- await agent.get('/admin-auth/').auth('admin', 'admin-password').expect(200);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1']);
- });
+ it("defers if empty list returned", async () => {
+ await agent.get("/").auth("user", "user-password").expect(403);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ "authorize_0",
+ "authorize_1",
+ ]);
+ });
- it('defers if empty list returned', async function () {
- await agent.get('/').auth('user', 'user-password').expect(403);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1',
- 'authorize_0',
- 'authorize_1']);
- });
+ it("does not defer if return [true], 200", async () => {
+ handlers.authorize[0].innerHandle = () => [true];
+ await agent.get("/").auth("user", "user-password").expect(200);
+ // Note: authorize_1 was not called because authorize_0 handled it.
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ "authorize_0",
+ ]);
+ });
- it('does not defer if return [true], 200', async function () {
- handlers.authorize[0].innerHandle = () => [true];
- await agent.get('/').auth('user', 'user-password').expect(200);
- // Note: authorize_1 was not called because authorize_0 handled it.
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1',
- 'authorize_0']);
- });
+ it("does not defer if return [false], 403", async () => {
+ handlers.authorize[0].innerHandle = () => [false];
+ await agent.get("/").auth("user", "user-password").expect(403);
+ // Note: authorize_1 was not called because authorize_0 handled it.
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ "authorize_0",
+ ]);
+ });
- it('does not defer if return [false], 403', async function () {
- handlers.authorize[0].innerHandle = () => [false];
- await agent.get('/').auth('user', 'user-password').expect(403);
- // Note: authorize_1 was not called because authorize_0 handled it.
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1',
- 'authorize_0']);
- });
+ it("passes req.path in context", async () => {
+ handlers.authorize[0].checkContext = ({
+ resource,
+ }: {
+ resource: string;
+ }) => {
+ assert.equal(resource, "/");
+ };
+ await agent.get("/").auth("user", "user-password").expect(403);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ "authorize_0",
+ "authorize_1",
+ ]);
+ });
- it('passes req.path in context', async function () {
- handlers.authorize[0].checkContext = ({resource}:{
- resource: string
- }) => {
- assert.equal(resource, '/');
- };
- await agent.get('/').auth('user', 'user-password').expect(403);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1',
- 'authorize_0',
- 'authorize_1']);
- });
+ it("returns 500 if an exception is thrown", async () => {
+ handlers.authorize[0].innerHandle = () => {
+ throw new Error("exception test");
+ };
+ await agent.get("/").auth("user", "user-password").expect(500);
+ assert.deepEqual(callOrder, [
+ "preAuthorize_0",
+ "preAuthorize_1",
+ "authenticate_0",
+ "authenticate_1",
+ "authorize_0",
+ ]);
+ });
+ });
+ });
- it('returns 500 if an exception is thrown', async function () {
- handlers.authorize[0].innerHandle = () => { throw new Error('exception test'); };
- await agent.get('/').auth('user', 'user-password').expect(500);
- assert.deepEqual(callOrder, ['preAuthorize_0',
- 'preAuthorize_1',
- 'authenticate_0',
- 'authenticate_1',
- 'authorize_0']);
- });
- });
- });
+ describe("webaccess: authnFailure, authzFailure, authFailure hooks", () => {
+ const Handler = class {
+ private hookName: string;
+ private shouldHandle: boolean;
+ private called: boolean;
+ constructor(hookName: string) {
+ this.hookName = hookName;
+ this.shouldHandle = false;
+ this.called = false;
+ }
+ handle(hookName: string, context: any, cb: Function) {
+ assert.equal(hookName, this.hookName);
+ assert(context != null);
+ assert(context.req != null);
+ assert(context.res != null);
+ assert(!this.called);
+ this.called = true;
+ if (this.shouldHandle) {
+ context.res.status(200).send(this.hookName);
+ return cb([true]);
+ }
+ return cb([]);
+ }
+ };
+ const handlers: MapArrayType = {};
- describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () {
- const Handler = class {
- private hookName: string;
- private shouldHandle: boolean;
- private called: boolean;
- constructor(hookName: string) {
- this.hookName = hookName;
- this.shouldHandle = false;
- this.called = false;
- }
- handle(hookName: string, context:any, cb: Function) {
- assert.equal(hookName, this.hookName);
- assert(context != null);
- assert(context.req != null);
- assert(context.res != null);
- assert(!this.called);
- this.called = true;
- if (this.shouldHandle) {
- context.res.status(200).send(this.hookName);
- return cb([true]);
- }
- return cb([]);
- }
- };
- const handlers:MapArrayType = {};
+ beforeEach(async () => {
+ failHookNames.forEach((hookName) => {
+ const handler = new Handler(hookName);
+ handlers[hookName] = handler;
+ plugins.hooks[hookName] = [
+ makeHook(hookName, handler.handle.bind(handler)),
+ ];
+ });
+ settings.requireAuthentication = true;
+ settings.requireAuthorization = true;
+ });
- beforeEach(async function () {
- failHookNames.forEach((hookName) => {
- const handler = new Handler(hookName);
- handlers[hookName] = handler;
- plugins.hooks[hookName] = [makeHook(hookName, handler.handle.bind(handler))];
- });
- settings.requireAuthentication = true;
- settings.requireAuthorization = true;
- });
+ // authn failure tests
+ it("authn fail, no hooks handle -> 401", async () => {
+ await agent.get("/").expect(401);
+ assert(handlers.authnFailure.called);
+ assert(!handlers.authzFailure.called);
+ assert(handlers.authFailure.called);
+ });
- // authn failure tests
- it('authn fail, no hooks handle -> 401', async function () {
- await agent.get('/').expect(401);
- assert(handlers.authnFailure.called);
- assert(!handlers.authzFailure.called);
- assert(handlers.authFailure.called);
- });
+ it("authn fail, authnFailure handles", async () => {
+ handlers.authnFailure.shouldHandle = true;
+ await agent.get("/").expect(200, "authnFailure");
+ assert(handlers.authnFailure.called);
+ assert(!handlers.authzFailure.called);
+ assert(!handlers.authFailure.called);
+ });
- it('authn fail, authnFailure handles', async function () {
- handlers.authnFailure.shouldHandle = true;
- await agent.get('/').expect(200, 'authnFailure');
- assert(handlers.authnFailure.called);
- assert(!handlers.authzFailure.called);
- assert(!handlers.authFailure.called);
- });
+ it("authn fail, authFailure handles", async () => {
+ handlers.authFailure.shouldHandle = true;
+ await agent.get("/").expect(200, "authFailure");
+ assert(handlers.authnFailure.called);
+ assert(!handlers.authzFailure.called);
+ assert(handlers.authFailure.called);
+ });
- it('authn fail, authFailure handles', async function () {
- handlers.authFailure.shouldHandle = true;
- await agent.get('/').expect(200, 'authFailure');
- assert(handlers.authnFailure.called);
- assert(!handlers.authzFailure.called);
- assert(handlers.authFailure.called);
- });
+ it("authnFailure trumps authFailure", async () => {
+ handlers.authnFailure.shouldHandle = true;
+ handlers.authFailure.shouldHandle = true;
+ await agent.get("/").expect(200, "authnFailure");
+ assert(handlers.authnFailure.called);
+ assert(!handlers.authFailure.called);
+ });
- it('authnFailure trumps authFailure', async function () {
- handlers.authnFailure.shouldHandle = true;
- handlers.authFailure.shouldHandle = true;
- await agent.get('/').expect(200, 'authnFailure');
- assert(handlers.authnFailure.called);
- assert(!handlers.authFailure.called);
- });
+ // authz failure tests
+ it("authz fail, no hooks handle -> 403", async () => {
+ await agent.get("/").auth("user", "user-password").expect(403);
+ assert(!handlers.authnFailure.called);
+ assert(handlers.authzFailure.called);
+ assert(handlers.authFailure.called);
+ });
- // authz failure tests
- it('authz fail, no hooks handle -> 403', async function () {
- await agent.get('/').auth('user', 'user-password').expect(403);
- assert(!handlers.authnFailure.called);
- assert(handlers.authzFailure.called);
- assert(handlers.authFailure.called);
- });
+ it("authz fail, authzFailure handles", async () => {
+ handlers.authzFailure.shouldHandle = true;
+ await agent
+ .get("/")
+ .auth("user", "user-password")
+ .expect(200, "authzFailure");
+ assert(!handlers.authnFailure.called);
+ assert(handlers.authzFailure.called);
+ assert(!handlers.authFailure.called);
+ });
- it('authz fail, authzFailure handles', async function () {
- handlers.authzFailure.shouldHandle = true;
- await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure');
- assert(!handlers.authnFailure.called);
- assert(handlers.authzFailure.called);
- assert(!handlers.authFailure.called);
- });
+ it("authz fail, authFailure handles", async () => {
+ handlers.authFailure.shouldHandle = true;
+ await agent
+ .get("/")
+ .auth("user", "user-password")
+ .expect(200, "authFailure");
+ assert(!handlers.authnFailure.called);
+ assert(handlers.authzFailure.called);
+ assert(handlers.authFailure.called);
+ });
- it('authz fail, authFailure handles', async function () {
- handlers.authFailure.shouldHandle = true;
- await agent.get('/').auth('user', 'user-password').expect(200, 'authFailure');
- assert(!handlers.authnFailure.called);
- assert(handlers.authzFailure.called);
- assert(handlers.authFailure.called);
- });
-
- it('authzFailure trumps authFailure', async function () {
- handlers.authzFailure.shouldHandle = true;
- handlers.authFailure.shouldHandle = true;
- await agent.get('/').auth('user', 'user-password').expect(200, 'authzFailure');
- assert(handlers.authzFailure.called);
- assert(!handlers.authFailure.called);
- });
- });
+ it("authzFailure trumps authFailure", async () => {
+ handlers.authzFailure.shouldHandle = true;
+ handlers.authFailure.shouldHandle = true;
+ await agent
+ .get("/")
+ .auth("user", "user-password")
+ .expect(200, "authzFailure");
+ assert(handlers.authzFailure.called);
+ assert(!handlers.authFailure.called);
+ });
+ });
});
diff --git a/src/tests/container/loadSettings.js b/src/tests/container/loadSettings.js
index b59ff0165..a4e3bc533 100644
--- a/src/tests/container/loadSettings.js
+++ b/src/tests/container/loadSettings.js
@@ -12,26 +12,30 @@
* back to a default)
*/
-const fs = require('fs');
-const jsonminify = require('jsonminify');
+const fs = require("fs");
+const jsonminify = require("jsonminify");
function loadSettings() {
- let settingsStr = fs.readFileSync(`${__dirname}/../../../settings.json.docker`).toString();
- // try to parse the settings
- try {
- if (settingsStr) {
- settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}');
- const settings = JSON.parse(settingsStr);
+ let settingsStr = fs
+ .readFileSync(`${__dirname}/../../../settings.json.docker`)
+ .toString();
+ // try to parse the settings
+ try {
+ if (settingsStr) {
+ settingsStr = jsonminify(settingsStr)
+ .replace(",]", "]")
+ .replace(",}", "}");
+ const settings = JSON.parse(settingsStr);
- // custom settings for running in a container
- settings.ip = 'localhost';
- settings.port = '9001';
+ // custom settings for running in a container
+ settings.ip = "localhost";
+ settings.port = "9001";
- return settings;
- }
- } catch (e) {
- console.error('whoops something is bad with settings');
- }
+ return settings;
+ }
+ } catch (e) {
+ console.error("whoops something is bad with settings");
+ }
}
exports.loadSettings = loadSettings;
diff --git a/src/tests/container/specs/api/pad.js b/src/tests/container/specs/api/pad.js
index f6ff8ebf5..7483764f1 100644
--- a/src/tests/container/specs/api/pad.js
+++ b/src/tests/container/specs/api/pad.js
@@ -5,34 +5,32 @@
* TODO: unify those two files, and merge in a single one.
*/
-const settings = require('../../loadSettings').loadSettings();
-const supertest = require('supertest');
+const settings = require("../../loadSettings").loadSettings();
+const supertest = require("supertest");
const api = supertest(`http://${settings.ip}:${settings.port}`);
const apiVersion = 1;
-describe('Connectivity', function () {
- it('can connect', function (done) {
- api.get('/api/')
- .expect('Content-Type', /json/)
- .expect(200, done);
- });
+describe("Connectivity", () => {
+ it("can connect", (done) => {
+ api.get("/api/").expect("Content-Type", /json/).expect(200, done);
+ });
});
-describe('API Versioning', function () {
- it('finds the version tag', function (done) {
- api.get('/api/')
- .expect((res) => {
- if (!res.body.currentVersion) throw new Error('No version set in API');
- return;
- })
- .expect(200, done);
- });
+describe("API Versioning", () => {
+ it("finds the version tag", (done) => {
+ api
+ .get("/api/")
+ .expect((res) => {
+ if (!res.body.currentVersion) throw new Error("No version set in API");
+
+ })
+ .expect(200, done);
+ });
});
-describe('Permission', function () {
- it('errors with invalid OAuth token', function (done) {
- api.get(`/api/${apiVersion}/createPad?padID=test`)
- .expect(401, done);
- });
+describe("Permission", () => {
+ it("errors with invalid OAuth token", (done) => {
+ api.get(`/api/${apiVersion}/createPad?padID=test`).expect(401, done);
+ });
});
diff --git a/src/tests/frontend-new/admin-spec/adminsettings.spec.ts b/src/tests/frontend-new/admin-spec/adminsettings.spec.ts
index ad3a0c441..8ccf9db1e 100644
--- a/src/tests/frontend-new/admin-spec/adminsettings.spec.ts
+++ b/src/tests/frontend-new/admin-spec/adminsettings.spec.ts
@@ -1,60 +1,61 @@
-import {expect, test} from "@playwright/test";
-import {loginToAdmin, restartEtherpad, saveSettings} from "../helper/adminhelper";
+import { expect, test } from "@playwright/test";
+import {
+ loginToAdmin,
+ restartEtherpad,
+ saveSettings,
+} from "../helper/adminhelper";
-test.beforeEach(async ({ page })=>{
- await loginToAdmin(page, 'admin', 'changeme1');
-})
+test.beforeEach(async ({ page }) => {
+ await loginToAdmin(page, "admin", "changeme1");
+});
-test.describe('admin settings',()=> {
+test.describe("admin settings", () => {
+ test("Are Settings visible, populated, does save work", async ({ page }) => {
+ await page.goto("http://localhost:9001/admin/settings");
+ await page.waitForSelector(".settings");
+ const settings = page.locator(".settings");
+ await expect(settings).not.toBeEmpty();
+ const settingsVal = await settings.inputValue();
+ const settingsLength = settingsVal.length;
- test('Are Settings visible, populated, does save work', async ({page}) => {
- await page.goto('http://localhost:9001/admin/settings');
- await page.waitForSelector('.settings');
- const settings = page.locator('.settings');
- await expect(settings).not.toBeEmpty();
+ await settings.fill(`/* test */\n${settingsVal}`);
+ const newValue = await settings.inputValue();
+ expect(newValue).toContain("/* test */");
+ expect(newValue.length).toEqual(settingsLength + 11);
+ await saveSettings(page);
- const settingsVal = await settings.inputValue()
- const settingsLength = settingsVal.length
+ // Check if the changes were actually saved
+ await page.reload();
+ await page.waitForSelector(".settings");
+ await expect(settings).not.toBeEmpty();
- await settings.fill(`/* test */\n${settingsVal}`)
- const newValue = await settings.inputValue()
- expect(newValue).toContain('/* test */')
- expect(newValue.length).toEqual(settingsLength+11)
- await saveSettings(page)
+ const newSettings = page.locator(".settings");
- // Check if the changes were actually saved
- await page.reload()
- await page.waitForSelector('.settings');
- await expect(settings).not.toBeEmpty();
+ const newSettingsVal = await newSettings.inputValue();
+ expect(newSettingsVal).toContain("/* test */");
- const newSettings = page.locator('.settings');
+ // Change back to old settings
+ await newSettings.fill(settingsVal);
+ await saveSettings(page);
- const newSettingsVal = await newSettings.inputValue()
- expect(newSettingsVal).toContain('/* test */')
+ await page.reload();
+ await page.waitForSelector(".settings");
+ await expect(settings).not.toBeEmpty();
+ const oldSettings = page.locator(".settings");
+ const oldSettingsVal = await oldSettings.inputValue();
+ expect(oldSettingsVal).toEqual(settingsVal);
+ expect(oldSettingsVal.length).toEqual(settingsLength);
+ });
-
- // Change back to old settings
- await newSettings.fill(settingsVal)
- await saveSettings(page)
-
- await page.reload()
- await page.waitForSelector('.settings');
- await expect(settings).not.toBeEmpty();
- const oldSettings = page.locator('.settings');
- const oldSettingsVal = await oldSettings.inputValue()
- expect(oldSettingsVal).toEqual(settingsVal)
- expect(oldSettingsVal.length).toEqual(settingsLength)
- })
-
- test('restart works', async function ({page}) {
- await page.goto('http://localhost:9001/admin/settings');
- await page.waitForSelector('.settings')
- await restartEtherpad(page)
- await page.waitForSelector('.settings')
- const settings = page.locator('.settings');
- await expect(settings).not.toBeEmpty();
- await page.waitForSelector('.menu')
- await page.waitForTimeout(5000)
- });
-})
+ test("restart works", async ({ page }) => {
+ await page.goto("http://localhost:9001/admin/settings");
+ await page.waitForSelector(".settings");
+ await restartEtherpad(page);
+ await page.waitForSelector(".settings");
+ const settings = page.locator(".settings");
+ await expect(settings).not.toBeEmpty();
+ await page.waitForSelector(".menu");
+ await page.waitForTimeout(5000);
+ });
+});
diff --git a/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts
index 9dc7c7a20..07b5a5800 100644
--- a/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts
+++ b/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts
@@ -1,39 +1,38 @@
-import {expect, test} from "@playwright/test";
-import {loginToAdmin} from "../helper/adminhelper";
+import { expect, test } from "@playwright/test";
+import { loginToAdmin } from "../helper/adminhelper";
-test.beforeEach(async ({ page })=>{
- await loginToAdmin(page, 'admin', 'changeme1');
- await page.goto('http://localhost:9001/admin/help')
-})
-
-test('Shows troubleshooting page manager', async ({page}) => {
- await page.goto('http://localhost:9001/admin/help')
- await page.waitForSelector('.menu')
- const menu = page.locator('.menu');
- await expect(menu.locator('li')).toHaveCount(4);
-})
-
-test('Shows a version number', async function ({page}) {
- await page.goto('http://localhost:9001/admin/help')
- await page.waitForSelector('.menu')
- const helper = page.locator('.help-block').locator('div').nth(1)
- const version = (await helper.textContent())!.split('.');
- expect(version.length).toBe(3)
+test.beforeEach(async ({ page }) => {
+ await loginToAdmin(page, "admin", "changeme1");
+ await page.goto("http://localhost:9001/admin/help");
});
-test('Lists installed parts', async function ({page}) {
- await page.goto('http://localhost:9001/admin/help')
- await page.waitForSelector('.menu')
- await page.waitForSelector('.innerwrapper ul')
- const parts = page.locator('.innerwrapper ul').nth(1);
- expect(await parts.textContent()).toContain('ep_etherpad-lite/adminsettings');
+test("Shows troubleshooting page manager", async ({ page }) => {
+ await page.goto("http://localhost:9001/admin/help");
+ await page.waitForSelector(".menu");
+ const menu = page.locator(".menu");
+ await expect(menu.locator("li")).toHaveCount(4);
});
-test('Lists installed hooks', async function ({page}) {
- await page.goto('http://localhost:9001/admin/help')
- await page.waitForSelector('.menu')
- await page.waitForSelector('.innerwrapper ul')
- const helper = page.locator('.innerwrapper ul').nth(2);
- expect(await helper.textContent()).toContain('express');
+test("Shows a version number", async ({ page }) => {
+ await page.goto("http://localhost:9001/admin/help");
+ await page.waitForSelector(".menu");
+ const helper = page.locator(".help-block").locator("div").nth(1);
+ const version = (await helper.textContent())!.split(".");
+ expect(version.length).toBe(3);
});
+test("Lists installed parts", async ({ page }) => {
+ await page.goto("http://localhost:9001/admin/help");
+ await page.waitForSelector(".menu");
+ await page.waitForSelector(".innerwrapper ul");
+ const parts = page.locator(".innerwrapper ul").nth(1);
+ expect(await parts.textContent()).toContain("ep_etherpad-lite/adminsettings");
+});
+
+test("Lists installed hooks", async ({ page }) => {
+ await page.goto("http://localhost:9001/admin/help");
+ await page.waitForSelector(".menu");
+ await page.waitForSelector(".innerwrapper ul");
+ const helper = page.locator(".innerwrapper ul").nth(2);
+ expect(await helper.textContent()).toContain("express");
+});
diff --git a/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts b/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts
index c1121d41b..79fdbf1e9 100644
--- a/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts
+++ b/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts
@@ -1,75 +1,78 @@
-import {expect, test} from "@playwright/test";
-import {loginToAdmin} from "../helper/adminhelper";
+import { expect, test } from "@playwright/test";
+import { loginToAdmin } from "../helper/adminhelper";
-test.beforeEach(async ({ page })=>{
- await loginToAdmin(page, 'admin', 'changeme1');
- await page.goto('http://localhost:9001/admin/plugins')
-})
+test.beforeEach(async ({ page }) => {
+ await loginToAdmin(page, "admin", "changeme1");
+ await page.goto("http://localhost:9001/admin/plugins");
+});
+test.describe("Plugins page", () => {
+ test("List some plugins", async ({ page }) => {
+ await page.waitForSelector(".search-field");
+ const pluginTable = page.locator("table tbody").nth(1);
+ await expect(pluginTable).not.toBeEmpty();
+ const plugins = await pluginTable.locator("tr").count();
+ expect(plugins).toBeGreaterThan(10);
+ });
-test.describe('Plugins page', ()=> {
+ test("Searches for a plugin", async ({ page }) => {
+ await page.waitForSelector(".search-field");
+ await page.click(".search-field");
+ await page.keyboard.type("ep_font_color3");
+ await page.keyboard.press("Enter");
+ const pluginTable = page.locator("table tbody").nth(1);
+ await expect(pluginTable.locator("tr")).toHaveCount(1);
+ await expect(pluginTable.locator("tr").first()).toContainText(
+ "ep_font_color3",
+ );
+ });
- test('List some plugins', async ({page}) => {
- await page.waitForSelector('.search-field');
- const pluginTable = page.locator('table tbody').nth(1);
- await expect(pluginTable).not.toBeEmpty()
- const plugins = await pluginTable.locator('tr').count()
- expect(plugins).toBeGreaterThan(10)
- })
+ test("Attempt to Install and Uninstall a plugin", async ({ page }) => {
+ await page.waitForSelector(".search-field");
+ const pluginTable = page.locator("table tbody").nth(1);
+ await expect(pluginTable).not.toBeEmpty({
+ timeout: 15000,
+ });
+ const plugins = await pluginTable.locator("tr").count();
+ expect(plugins).toBeGreaterThan(10);
- test('Searches for a plugin', async ({page}) => {
- await page.waitForSelector('.search-field');
- await page.click('.search-field')
- await page.keyboard.type('ep_font_color3')
- await page.keyboard.press('Enter')
- const pluginTable = page.locator('table tbody').nth(1);
- await expect(pluginTable.locator('tr')).toHaveCount(1)
- await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color3')
- })
+ // Now everything is loaded, lets install a plugin
+ await page.click(".search-field");
+ await page.keyboard.type("ep_font_color3");
+ await page.keyboard.press("Enter");
- test('Attempt to Install and Uninstall a plugin', async ({page}) => {
- await page.waitForSelector('.search-field');
- const pluginTable = page.locator('table tbody').nth(1);
- await expect(pluginTable).not.toBeEmpty({
- timeout: 15000
- })
- const plugins = await pluginTable.locator('tr').count()
- expect(plugins).toBeGreaterThan(10)
+ await expect(pluginTable.locator("tr")).toHaveCount(1);
+ const pluginRow = pluginTable.locator("tr").first();
+ await expect(pluginRow).toContainText("ep_font_color3");
- // Now everything is loaded, lets install a plugin
+ // Select Installation button
+ await pluginRow.locator("td").nth(4).locator("button").first().click();
+ await page.waitForTimeout(100);
+ await page.waitForSelector("table tbody");
+ const installedPlugins = page.locator("table tbody").first();
+ const installedPluginsRows = installedPlugins.locator("tr");
+ await expect(installedPluginsRows).toHaveCount(2, {
+ timeout: 15000,
+ });
- await page.click('.search-field')
- await page.keyboard.type('ep_font_color3')
- await page.keyboard.press('Enter')
+ const installedPluginRow = installedPluginsRows.nth(1);
- await expect(pluginTable.locator('tr')).toHaveCount(1)
- const pluginRow = pluginTable.locator('tr').first()
- await expect(pluginRow).toContainText('ep_font_color3')
-
- // Select Installation button
- await pluginRow.locator('td').nth(4).locator('button').first().click()
- await page.waitForTimeout(100)
- await page.waitForSelector('table tbody')
- const installedPlugins = page.locator('table tbody').first()
- const installedPluginsRows = installedPlugins.locator('tr')
- await expect(installedPluginsRows).toHaveCount(2, {
- timeout: 15000
- })
-
- const installedPluginRow = installedPluginsRows.nth(1)
-
- await expect(installedPluginRow).toContainText('ep_font_color3')
- await installedPluginRow.locator('td').nth(2).locator('button').first().click()
-
- // Wait for the uninstallation to complete
- await expect(installedPluginsRows).toHaveCount(1, {
- timeout: 15000
- })
- await page.waitForTimeout(5000)
- })
-})
+ await expect(installedPluginRow).toContainText("ep_font_color3");
+ await installedPluginRow
+ .locator("td")
+ .nth(2)
+ .locator("button")
+ .first()
+ .click();
+ // Wait for the uninstallation to complete
+ await expect(installedPluginsRows).toHaveCount(1, {
+ timeout: 15000,
+ });
+ await page.waitForTimeout(5000);
+ });
+});
/*
it('Attempt to Update a plugin', async function () {
diff --git a/src/tests/frontend-new/helper/adminhelper.ts b/src/tests/frontend-new/helper/adminhelper.ts
index 8f2242f89..95b1806f2 100644
--- a/src/tests/frontend-new/helper/adminhelper.ts
+++ b/src/tests/frontend-new/helper/adminhelper.ts
@@ -1,32 +1,38 @@
-import {expect, Page} from "@playwright/test";
+import { type Page, expect } from "@playwright/test";
-export const loginToAdmin = async (page: Page, username: string, password: string) => {
-
- await page.goto('http://localhost:9001/admin/');
-
- await page.waitForSelector('input[name="username"]');
- await page.fill('input[name="username"]', username);
- await page.fill('input[name="password"]', password);
- await page.click('input[type="submit"]');
-}
+export const loginToAdmin = async (
+ page: Page,
+ username: string,
+ password: string,
+) => {
+ await page.goto("http://localhost:9001/admin/");
+ await page.waitForSelector('input[name="username"]');
+ await page.fill('input[name="username"]', username);
+ await page.fill('input[name="password"]', password);
+ await page.click('input[type="submit"]');
+};
export const saveSettings = async (page: Page) => {
- // Click save
- await page.locator('.settings-button-bar').locator('button').first().click()
- await page.waitForSelector('.ToastRootSuccess')
-}
+ // Click save
+ await page.locator(".settings-button-bar").locator("button").first().click();
+ await page.waitForSelector(".ToastRootSuccess");
+};
export const restartEtherpad = async (page: Page) => {
- // Click restart
- const restartButton = page.locator('.settings-button-bar').locator('.settingsButton').nth(1)
- const settings = page.locator('.settings');
- await expect(settings).not.toBeEmpty();
- await expect(restartButton).toBeVisible()
- await page.locator('.settings-button-bar')
- .locator('.settingsButton')
- .nth(1)
- .click()
- await page.waitForTimeout(500)
- await page.waitForSelector('.settings')
-}
+ // Click restart
+ const restartButton = page
+ .locator(".settings-button-bar")
+ .locator(".settingsButton")
+ .nth(1);
+ const settings = page.locator(".settings");
+ await expect(settings).not.toBeEmpty();
+ await expect(restartButton).toBeVisible();
+ await page
+ .locator(".settings-button-bar")
+ .locator(".settingsButton")
+ .nth(1)
+ .click();
+ await page.waitForTimeout(500);
+ await page.waitForSelector(".settings");
+};
diff --git a/src/tests/frontend-new/helper/padHelper.ts b/src/tests/frontend-new/helper/padHelper.ts
index f52cd0a35..d3aa25bdd 100644
--- a/src/tests/frontend-new/helper/padHelper.ts
+++ b/src/tests/frontend-new/helper/padHelper.ts
@@ -1,157 +1,165 @@
-import {Frame, Locator, Page} from "@playwright/test";
-import {MapArrayType} from "../../../node/types/MapType";
-import {randomUUID} from "node:crypto";
+import { randomUUID } from "node:crypto";
+import type { Frame, Locator, Page } from "@playwright/test";
+import type { MapArrayType } from "../../../node/types/MapType";
-export const getPadOuter = async (page: Page): Promise => {
- return page.frame('ace_outer')!;
-}
+export const getPadOuter = async (page: Page): Promise => {
+ return page.frame("ace_outer")!;
+};
-export const getPadBody = async (page: Page): Promise => {
- return page.frame('ace_inner')!.locator('#innerdocbody')
-}
+export const getPadBody = async (page: Page): Promise => {
+ return page.frame("ace_inner")!.locator("#innerdocbody");
+};
export const selectAllText = async (page: Page) => {
- await page.keyboard.down('Control');
- await page.keyboard.press('A');
- await page.keyboard.up('Control');
-}
+ await page.keyboard.down("Control");
+ await page.keyboard.press("A");
+ await page.keyboard.up("Control");
+};
export const toggleUserList = async (page: Page) => {
- await page.locator("button[data-l10n-id='pad.toolbar.showusers.title']").click()
-}
+ await page
+ .locator("button[data-l10n-id='pad.toolbar.showusers.title']")
+ .click();
+};
export const setUserName = async (page: Page, userName: string) => {
- await page.waitForSelector('[class="popup popup-show"]')
- await page.click("input[data-l10n-id='pad.userlist.entername']");
- await page.keyboard.type(userName);
-}
-
+ await page.waitForSelector('[class="popup popup-show"]');
+ await page.click("input[data-l10n-id='pad.userlist.entername']");
+ await page.keyboard.type(userName);
+};
export const showChat = async (page: Page) => {
- const chatIcon = page.locator("#chaticon")
- const classes = await chatIcon.getAttribute('class')
- if (classes && !classes.includes('visible')) return
- await chatIcon.click()
- await page.waitForFunction(`!document.querySelector('#chaticon').classList.contains('visible')`)
-}
+ const chatIcon = page.locator("#chaticon");
+ const classes = await chatIcon.getAttribute("class");
+ if (classes && !classes.includes("visible")) return;
+ await chatIcon.click();
+ await page.waitForFunction(
+ `!document.querySelector('#chaticon').classList.contains('visible')`,
+ );
+};
export const getCurrentChatMessageCount = async (page: Page) => {
- return await page.locator('#chattext').locator('p').count()
-}
+ return await page.locator("#chattext").locator("p").count();
+};
export const getChatUserName = async (page: Page) => {
- return await page.locator('#chattext')
- .locator('p')
- .locator('b')
- .innerText()
-}
+ return await page.locator("#chattext").locator("p").locator("b").innerText();
+};
export const getChatMessage = async (page: Page) => {
- return (await page.locator('#chattext')
- .locator('p')
- .textContent({}))!
- .split(await getChatTime(page))[1]
-
-}
-
+ return (await page.locator("#chattext").locator("p").textContent({}))!.split(
+ await getChatTime(page),
+ )[1];
+};
export const getChatTime = async (page: Page) => {
- return await page.locator('#chattext')
- .locator('p')
- .locator('.time')
- .innerText()
-}
+ return await page
+ .locator("#chattext")
+ .locator("p")
+ .locator(".time")
+ .innerText();
+};
export const sendChatMessage = async (page: Page, message: string) => {
- let currentChatCount = await getCurrentChatMessageCount(page)
+ const currentChatCount = await getCurrentChatMessageCount(page);
- const chatInput = page.locator('#chatinput')
- await chatInput.click()
- await page.keyboard.type(message)
- await page.keyboard.press('Enter')
- if(message === "") return
- await page.waitForFunction(`document.querySelector('#chattext').querySelectorAll('p').length >${currentChatCount}`)
-}
+ const chatInput = page.locator("#chatinput");
+ await chatInput.click();
+ await page.keyboard.type(message);
+ await page.keyboard.press("Enter");
+ if (message === "") return;
+ await page.waitForFunction(
+ `document.querySelector('#chattext').querySelectorAll('p').length >${currentChatCount}`,
+ );
+};
-export const isChatBoxShown = async (page: Page):Promise => {
- const classes = await page.locator('#chatbox').getAttribute('class')
- return classes !==null && classes.includes('visible')
-}
+export const isChatBoxShown = async (page: Page): Promise => {
+ const classes = await page.locator("#chatbox").getAttribute("class");
+ return classes !== null && classes.includes("visible");
+};
-export const isChatBoxSticky = async (page: Page):Promise => {
- const classes = await page.locator('#chatbox').getAttribute('class')
- console.log('Chat', classes && classes.includes('stickyChat'))
- return classes !==null && classes.includes('stickyChat')
-}
+export const isChatBoxSticky = async (page: Page): Promise => {
+ const classes = await page.locator("#chatbox").getAttribute("class");
+ console.log("Chat", classes && classes.includes("stickyChat"));
+ return classes !== null && classes.includes("stickyChat");
+};
export const hideChat = async (page: Page) => {
- if(!await isChatBoxShown(page)|| await isChatBoxSticky(page)) return
- await page.locator('#titlecross').click()
- await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`)
-
-}
+ if (!(await isChatBoxShown(page)) || (await isChatBoxSticky(page))) return;
+ await page.locator("#titlecross").click();
+ await page.waitForFunction(
+ `!document.querySelector('#chatbox').classList.contains('stickyChat')`,
+ );
+};
export const enableStickyChatviaIcon = async (page: Page) => {
- if(await isChatBoxSticky(page)) return
- await page.locator('#titlesticky').click()
- await page.waitForFunction(`document.querySelector('#chatbox').classList.contains('stickyChat')`)
-}
+ if (await isChatBoxSticky(page)) return;
+ await page.locator("#titlesticky").click();
+ await page.waitForFunction(
+ `document.querySelector('#chatbox').classList.contains('stickyChat')`,
+ );
+};
export const disableStickyChatviaIcon = async (page: Page) => {
- if(!await isChatBoxSticky(page)) return
- await page.locator('#titlecross').click()
- await page.waitForFunction(`!document.querySelector('#chatbox').classList.contains('stickyChat')`)
-}
+ if (!(await isChatBoxSticky(page))) return;
+ await page.locator("#titlecross").click();
+ await page.waitForFunction(
+ `!document.querySelector('#chatbox').classList.contains('stickyChat')`,
+ );
+};
-
-export const appendQueryParams = async (page: Page, queryParameters: MapArrayType) => {
- const searchParams = new URLSearchParams(page.url().split('?')[1]);
- Object.keys(queryParameters).forEach((key) => {
- searchParams.append(key, queryParameters[key]);
- });
- await page.goto(page.url()+"?"+ searchParams.toString());
- await page.waitForSelector('iframe[name="ace_outer"]');
-}
+export const appendQueryParams = async (
+ page: Page,
+ queryParameters: MapArrayType,
+) => {
+ const searchParams = new URLSearchParams(page.url().split("?")[1]);
+ Object.keys(queryParameters).forEach((key) => {
+ searchParams.append(key, queryParameters[key]);
+ });
+ await page.goto(page.url() + "?" + searchParams.toString());
+ await page.waitForSelector('iframe[name="ace_outer"]');
+};
export const goToNewPad = async (page: Page) => {
- // create a new pad before each test run
- const padId = "FRONTEND_TESTS"+randomUUID();
- await page.goto('http://localhost:9001/p/'+padId);
- await page.waitForSelector('iframe[name="ace_outer"]');
- return padId;
-}
+ // create a new pad before each test run
+ const padId = "FRONTEND_TESTS" + randomUUID();
+ await page.goto("http://localhost:9001/p/" + padId);
+ await page.waitForSelector('iframe[name="ace_outer"]');
+ return padId;
+};
export const goToPad = async (page: Page, padId: string) => {
- await page.goto('http://localhost:9001/p/'+padId);
- await page.waitForSelector('iframe[name="ace_outer"]');
-}
-
+ await page.goto("http://localhost:9001/p/" + padId);
+ await page.waitForSelector('iframe[name="ace_outer"]');
+};
export const clearPadContent = async (page: Page) => {
- const body = await getPadBody(page);
- await body.click();
- await page.keyboard.down('Control');
- await page.keyboard.press('A');
- await page.keyboard.up('Control');
- await page.keyboard.press('Delete');
-}
+ const body = await getPadBody(page);
+ await body.click();
+ await page.keyboard.down("Control");
+ await page.keyboard.press("A");
+ await page.keyboard.up("Control");
+ await page.keyboard.press("Delete");
+};
export const writeToPad = async (page: Page, text: string) => {
- const body = await getPadBody(page);
- await body.click();
- await page.keyboard.type(text);
-}
+ const body = await getPadBody(page);
+ await body.click();
+ await page.keyboard.type(text);
+};
export const clearAuthorship = async (page: Page) => {
- await page.locator("button[data-l10n-id='pad.toolbar.clearAuthorship.title']").click()
-}
+ await page
+ .locator("button[data-l10n-id='pad.toolbar.clearAuthorship.title']")
+ .click();
+};
export const undoChanges = async (page: Page) => {
- await page.keyboard.down('Control');
- await page.keyboard.press('z');
- await page.keyboard.up('Control');
-}
+ await page.keyboard.down("Control");
+ await page.keyboard.press("z");
+ await page.keyboard.up("Control");
+};
export const pressUndoButton = async (page: Page) => {
- await page.locator('.buttonicon-undo').click()
-}
+ await page.locator(".buttonicon-undo").click();
+};
diff --git a/src/tests/frontend-new/helper/settingsHelper.ts b/src/tests/frontend-new/helper/settingsHelper.ts
index 729dd48f6..adc975bf6 100644
--- a/src/tests/frontend-new/helper/settingsHelper.ts
+++ b/src/tests/frontend-new/helper/settingsHelper.ts
@@ -1,35 +1,42 @@
-import {Page} from "@playwright/test";
+import type { Page } from "@playwright/test";
export const isSettingsShown = async (page: Page) => {
- const classes = await page.locator('#settings').getAttribute('class')
- return classes && classes.includes('popup-show')
-}
-
+ const classes = await page.locator("#settings").getAttribute("class");
+ return classes && classes.includes("popup-show");
+};
export const showSettings = async (page: Page) => {
- if(await isSettingsShown(page)) return
- await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click()
- await page.waitForFunction(`document.querySelector('#settings').classList.contains('popup-show')`)
-}
+ if (await isSettingsShown(page)) return;
+ await page
+ .locator("button[data-l10n-id='pad.toolbar.settings.title']")
+ .click();
+ await page.waitForFunction(
+ `document.querySelector('#settings').classList.contains('popup-show')`,
+ );
+};
export const hideSettings = async (page: Page) => {
- if(!await isSettingsShown(page)) return
- await page.locator("button[data-l10n-id='pad.toolbar.settings.title']").click()
- await page.waitForFunction(`!document.querySelector('#settings').classList.contains('popup-show')`)
-}
+ if (!(await isSettingsShown(page))) return;
+ await page
+ .locator("button[data-l10n-id='pad.toolbar.settings.title']")
+ .click();
+ await page.waitForFunction(
+ `!document.querySelector('#settings').classList.contains('popup-show')`,
+ );
+};
export const enableStickyChatviaSettings = async (page: Page) => {
- const stickyChat = page.locator('#options-stickychat')
- const checked = await stickyChat.isChecked()
- if(checked) return
- await stickyChat.check({force: true})
- await page.waitForSelector('#options-stickychat:checked')
-}
+ const stickyChat = page.locator("#options-stickychat");
+ const checked = await stickyChat.isChecked();
+ if (checked) return;
+ await stickyChat.check({ force: true });
+ await page.waitForSelector("#options-stickychat:checked");
+};
export const disableStickyChat = async (page: Page) => {
- const stickyChat = page.locator('#options-stickychat')
- const checked = await stickyChat.isChecked()
- if(!checked) return
- await stickyChat.uncheck({force: true})
- await page.waitForSelector('#options-stickychat:not(:checked)')
-}
+ const stickyChat = page.locator("#options-stickychat");
+ const checked = await stickyChat.isChecked();
+ if (!checked) return;
+ await stickyChat.uncheck({ force: true });
+ await page.waitForSelector("#options-stickychat:not(:checked)");
+};
diff --git a/src/tests/frontend-new/helper/timeslider.ts b/src/tests/frontend-new/helper/timeslider.ts
index e193048e0..47bfe30d0 100644
--- a/src/tests/frontend-new/helper/timeslider.ts
+++ b/src/tests/frontend-new/helper/timeslider.ts
@@ -1,4 +1,4 @@
-import {Page} from "@playwright/test";
+import type { Page } from "@playwright/test";
/**
* Sets the src-attribute of the main iframe to the timeslider
@@ -13,8 +13,11 @@ import {Page} from "@playwright/test";
* @todo for some reason this does only work the first time, you cannot
* goto rev 0 and then via the same method to rev 5. Use buttons instead
*/
-export const gotoTimeslider = async (page: Page, revision: number): Promise => {
- let revisionString = Number.isInteger(revision) ? `#${revision}` : '';
- await page.goto(`${page.url()}/timeslider${revisionString}`);
- await page.waitForSelector('#timer')
+export const gotoTimeslider = async (
+ page: Page,
+ revision: number,
+): Promise => {
+ const revisionString = Number.isInteger(revision) ? `#${revision}` : "";
+ await page.goto(`${page.url()}/timeslider${revisionString}`);
+ await page.waitForSelector("#timer");
};
diff --git a/src/tests/frontend-new/specs/alphabet.spec.ts b/src/tests/frontend-new/specs/alphabet.spec.ts
index fcd8f7f9d..596a3857f 100644
--- a/src/tests/frontend-new/specs/alphabet.spec.ts
+++ b/src/tests/frontend-new/specs/alphabet.spec.ts
@@ -1,27 +1,30 @@
-import {expect, Page, test} from "@playwright/test";
-import {clearPadContent, getPadBody, getPadOuter, goToNewPad} from "../helper/padHelper";
+import { Page, expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ getPadOuter,
+ goToNewPad,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
-
-test.describe('All the alphabet works n stuff', () => {
- const expectedString = 'abcdefghijklmnopqrstuvwxyz';
-
- test('when you enter any char it appears right', async ({page}) => {
-
- // get the inner iframe
- const innerFrame = await getPadBody(page!);
-
- await innerFrame.click();
-
- // delete possible old content
- await clearPadContent(page!);
-
-
- await page.keyboard.type(expectedString);
- const text = await innerFrame.locator('div').innerText();
- expect(text).toBe(expectedString);
- });
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+});
+
+test.describe("All the alphabet works n stuff", () => {
+ const expectedString = "abcdefghijklmnopqrstuvwxyz";
+
+ test("when you enter any char it appears right", async ({ page }) => {
+ // get the inner iframe
+ const innerFrame = await getPadBody(page!);
+
+ await innerFrame.click();
+
+ // delete possible old content
+ await clearPadContent(page!);
+
+ await page.keyboard.type(expectedString);
+ const text = await innerFrame.locator("div").innerText();
+ expect(text).toBe(expectedString);
+ });
});
diff --git a/src/tests/frontend-new/specs/bold.spec.ts b/src/tests/frontend-new/specs/bold.spec.ts
index 6c1769da2..4f3555725 100644
--- a/src/tests/frontend-new/specs/bold.spec.ts
+++ b/src/tests/frontend-new/specs/bold.spec.ts
@@ -1,50 +1,46 @@
-import {expect, test} from "@playwright/test";
-import {randomInt} from "node:crypto";
-import {getPadBody, goToNewPad, selectAllText} from "../helper/padHelper";
import exp from "node:constants";
+import { randomInt } from "node:crypto";
+import { expect, test } from "@playwright/test";
+import { getPadBody, goToNewPad, selectAllText } from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
-test.describe('bold button', ()=>{
+test.describe("bold button", () => {
+ test("makes text bold on click", async ({ page }) => {
+ // get the inner iframe
+ const innerFrame = await getPadBody(page);
- test('makes text bold on click', async ({page}) => {
-// get the inner iframe
- const innerFrame = await getPadBody(page);
+ await innerFrame.click();
+ // Select pad text
+ await selectAllText(page);
+ await page.keyboard.type("Hi Etherpad");
+ await selectAllText(page);
- await innerFrame.click()
- // Select pad text
- await selectAllText(page);
- await page.keyboard.type("Hi Etherpad");
- await selectAllText(page);
+ // click the bold button
+ await page.locator("button[data-l10n-id='pad.toolbar.bold.title']").click();
- // click the bold button
- await page.locator("button[data-l10n-id='pad.toolbar.bold.title']").click();
+ // check if the text is bold
+ expect(await innerFrame.locator("b").innerText()).toBe("Hi Etherpad");
+ });
+ test("makes text bold on keypress", async ({ page }) => {
+ // get the inner iframe
+ const innerFrame = await getPadBody(page);
- // check if the text is bold
- expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad');
- })
+ await innerFrame.click();
+ // Select pad text
+ await selectAllText(page);
+ await page.keyboard.type("Hi Etherpad");
+ await selectAllText(page);
- test('makes text bold on keypress', async ({page}) => {
- // get the inner iframe
- const innerFrame = await getPadBody(page);
+ // Press CTRL + B
+ await page.keyboard.down("Control");
+ await page.keyboard.press("b");
+ await page.keyboard.up("Control");
- await innerFrame.click()
- // Select pad text
- await selectAllText(page);
- await page.keyboard.type("Hi Etherpad");
- await selectAllText(page);
-
- // Press CTRL + B
- await page.keyboard.down('Control');
- await page.keyboard.press('b');
- await page.keyboard.up('Control');
-
-
- // check if the text is bold
- expect(await innerFrame.locator('b').innerText()).toBe('Hi Etherpad');
- })
-
-})
+ // check if the text is bold
+ expect(await innerFrame.locator("b").innerText()).toBe("Hi Etherpad");
+ });
+});
diff --git a/src/tests/frontend-new/specs/change_user_color.spec.ts b/src/tests/frontend-new/specs/change_user_color.spec.ts
index bc6b609a1..b294058a9 100644
--- a/src/tests/frontend-new/specs/change_user_color.spec.ts
+++ b/src/tests/frontend-new/specs/change_user_color.spec.ts
@@ -1,103 +1,100 @@
-import {expect, test} from "@playwright/test";
-import {goToNewPad, sendChatMessage, showChat} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import { goToNewPad, sendChatMessage, showChat } from "../helper/padHelper";
-test.beforeEach(async ({page}) => {
- await goToNewPad(page);
-})
-
-test.describe('change user color', function () {
-
- test('Color picker matches original color and remembers the user color after a refresh',
- async function ({page}) {
-
- // click on the settings button to make settings visible
- let $userButton = page.locator('.buttonicon-showusers');
- await $userButton.click()
-
- let $userSwatch = page.locator('#myswatch');
- await $userSwatch.click()
- // Change the color value of the Farbtastic color picker
-
- const $colorPickerSave = page.locator('#mycolorpickersave');
- let $colorPickerPreview = page.locator('#mycolorpickerpreview');
-
- // Same color represented in two different ways
- const testColorHash = '#abcdef';
- const testColorRGB = 'rgb(171, 205, 239)';
-
- // Check that the color picker matches the automatically assigned random color on the swatch.
- // NOTE: This has a tiny chance of creating a false positive for passing in the
- // off-chance the randomly assigned color is the same as the test color.
- expect(await $colorPickerPreview.getAttribute('style')).toContain(await $userSwatch.getAttribute('style'));
-
- // The swatch updates as the test color is picked.
- await page.evaluate((testRGBColor) => {
- document.getElementById('mycolorpickerpreview')!.style.backgroundColor = testRGBColor;
- }, testColorRGB
- )
-
- await $colorPickerSave.click();
-
- // give it a second to save the color on the server side
- await page.waitForTimeout(1000)
-
-
- // get a new pad, but don't clear the cookies
- await goToNewPad(page)
-
-
- // click on the settings button to make settings visible
- await $userButton.click()
-
- await $userSwatch.click()
-
-
-
- expect(await $colorPickerPreview.getAttribute('style')).toContain(await $userSwatch.getAttribute('style'));
- });
-
- test('Own user color is shown when you enter a chat', async function ({page}) {
-
- const colorOption = page.locator('#options-colorscheck');
- if (!(await colorOption.isChecked())) {
- await colorOption.check();
- }
-
- // click on the settings button to make settings visible
- const $userButton = page.locator('.buttonicon-showusers');
- await $userButton.click()
-
- const $userSwatch = page.locator('#myswatch');
- await $userSwatch.click()
-
- const $colorPickerSave = page.locator('#mycolorpickersave');
-
- // Same color represented in two different ways
- const testColorHash = '#abcdef';
- const testColorRGB = 'rgb(171, 205, 239)';
-
- // The swatch updates as the test color is picked.
- await page.evaluate((testRGBColor) => {
- document.getElementById('mycolorpickerpreview')!.style.backgroundColor = testRGBColor;
- }, testColorRGB
- )
-
-
- await $colorPickerSave.click();
- // click on the chat button to make chat visible
- await showChat(page)
- await sendChatMessage(page, 'O hi');
-
- // wait until the chat message shows up
- const chatP = page.locator('#chattext').locator('p')
- const chatText = await chatP.innerText();
-
- expect(chatText).toContain('O hi');
-
- const color = await chatP.evaluate((el) => {
- return window.getComputedStyle(el).getPropertyValue('background-color');
- }, chatText);
-
- expect(color).toBe(testColorRGB);
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("change user color", () => {
+ test("Color picker matches original color and remembers the user color after a refresh", async ({
+ page,
+ }) => {
+ // click on the settings button to make settings visible
+ const $userButton = page.locator(".buttonicon-showusers");
+ await $userButton.click();
+
+ const $userSwatch = page.locator("#myswatch");
+ await $userSwatch.click();
+ // Change the color value of the Farbtastic color picker
+
+ const $colorPickerSave = page.locator("#mycolorpickersave");
+ const $colorPickerPreview = page.locator("#mycolorpickerpreview");
+
+ // Same color represented in two different ways
+ const testColorHash = "#abcdef";
+ const testColorRGB = "rgb(171, 205, 239)";
+
+ // Check that the color picker matches the automatically assigned random color on the swatch.
+ // NOTE: This has a tiny chance of creating a false positive for passing in the
+ // off-chance the randomly assigned color is the same as the test color.
+ expect(await $colorPickerPreview.getAttribute("style")).toContain(
+ await $userSwatch.getAttribute("style"),
+ );
+
+ // The swatch updates as the test color is picked.
+ await page.evaluate((testRGBColor) => {
+ document.getElementById("mycolorpickerpreview")!.style.backgroundColor =
+ testRGBColor;
+ }, testColorRGB);
+
+ await $colorPickerSave.click();
+
+ // give it a second to save the color on the server side
+ await page.waitForTimeout(1000);
+
+ // get a new pad, but don't clear the cookies
+ await goToNewPad(page);
+
+ // click on the settings button to make settings visible
+ await $userButton.click();
+
+ await $userSwatch.click();
+
+ expect(await $colorPickerPreview.getAttribute("style")).toContain(
+ await $userSwatch.getAttribute("style"),
+ );
+ });
+
+ test("Own user color is shown when you enter a chat", async ({ page }) => {
+ const colorOption = page.locator("#options-colorscheck");
+ if (!(await colorOption.isChecked())) {
+ await colorOption.check();
+ }
+
+ // click on the settings button to make settings visible
+ const $userButton = page.locator(".buttonicon-showusers");
+ await $userButton.click();
+
+ const $userSwatch = page.locator("#myswatch");
+ await $userSwatch.click();
+
+ const $colorPickerSave = page.locator("#mycolorpickersave");
+
+ // Same color represented in two different ways
+ const testColorHash = "#abcdef";
+ const testColorRGB = "rgb(171, 205, 239)";
+
+ // The swatch updates as the test color is picked.
+ await page.evaluate((testRGBColor) => {
+ document.getElementById("mycolorpickerpreview")!.style.backgroundColor =
+ testRGBColor;
+ }, testColorRGB);
+
+ await $colorPickerSave.click();
+ // click on the chat button to make chat visible
+ await showChat(page);
+ await sendChatMessage(page, "O hi");
+
+ // wait until the chat message shows up
+ const chatP = page.locator("#chattext").locator("p");
+ const chatText = await chatP.innerText();
+
+ expect(chatText).toContain("O hi");
+
+ const color = await chatP.evaluate((el) => {
+ return window.getComputedStyle(el).getPropertyValue("background-color");
+ }, chatText);
+
+ expect(color).toBe(testColorRGB);
+ });
});
diff --git a/src/tests/frontend-new/specs/change_user_name.spec.ts b/src/tests/frontend-new/specs/change_user_name.spec.ts
index bf7ea95c3..fec7b4257 100644
--- a/src/tests/frontend-new/specs/change_user_name.spec.ts
+++ b/src/tests/frontend-new/specs/change_user_name.spec.ts
@@ -1,35 +1,41 @@
-import {expect, test} from "@playwright/test";
-import {randomInt} from "node:crypto";
-import {goToNewPad, sendChatMessage, setUserName, showChat, toggleUserList} from "../helper/padHelper";
+import { randomInt } from "node:crypto";
+import { expect, test } from "@playwright/test";
+import {
+ goToNewPad,
+ sendChatMessage,
+ setUserName,
+ showChat,
+ toggleUserList,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
-
-
-test("Remembers the username after a refresh", async ({page}) => {
- await toggleUserList(page);
- await setUserName(page,'😃')
- await toggleUserList(page)
-
- await page.reload();
- await toggleUserList(page);
- const usernameField = page.locator("input[data-l10n-id='pad.userlist.entername']");
- await expect(usernameField).toHaveValue('😃');
-})
-
-
-test('Own user name is shown when you enter a chat', async ({page})=> {
- const chatMessage = 'O hi';
-
- await toggleUserList(page);
- await setUserName(page,'😃');
- await toggleUserList(page);
-
- await showChat(page);
- await sendChatMessage(page,chatMessage);
- const chatText = await page.locator('#chattext').locator('p').innerText();
- expect(chatText).toContain('😃')
- expect(chatText).toContain(chatMessage)
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+});
+
+test("Remembers the username after a refresh", async ({ page }) => {
+ await toggleUserList(page);
+ await setUserName(page, "😃");
+ await toggleUserList(page);
+
+ await page.reload();
+ await toggleUserList(page);
+ const usernameField = page.locator(
+ "input[data-l10n-id='pad.userlist.entername']",
+ );
+ await expect(usernameField).toHaveValue("😃");
+});
+
+test("Own user name is shown when you enter a chat", async ({ page }) => {
+ const chatMessage = "O hi";
+
+ await toggleUserList(page);
+ await setUserName(page, "😃");
+ await toggleUserList(page);
+
+ await showChat(page);
+ await sendChatMessage(page, chatMessage);
+ const chatText = await page.locator("#chattext").locator("p").innerText();
+ expect(chatText).toContain("😃");
+ expect(chatText).toContain(chatMessage);
});
diff --git a/src/tests/frontend-new/specs/chat.spec.ts b/src/tests/frontend-new/specs/chat.spec.ts
index 4d4f1bd1c..ed97816d1 100644
--- a/src/tests/frontend-new/specs/chat.spec.ts
+++ b/src/tests/frontend-new/specs/chat.spec.ts
@@ -1,116 +1,129 @@
-import {expect, test} from "@playwright/test";
-import {randomInt} from "node:crypto";
+import { expect, test } from "@playwright/test";
import {
- appendQueryParams,
- disableStickyChatviaIcon,
- enableStickyChatviaIcon,
- getChatMessage,
- getChatTime,
- getChatUserName,
- getCurrentChatMessageCount, goToNewPad, hideChat, isChatBoxShown, isChatBoxSticky,
- sendChatMessage,
- showChat,
+ appendQueryParams,
+ disableStickyChatviaIcon,
+ enableStickyChatviaIcon,
+ getChatMessage,
+ getChatTime,
+ getChatUserName,
+ getCurrentChatMessageCount,
+ goToNewPad,
+ hideChat,
+ isChatBoxShown,
+ isChatBoxSticky,
+ sendChatMessage,
+ showChat,
} from "../helper/padHelper";
-import {disableStickyChat, enableStickyChatviaSettings, hideSettings, showSettings} from "../helper/settingsHelper";
+import {
+ disableStickyChat,
+ enableStickyChatviaSettings,
+ hideSettings,
+ showSettings,
+} from "../helper/settingsHelper";
-
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-
-test('opens chat, sends a message, makes sure it exists on the page and hides chat', async ({page}) => {
- const chatValue = "JohnMcLear"
-
- // Open chat
- await showChat(page);
- await sendChatMessage(page, chatValue);
-
- expect(await getCurrentChatMessageCount(page)).toBe(1);
- const username = await getChatUserName(page)
- const time = await getChatTime(page)
- const chatMessage = await getChatMessage(page)
-
- expect(username).toBe('unnamed:');
- const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$');
- expect(time).toMatch(regex);
- expect(chatMessage).toBe(" "+chatValue);
-})
-
-test("makes sure that an empty message can't be sent", async function ({page}) {
- const chatValue = 'mluto';
-
- await showChat(page);
-
- await sendChatMessage(page,"");
- // Send a message
- await sendChatMessage(page,chatValue);
-
- expect(await getCurrentChatMessageCount(page)).toBe(1);
-
- // check that the received message is not the empty one
- const username = await getChatUserName(page)
- const time = await getChatTime(page);
- const chatMessage = await getChatMessage(page);
-
- expect(username).toBe('unnamed:');
- const regex = new RegExp('^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$');
- expect(time).toMatch(regex);
- expect(chatMessage).toBe(" "+chatValue);
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
});
-test('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async ({page}) =>{
- await showSettings(page);
+test("opens chat, sends a message, makes sure it exists on the page and hides chat", async ({
+ page,
+}) => {
+ const chatValue = "JohnMcLear";
- await enableStickyChatviaSettings(page);
- expect(await isChatBoxShown(page)).toBe(true);
- expect(await isChatBoxSticky(page)).toBe(true);
+ // Open chat
+ await showChat(page);
+ await sendChatMessage(page, chatValue);
- await disableStickyChat(page);
- expect(await isChatBoxShown(page)).toBe(true);
- expect(await isChatBoxSticky(page)).toBe(false);
- await hideSettings(page);
- await hideChat(page);
- expect(await isChatBoxShown(page)).toBe(false);
- expect(await isChatBoxSticky(page)).toBe(false);
+ expect(await getCurrentChatMessageCount(page)).toBe(1);
+ const username = await getChatUserName(page);
+ const time = await getChatTime(page);
+ const chatMessage = await getChatMessage(page);
+
+ expect(username).toBe("unnamed:");
+ const regex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
+ expect(time).toMatch(regex);
+ expect(chatMessage).toBe(" " + chatValue);
});
-test('makes chat stick to right side of the screen via icon on the top right, ' +
- 'remove sticky via icon, close it', async function ({page}) {
- await showChat(page);
+test("makes sure that an empty message can't be sent", async ({ page }) => {
+ const chatValue = "mluto";
- await enableStickyChatviaIcon(page);
- expect(await isChatBoxShown(page)).toBe(true);
- expect(await isChatBoxSticky(page)).toBe(true);
+ await showChat(page);
- await disableStickyChatviaIcon(page);
- expect(await isChatBoxShown(page)).toBe(true);
- expect(await isChatBoxSticky(page)).toBe(false);
+ await sendChatMessage(page, "");
+ // Send a message
+ await sendChatMessage(page, chatValue);
- await hideChat(page);
- expect(await isChatBoxSticky(page)).toBe(false);
- expect(await isChatBoxShown(page)).toBe(false);
+ expect(await getCurrentChatMessageCount(page)).toBe(1);
+
+ // check that the received message is not the empty one
+ const username = await getChatUserName(page);
+ const time = await getChatTime(page);
+ const chatMessage = await getChatMessage(page);
+
+ expect(username).toBe("unnamed:");
+ const regex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
+ expect(time).toMatch(regex);
+ expect(chatMessage).toBe(" " + chatValue);
});
+test("makes chat stick to right side of the screen via settings, remove sticky via settings, close it", async ({
+ page,
+}) => {
+ await showSettings(page);
-test('Checks showChat=false URL Parameter hides chat then' +
- ' when removed it shows chat', async function ({page}) {
+ await enableStickyChatviaSettings(page);
+ expect(await isChatBoxShown(page)).toBe(true);
+ expect(await isChatBoxSticky(page)).toBe(true);
- // get a new pad, but don't clear the cookies
- await appendQueryParams(page, {
- showChat: 'false'
- });
-
- const chaticon = page.locator('#chaticon')
-
-
- // chat should be hidden.
- expect(await chaticon.isVisible()).toBe(false);
-
- // get a new pad, but don't clear the cookies
- await goToNewPad(page);
- const secondChatIcon = page.locator('#chaticon')
-
- // chat should be visible.
- expect(await secondChatIcon.isVisible()).toBe(true)
+ await disableStickyChat(page);
+ expect(await isChatBoxShown(page)).toBe(true);
+ expect(await isChatBoxSticky(page)).toBe(false);
+ await hideSettings(page);
+ await hideChat(page);
+ expect(await isChatBoxShown(page)).toBe(false);
+ expect(await isChatBoxSticky(page)).toBe(false);
});
+
+test(
+ "makes chat stick to right side of the screen via icon on the top right, " +
+ "remove sticky via icon, close it",
+ async ({ page }) => {
+ await showChat(page);
+
+ await enableStickyChatviaIcon(page);
+ expect(await isChatBoxShown(page)).toBe(true);
+ expect(await isChatBoxSticky(page)).toBe(true);
+
+ await disableStickyChatviaIcon(page);
+ expect(await isChatBoxShown(page)).toBe(true);
+ expect(await isChatBoxSticky(page)).toBe(false);
+
+ await hideChat(page);
+ expect(await isChatBoxSticky(page)).toBe(false);
+ expect(await isChatBoxShown(page)).toBe(false);
+ },
+);
+
+test(
+ "Checks showChat=false URL Parameter hides chat then" +
+ " when removed it shows chat",
+ async ({ page }) => {
+ // get a new pad, but don't clear the cookies
+ await appendQueryParams(page, {
+ showChat: "false",
+ });
+
+ const chaticon = page.locator("#chaticon");
+
+ // chat should be hidden.
+ expect(await chaticon.isVisible()).toBe(false);
+
+ // get a new pad, but don't clear the cookies
+ await goToNewPad(page);
+ const secondChatIcon = page.locator("#chaticon");
+
+ // chat should be visible.
+ expect(await secondChatIcon.isVisible()).toBe(true);
+ },
+);
diff --git a/src/tests/frontend-new/specs/clear_authorship_color.spec.ts b/src/tests/frontend-new/specs/clear_authorship_color.spec.ts
index 6a999a57e..a28574a5c 100644
--- a/src/tests/frontend-new/specs/clear_authorship_color.spec.ts
+++ b/src/tests/frontend-new/specs/clear_authorship_color.spec.ts
@@ -1,87 +1,105 @@
-import {expect, test} from "@playwright/test";
+import { expect, test } from "@playwright/test";
import {
- clearAuthorship,
- clearPadContent,
- getPadBody,
- goToNewPad, pressUndoButton,
- selectAllText,
- undoChanges,
- writeToPad
+ clearAuthorship,
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ pressUndoButton,
+ selectAllText,
+ undoChanges,
+ writeToPad,
} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
-
-test('clear authorship color', async ({page}) => {
- // get the inner iframe
- const innerFrame = await getPadBody(page);
- const padText = "Hello"
-
- // type some text
- await clearPadContent(page);
- await writeToPad(page, padText);
- const retrievedClasses = await innerFrame.locator('div span').nth(0).getAttribute('class')
- expect(retrievedClasses).toContain('author');
-
- // select the text
- await innerFrame.click()
- await selectAllText(page);
-
- await clearAuthorship(page);
- // does the first div include an author class?
- const firstDivClass = await innerFrame.locator('div').nth(0).getAttribute('class');
- expect(firstDivClass).not.toContain('author');
- const classes = page.locator('div.disconnected')
- expect(await classes.isVisible()).toBe(false)
-})
-
-
-test("makes text clear authorship colors and checks it can't be undone", async function ({page}) {
- const innnerPad = await getPadBody(page);
- const padText = "Hello"
-
- // type some text
- await clearPadContent(page);
- await writeToPad(page, padText);
-
- // get the first text element out of the inner iframe
- const firstDivClass = innnerPad.locator('div').nth(0)
- const retrievedClasses = await innnerPad.locator('div span').nth(0).getAttribute('class')
- expect(retrievedClasses).toContain('author');
-
-
- await firstDivClass.focus()
- await clearAuthorship(page);
- expect(await firstDivClass.getAttribute('class')).not.toContain('author');
-
- await undoChanges(page);
- const changedFirstDiv = innnerPad.locator('div').nth(0)
- expect(await changedFirstDiv.getAttribute('class')).not.toContain('author');
-
-
- await pressUndoButton(page);
- const secondChangedFirstDiv = innnerPad.locator('div').nth(0)
- expect(await secondChangedFirstDiv.getAttribute('class')).not.toContain('author');
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
});
+test("clear authorship color", async ({ page }) => {
+ // get the inner iframe
+ const innerFrame = await getPadBody(page);
+ const padText = "Hello";
+
+ // type some text
+ await clearPadContent(page);
+ await writeToPad(page, padText);
+ const retrievedClasses = await innerFrame
+ .locator("div span")
+ .nth(0)
+ .getAttribute("class");
+ expect(retrievedClasses).toContain("author");
+
+ // select the text
+ await innerFrame.click();
+ await selectAllText(page);
+
+ await clearAuthorship(page);
+ // does the first div include an author class?
+ const firstDivClass = await innerFrame
+ .locator("div")
+ .nth(0)
+ .getAttribute("class");
+ expect(firstDivClass).not.toContain("author");
+ const classes = page.locator("div.disconnected");
+ expect(await classes.isVisible()).toBe(false);
+});
+
+test("makes text clear authorship colors and checks it can't be undone", async ({
+ page,
+}) => {
+ const innnerPad = await getPadBody(page);
+ const padText = "Hello";
+
+ // type some text
+ await clearPadContent(page);
+ await writeToPad(page, padText);
+
+ // get the first text element out of the inner iframe
+ const firstDivClass = innnerPad.locator("div").nth(0);
+ const retrievedClasses = await innnerPad
+ .locator("div span")
+ .nth(0)
+ .getAttribute("class");
+ expect(retrievedClasses).toContain("author");
+
+ await firstDivClass.focus();
+ await clearAuthorship(page);
+ expect(await firstDivClass.getAttribute("class")).not.toContain("author");
+
+ await undoChanges(page);
+ const changedFirstDiv = innnerPad.locator("div").nth(0);
+ expect(await changedFirstDiv.getAttribute("class")).not.toContain("author");
+
+ await pressUndoButton(page);
+ const secondChangedFirstDiv = innnerPad.locator("div").nth(0);
+ expect(await secondChangedFirstDiv.getAttribute("class")).not.toContain(
+ "author",
+ );
+});
// Test for https://github.com/ether/etherpad-lite/issues/5128
-test('clears authorship when first line has line attributes', async function ({page}) {
- // Make sure there is text with author info. The first line must have a line attribute.
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page);
- await writeToPad(page,'Hello')
- await page.locator('.buttonicon-insertunorderedlist').click();
- const retrievedClasses = await padBody.locator('div span').nth(0).getAttribute('class')
- expect(retrievedClasses).toContain('author');
- await padBody.click()
- await selectAllText(page);
- await clearAuthorship(page);
- const retrievedClasses2 = await padBody.locator('div span').nth(0).getAttribute('class')
- expect(retrievedClasses2).not.toContain('author');
+test("clears authorship when first line has line attributes", async ({
+ page,
+}) => {
+ // Make sure there is text with author info. The first line must have a line attribute.
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+ await writeToPad(page, "Hello");
+ await page.locator(".buttonicon-insertunorderedlist").click();
+ const retrievedClasses = await padBody
+ .locator("div span")
+ .nth(0)
+ .getAttribute("class");
+ expect(retrievedClasses).toContain("author");
+ await padBody.click();
+ await selectAllText(page);
+ await clearAuthorship(page);
+ const retrievedClasses2 = await padBody
+ .locator("div span")
+ .nth(0)
+ .getAttribute("class");
+ expect(retrievedClasses2).not.toContain("author");
- expect(await page.locator('[class*="author-"]').count()).toBe(0)
+ expect(await page.locator('[class*="author-"]').count()).toBe(0);
});
diff --git a/src/tests/frontend-new/specs/collab_client.spec.ts b/src/tests/frontend-new/specs/collab_client.spec.ts
index 5cc9c1ec3..1b2641b3a 100644
--- a/src/tests/frontend-new/specs/collab_client.spec.ts
+++ b/src/tests/frontend-new/specs/collab_client.spec.ts
@@ -1,94 +1,101 @@
-import {clearPadContent, getPadBody, goToNewPad, goToPad, writeToPad} from "../helper/padHelper";
-import {expect, Page, test} from "@playwright/test";
+import { type Page, expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ goToPad,
+ writeToPad,
+} from "../helper/padHelper";
let padId = "";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- padId = await goToNewPad(page);
- const body = await getPadBody(page);
- await body.click();
- await clearPadContent(page);
- await writeToPad(page, "Hello World");
- await page.keyboard.press('Enter');
- await writeToPad(page, "Hello World");
- await page.keyboard.press('Enter');
- await writeToPad(page, "Hello World");
- await page.keyboard.press('Enter');
- await writeToPad(page, "Hello World");
- await page.keyboard.press('Enter');
- await writeToPad(page, "Hello World");
- await page.keyboard.press('Enter');
-})
-
-test.describe('Messages in the COLLABROOM', function () {
- const user1Text = 'text created by user 1';
- const user2Text = 'text created by user 2';
-
- const replaceLineText = async (lineNumber: number, newText: string, page: Page) => {
- const body = await getPadBody(page)
-
- const div = body.locator('div').nth(lineNumber)
-
- // simulate key presses to delete content
- await div.locator('span').selectText() // select all
- await page.keyboard.press('Backspace') // clear the first line
- await page.keyboard.type(newText) // insert the string
- };
-
- test('bug #4978 regression test', async function ({browser}) {
- // The bug was triggered by receiving a change from another user while simultaneously composing
- // a character and waiting for an acknowledgement of a previously sent change.
-
- // User 1
- const context1 = await browser.newContext();
- const page1 = await context1.newPage();
- await goToPad(page1, padId)
- const body1 = await getPadBody(page1)
- // Perform actions as User 1...
-
- // User 2
- const context2 = await browser.newContext();
- const page2 = await context2.newPage();
- await goToPad(page2, padId)
- const body2 = await getPadBody(page1)
-
- await replaceLineText(0, user1Text,page1);
-
- const text = await body2.locator('div').nth(0).textContent()
- const res = text === user1Text
- expect(res).toBe(true)
-
- // User 1 starts a character composition.
-
-
- await replaceLineText(1, user2Text, page2)
-
- await expect(body1.locator('div').nth(1)).toHaveText(user2Text)
-
-
- // Users 1 and 2 make some more changes.
- await replaceLineText(3, user2Text, page2);
-
- await expect(body1.locator('div').nth(3)).toHaveText(user2Text)
-
- await replaceLineText(2, user1Text, page1);
- await expect(body2.locator('div').nth(2)).toHaveText(user1Text)
-
- // All changes should appear in both views.
- const expectedLines = [
- user1Text,
- user2Text,
- user1Text,
- user2Text,
- ];
-
- for (let i=0;i {
+ // create a new pad before each test run
+ padId = await goToNewPad(page);
+ const body = await getPadBody(page);
+ await body.click();
+ await clearPadContent(page);
+ await writeToPad(page, "Hello World");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "Hello World");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "Hello World");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "Hello World");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "Hello World");
+ await page.keyboard.press("Enter");
+});
+
+test.describe("Messages in the COLLABROOM", () => {
+ const user1Text = "text created by user 1";
+ const user2Text = "text created by user 2";
+
+ const replaceLineText = async (
+ lineNumber: number,
+ newText: string,
+ page: Page,
+ ) => {
+ const body = await getPadBody(page);
+
+ const div = body.locator("div").nth(lineNumber);
+
+ // simulate key presses to delete content
+ await div.locator("span").selectText(); // select all
+ await page.keyboard.press("Backspace"); // clear the first line
+ await page.keyboard.type(newText); // insert the string
+ };
+
+ test("bug #4978 regression test", async ({ browser }) => {
+ // The bug was triggered by receiving a change from another user while simultaneously composing
+ // a character and waiting for an acknowledgement of a previously sent change.
+
+ // User 1
+ const context1 = await browser.newContext();
+ const page1 = await context1.newPage();
+ await goToPad(page1, padId);
+ const body1 = await getPadBody(page1);
+ // Perform actions as User 1...
+
+ // User 2
+ const context2 = await browser.newContext();
+ const page2 = await context2.newPage();
+ await goToPad(page2, padId);
+ const body2 = await getPadBody(page1);
+
+ await replaceLineText(0, user1Text, page1);
+
+ const text = await body2.locator("div").nth(0).textContent();
+ const res = text === user1Text;
+ expect(res).toBe(true);
+
+ // User 1 starts a character composition.
+
+ await replaceLineText(1, user2Text, page2);
+
+ await expect(body1.locator("div").nth(1)).toHaveText(user2Text);
+
+ // Users 1 and 2 make some more changes.
+ await replaceLineText(3, user2Text, page2);
+
+ await expect(body1.locator("div").nth(3)).toHaveText(user2Text);
+
+ await replaceLineText(2, user1Text, page1);
+ await expect(body2.locator("div").nth(2)).toHaveText(user1Text);
+
+ // All changes should appear in both views.
+ const expectedLines = [user1Text, user2Text, user1Text, user2Text];
+
+ for (let i = 0; i < expectedLines.length; i++) {
+ expect(await body1.locator("div").nth(i).textContent()).toBe(
+ expectedLines[i],
+ );
+ }
+
+ for (let i = 0; i < expectedLines.length; i++) {
+ expect(await body2.locator("div").nth(i).textContent()).toBe(
+ expectedLines[i],
+ );
+ }
+ });
});
diff --git a/src/tests/frontend-new/specs/delete.spec.ts b/src/tests/frontend-new/specs/delete.spec.ts
index 26eb3fbdb..5d2c24c49 100644
--- a/src/tests/frontend-new/specs/delete.spec.ts
+++ b/src/tests/frontend-new/specs/delete.spec.ts
@@ -1,22 +1,21 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import { clearPadContent, getPadBody, goToNewPad } from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+});
-
-test('delete keystroke', async ({page}) => {
- const padText = "Hello World this is a test"
- const body = await getPadBody(page)
- await body.click()
- await clearPadContent(page)
- await page.keyboard.type(padText)
- // Navigate to the end of the text
- await page.keyboard.press('End');
- // Delete the last character
- await page.keyboard.press('Backspace');
- const text = await body.locator('div').innerText();
- expect(text).toBe(padText.slice(0, -1));
-})
+test("delete keystroke", async ({ page }) => {
+ const padText = "Hello World this is a test";
+ const body = await getPadBody(page);
+ await body.click();
+ await clearPadContent(page);
+ await page.keyboard.type(padText);
+ // Navigate to the end of the text
+ await page.keyboard.press("End");
+ // Delete the last character
+ await page.keyboard.press("Backspace");
+ const text = await body.locator("div").innerText();
+ expect(text).toBe(padText.slice(0, -1));
+});
diff --git a/src/tests/frontend-new/specs/embed_value.spec.ts b/src/tests/frontend-new/specs/embed_value.spec.ts
index a65276cc9..dc71f6771 100644
--- a/src/tests/frontend-new/specs/embed_value.spec.ts
+++ b/src/tests/frontend-new/specs/embed_value.spec.ts
@@ -1,136 +1,136 @@
-import {expect, Page, test} from "@playwright/test";
-import {goToNewPad} from "../helper/padHelper";
+import { type Page, expect, test } from "@playwright/test";
+import { goToNewPad } from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+});
-test.describe('embed links', function () {
- const objectify = function (str: string) {
- const hash = {};
- const parts = str.split('&');
- for (let i = 0; i < parts.length; i++) {
- const keyValue = parts[i].split('=');
- // @ts-ignore
- hash[keyValue[0]] = keyValue[1];
- }
- return hash;
- };
+test.describe("embed links", () => {
+ const objectify = (str: string) => {
+ const hash = {};
+ const parts = str.split("&");
+ for (let i = 0; i < parts.length; i++) {
+ const keyValue = parts[i].split("=");
+ // @ts-ignore
+ hash[keyValue[0]] = keyValue[1];
+ }
+ return hash;
+ };
- const checkiFrameCode = async function (embedCode: string, readonly: boolean, page: Page) {
- // turn the code into an html element
+ const checkiFrameCode = async (
+ embedCode: string,
+ readonly: boolean,
+ page: Page,
+ ) => {
+ // turn the code into an html element
- await page.setContent(embedCode, {waitUntil: 'load'})
- const locator = page.locator('body').locator('iframe').last()
+ await page.setContent(embedCode, { waitUntil: "load" });
+ const locator = page.locator("body").locator("iframe").last();
+ // read and check the frame attributes
+ const width = await locator.getAttribute("width");
+ const height = await locator.getAttribute("height");
+ const name = await locator.getAttribute("name");
+ expect(width).toBe("100%");
+ expect(height).toBe("600");
+ expect(name).toBe(readonly ? "embed_readonly" : "embed_readwrite");
- // read and check the frame attributes
- const width = await locator.getAttribute('width');
- const height = await locator.getAttribute('height');
- const name = await locator.getAttribute('name');
- expect(width).toBe('100%');
- expect(height).toBe('600');
- expect(name).toBe(readonly ? 'embed_readonly' : 'embed_readwrite');
+ // parse the url
+ const src = (await locator.getAttribute("src"))!;
+ const questionMark = src.indexOf("?");
+ const url = src.substring(0, questionMark);
+ const paramsStr = src.substring(questionMark + 1);
+ const params = objectify(paramsStr);
- // parse the url
- const src = (await locator.getAttribute('src'))!;
- const questionMark = src.indexOf('?');
- const url = src.substring(0, questionMark);
- const paramsStr = src.substring(questionMark + 1);
- const params = objectify(paramsStr);
+ const expectedParams = {
+ showControls: "true",
+ showChat: "true",
+ showLineNumbers: "true",
+ useMonospaceFont: "false",
+ };
- const expectedParams = {
- showControls: 'true',
- showChat: 'true',
- showLineNumbers: 'true',
- useMonospaceFont: 'false',
- };
+ // check the url
+ if (readonly) {
+ expect(url.indexOf("r.") > 0).toBe(true);
+ } else {
+ expect(url).toBe(await page.evaluate(() => window.location.href));
+ }
- // check the url
- if (readonly) {
- expect(url.indexOf('r.') > 0).toBe(true);
- } else {
- expect(url).toBe(await page.evaluate(() => window.location.href));
- }
+ // check if all parts of the url are like expected
+ expect(params).toEqual(expectedParams);
+ };
- // check if all parts of the url are like expected
- expect(params).toEqual(expectedParams);
- };
+ test.describe("read and write", () => {
+ test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+ });
+ test("the share link is the actual pad url", async ({ page }) => {
+ const shareButton = page.locator(".buttonicon-embed");
+ // open share dropdown
+ await shareButton.click();
- test.describe('read and write', function () {
- test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
- })
- test('the share link is the actual pad url', async function ({page}) {
+ // get the link of the share field + the actual pad url and compare them
+ const shareLink = await page.locator("#linkinput").inputValue();
+ const padURL = page.url();
+ expect(shareLink).toBe(padURL);
+ });
- const shareButton = page.locator('.buttonicon-embed')
- // open share dropdown
- await shareButton.click()
+ test("is an iframe with the correct url parameters and correct size", async ({
+ page,
+ }) => {
+ const shareButton = page.locator(".buttonicon-embed");
+ await shareButton.click();
- // get the link of the share field + the actual pad url and compare them
- const shareLink = await page.locator('#linkinput').inputValue()
- const padURL = page.url();
- expect(shareLink).toBe(padURL);
- });
+ // get the link of the share field + the actual pad url and compare them
+ const embedCode = await page.locator("#embedinput").inputValue();
- test('is an iframe with the correct url parameters and correct size', async function ({page}) {
+ await checkiFrameCode(embedCode, false, page);
+ });
+ });
- const shareButton = page.locator('.buttonicon-embed')
- await shareButton.click()
+ test.describe("when read only option is set", () => {
+ test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+ });
- // get the link of the share field + the actual pad url and compare them
- const embedCode = await page.locator('#embedinput').inputValue()
+ test("the share link shows a read only url", async ({ page }) => {
+ // open share dropdown
+ const shareButton = page.locator(".buttonicon-embed");
+ await shareButton.click();
+ const readonlyCheckbox = page.locator("#readonlyinput");
+ await readonlyCheckbox.click({
+ force: true,
+ });
+ await page.waitForSelector("#readonlyinput:checked");
+ // get the link of the share field + the actual pad url and compare them
+ const shareLink = await page.locator("#linkinput").inputValue();
+ const containsReadOnlyLink = shareLink.indexOf("r.") > 0;
+ expect(containsReadOnlyLink).toBe(true);
+ });
- await checkiFrameCode(embedCode, false, page);
- });
- });
+ test("the embed as iframe code is an iframe with the correct url parameters and correct size", async ({
+ page,
+ }) => {
+ // open share dropdown
+ const shareButton = page.locator(".buttonicon-embed");
+ await shareButton.click();
- test.describe('when read only option is set', function () {
- test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
- })
+ // check read only checkbox, a bit hacky
+ const readonlyCheckbox = page.locator("#readonlyinput");
+ await readonlyCheckbox.click({
+ force: true,
+ });
- test('the share link shows a read only url', async function ({page}) {
+ await page.waitForSelector("#readonlyinput:checked");
- // open share dropdown
- const shareButton = page.locator('.buttonicon-embed')
- await shareButton.click()
- const readonlyCheckbox = page.locator('#readonlyinput')
- await readonlyCheckbox.click({
- force: true
- })
- await page.waitForSelector('#readonlyinput:checked')
+ // get the link of the share field + the actual pad url and compare them
+ const embedCode = await page.locator("#embedinput").inputValue();
- // get the link of the share field + the actual pad url and compare them
- const shareLink = await page.locator('#linkinput').inputValue()
- const containsReadOnlyLink = shareLink.indexOf('r.') > 0;
- expect(containsReadOnlyLink).toBe(true);
- });
-
- test('the embed as iframe code is an iframe with the correct url parameters and correct size', async function ({page}) {
-
-
- // open share dropdown
- const shareButton = page.locator('.buttonicon-embed')
- await shareButton.click()
-
- // check read only checkbox, a bit hacky
- const readonlyCheckbox = page.locator('#readonlyinput')
- await readonlyCheckbox.click({
- force: true
- })
-
- await page.waitForSelector('#readonlyinput:checked')
-
-
- // get the link of the share field + the actual pad url and compare them
- const embedCode = await page.locator('#embedinput').inputValue()
-
- await checkiFrameCode(embedCode, true, page);
- });
- })
-})
+ await checkiFrameCode(embedCode, true, page);
+ });
+ });
+});
diff --git a/src/tests/frontend-new/specs/enter.spec.ts b/src/tests/frontend-new/specs/enter.spec.ts
index fd9c732c2..945d863f0 100644
--- a/src/tests/frontend-new/specs/enter.spec.ts
+++ b/src/tests/frontend-new/specs/enter.spec.ts
@@ -1,63 +1,67 @@
-'use strict';
-import {expect, test} from "@playwright/test";
-import {getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import { getPadBody, goToNewPad, writeToPad } from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-test.describe('enter keystroke', function () {
-
- test('creates a new line & puts cursor onto a new line', async function ({page}) {
- const padBody = await getPadBody(page);
-
- // get the first text element out of the inner iframe
- const firstTextElement = padBody.locator('div').nth(0)
-
- // get the original string value minus the last char
- const originalTextValue = await firstTextElement.textContent();
-
- // simulate key presses to enter content
- await firstTextElement.click()
- await page.keyboard.press('Home');
- await page.keyboard.press('Enter');
-
- const updatedFirstElement = padBody.locator('div').nth(0)
- expect(await updatedFirstElement.textContent()).toBe('')
-
- const newSecondLine = padBody.locator('div').nth(1);
- // expect the second line to be the same as the original first line.
- expect(await newSecondLine.textContent()).toBe(originalTextValue);
- });
-
- test('enter is always visible after event', async function ({page}) {
- const padBody = await getPadBody(page);
- const originalLength = await padBody.locator('div').count();
- let lastLine = padBody.locator('div').last();
-
- // simulate key presses to enter content
- let i = 0;
- const numberOfLines = 15;
- while (i < numberOfLines) {
- lastLine = padBody.locator('div').last();
- await lastLine.focus();
- await page.keyboard.press('End');
- await page.keyboard.press('Enter');
-
- // check we can see the caret..
- i++;
- }
-
- expect(await padBody.locator('div').count()).toBe(numberOfLines + originalLength);
-
- // is edited line fully visible?
- const lastDiv = padBody.locator('div').last()
- const lastDivOffset = await lastDiv.boundingBox();
- const bottomOfLastLine = lastDivOffset!.y + lastDivOffset!.height;
- const scrolledWindow = page.frames()[0];
- const windowOffset = await scrolledWindow.evaluate(() => window.pageYOffset);
- const windowHeight = await scrolledWindow.evaluate(() => window.innerHeight);
-
- expect(windowOffset + windowHeight).toBeGreaterThan(bottomOfLastLine);
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("enter keystroke", () => {
+ test("creates a new line & puts cursor onto a new line", async ({ page }) => {
+ const padBody = await getPadBody(page);
+
+ // get the first text element out of the inner iframe
+ const firstTextElement = padBody.locator("div").nth(0);
+
+ // get the original string value minus the last char
+ const originalTextValue = await firstTextElement.textContent();
+
+ // simulate key presses to enter content
+ await firstTextElement.click();
+ await page.keyboard.press("Home");
+ await page.keyboard.press("Enter");
+
+ const updatedFirstElement = padBody.locator("div").nth(0);
+ expect(await updatedFirstElement.textContent()).toBe("");
+
+ const newSecondLine = padBody.locator("div").nth(1);
+ // expect the second line to be the same as the original first line.
+ expect(await newSecondLine.textContent()).toBe(originalTextValue);
+ });
+
+ test("enter is always visible after event", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ const originalLength = await padBody.locator("div").count();
+ let lastLine = padBody.locator("div").last();
+
+ // simulate key presses to enter content
+ let i = 0;
+ const numberOfLines = 15;
+ while (i < numberOfLines) {
+ lastLine = padBody.locator("div").last();
+ await lastLine.focus();
+ await page.keyboard.press("End");
+ await page.keyboard.press("Enter");
+
+ // check we can see the caret..
+ i++;
+ }
+
+ expect(await padBody.locator("div").count()).toBe(
+ numberOfLines + originalLength,
+ );
+
+ // is edited line fully visible?
+ const lastDiv = padBody.locator("div").last();
+ const lastDivOffset = await lastDiv.boundingBox();
+ const bottomOfLastLine = lastDivOffset!.y + lastDivOffset!.height;
+ const scrolledWindow = page.frames()[0];
+ const windowOffset = await scrolledWindow.evaluate(
+ () => window.pageYOffset,
+ );
+ const windowHeight = await scrolledWindow.evaluate(
+ () => window.innerHeight,
+ );
+
+ expect(windowOffset + windowHeight).toBeGreaterThan(bottomOfLastLine);
+ });
});
diff --git a/src/tests/frontend-new/specs/font_type.spec.ts b/src/tests/frontend-new/specs/font_type.spec.ts
index a2772da99..7fca42553 100644
--- a/src/tests/frontend-new/specs/font_type.spec.ts
+++ b/src/tests/frontend-new/specs/font_type.spec.ts
@@ -1,39 +1,39 @@
-import {expect, test} from "@playwright/test";
-import {getPadBody, goToNewPad} from "../helper/padHelper";
-import {showSettings} from "../helper/settingsHelper";
+import { expect, test } from "@playwright/test";
+import { getPadBody, goToNewPad } from "../helper/padHelper";
+import { showSettings } from "../helper/settingsHelper";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
-
-
-test.describe('font select', function () {
- // create a new pad before each test run
-
- test('makes text RobotoMono', async function ({page}) {
- // click on the settings button to make settings visible
- await showSettings(page);
-
- // get the font menu and RobotoMono option
- const viewFontMenu = page.locator('#viewfontmenu');
-
- // select RobotoMono and fire change event
- // $RobotoMonooption.attr('selected','selected');
- // commenting out above will break safari test
- const dropdown = page.locator('.dropdowns-container .dropdown-line .current').nth(0)
- await dropdown.click()
- await page.locator('li:text("RobotoMono")').click()
-
- await viewFontMenu.dispatchEvent('change');
- const padBody = await getPadBody(page)
- const color = await padBody.evaluate((e) => {
- return window.getComputedStyle(e).getPropertyValue("font-family")
- })
-
-
- // check if font changed to RobotoMono
- const containsStr = color.toLowerCase().indexOf('robotomono');
- expect(containsStr).not.toBe(-1);
- });
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+});
+
+test.describe("font select", () => {
+ // create a new pad before each test run
+
+ test("makes text RobotoMono", async ({ page }) => {
+ // click on the settings button to make settings visible
+ await showSettings(page);
+
+ // get the font menu and RobotoMono option
+ const viewFontMenu = page.locator("#viewfontmenu");
+
+ // select RobotoMono and fire change event
+ // $RobotoMonooption.attr('selected','selected');
+ // commenting out above will break safari test
+ const dropdown = page
+ .locator(".dropdowns-container .dropdown-line .current")
+ .nth(0);
+ await dropdown.click();
+ await page.locator('li:text("RobotoMono")').click();
+
+ await viewFontMenu.dispatchEvent("change");
+ const padBody = await getPadBody(page);
+ const color = await padBody.evaluate((e) => {
+ return window.getComputedStyle(e).getPropertyValue("font-family");
+ });
+
+ // check if font changed to RobotoMono
+ const containsStr = color.toLowerCase().indexOf("robotomono");
+ expect(containsStr).not.toBe(-1);
+ });
});
diff --git a/src/tests/frontend-new/specs/indentation.spec.ts b/src/tests/frontend-new/specs/indentation.spec.ts
index 3e94dbad3..53bd0d033 100644
--- a/src/tests/frontend-new/specs/indentation.spec.ts
+++ b/src/tests/frontend-new/specs/indentation.spec.ts
@@ -1,241 +1,250 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
-
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-test.describe('indentation button', function () {
- test('indent text with keypress', async function ({page}) {
- const padBody = await getPadBody(page);
-
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
-
- // select this text element
- await $firstTextElement.selectText()
-
- await page.keyboard.press('Tab');
-
- const uls = padBody.locator('div').first().locator('ul li')
- await expect(uls).toHaveCount(1);
- });
-
- test('indent text with button', async function ({page}) {
- const padBody = await getPadBody(page);
- await page.locator('.buttonicon-indent').click()
-
- const uls = padBody.locator('div').first().locator('ul')
- await expect(uls).toHaveCount(1);
- });
-
-
- test('keeps the indent on enter for the new line', async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- await page.locator('.buttonicon-indent').click()
-
- // type a bit, make a line break and type again
- await padBody.locator('div').first().focus()
- await page.keyboard.type('line 1')
- await page.keyboard.press('Enter');
- await page.keyboard.type('line 2')
- await page.keyboard.press('Enter');
-
- const $newSecondLine = padBody.locator('div span').nth(1)
-
- const hasULElement = padBody.locator('ul li')
-
- await expect(hasULElement).toHaveCount(3);
- await expect($newSecondLine).toHaveText('line 2');
- });
-
-
- test('indents text with spaces on enter if previous line ends ' +
- "with ':', '[', '(', or '{'", async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
- // type a bit, make a line break and type again
- const $firstTextElement = padBody.locator('div').first();
- await writeToPad(page, "line with ':'");
- await page.keyboard.press('Enter');
- await writeToPad(page, "line with '['");
- await page.keyboard.press('Enter');
- await writeToPad(page, "line with '('");
- await page.keyboard.press('Enter');
- await writeToPad(page, "line with '{{}'");
-
- await expect(padBody.locator('div').nth(3)).toHaveText("line with '{{}'");
-
- // we validate bottom to top for easier implementation
-
-
- // curly braces
- const $lineWithCurlyBraces = padBody.locator('div').nth(3)
- await $lineWithCurlyBraces.click();
- await page.keyboard.press('End');
- await page.keyboard.type('{{');
-
- // cannot use sendkeys('{enter}') here, browser does not read the command properly
- await page.keyboard.press('Enter');
-
- expect(await padBody.locator('div').nth(4).textContent()).toMatch(/\s{4}/); // tab === 4 spaces
-
-
-
- // parenthesis
- const $lineWithParenthesis = padBody.locator('div').nth(2)
- await $lineWithParenthesis.click();
- await page.keyboard.press('End');
- await page.keyboard.type('(');
- await page.keyboard.press('Enter');
- const $lineAfterParenthesis = padBody.locator('div').nth(3)
- expect(await $lineAfterParenthesis.textContent()).toMatch(/\s{4}/);
-
- // bracket
- const $lineWithBracket = padBody.locator('div').nth(1)
- await $lineWithBracket.click();
- await page.keyboard.press('End');
- await page.keyboard.type('[');
- await page.keyboard.press('Enter');
- const $lineAfterBracket = padBody.locator('div').nth(2);
- expect(await $lineAfterBracket.textContent()).toMatch(/\s{4}/);
-
- // colon
- const $lineWithColon = padBody.locator('div').first();
- await $lineWithColon.click();
- await page.keyboard.press('End');
- await page.keyboard.type(':');
- await page.keyboard.press('Enter');
- const $lineAfterColon = padBody.locator('div').nth(1);
- expect(await $lineAfterColon.textContent()).toMatch(/\s{4}/);
- });
-
- test('appends indentation to the indent of previous line if previous line ends ' +
- "with ':', '[', '(', or '{'", async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- // type a bit, make a line break and type again
- await writeToPad(page, " line with some indentation and ':'")
- await page.keyboard.press('Enter');
- await writeToPad(page, "line 2")
-
- const $lineWithColon = padBody.locator('div').first();
- await $lineWithColon.click();
- await page.keyboard.press('End');
- await page.keyboard.type(':');
- await page.keyboard.press('Enter');
-
- const $lineAfterColon = padBody.locator('div').nth(1);
- // previous line indentation + regular tab (4 spaces)
- expect(await $lineAfterColon.textContent()).toMatch(/\s{6}/);
- });
-
- test("issue #2772 shows '*' when multiple indented lines " +
- ' receive a style and are outdented', async function ({page}) {
-
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- const inner = padBody.locator('div').first();
- // make sure pad has more than one line
- await inner.click()
- await page.keyboard.type('First');
- await page.keyboard.press('Enter');
- await page.keyboard.type('Second');
-
-
- // indent first 2 lines
- await padBody.locator('div').nth(0).selectText();
- await page.locator('.buttonicon-indent').click()
-
- await padBody.locator('div').nth(1).selectText();
- await page.locator('.buttonicon-indent').click()
-
-
- await expect(padBody.locator('ul li')).toHaveCount(2);
-
-
- // apply bold
- await padBody.locator('div').nth(0).selectText();
- await page.locator('.buttonicon-bold').click()
-
- await padBody.locator('div').nth(1).selectText();
- await page.locator('.buttonicon-bold').click()
-
- await expect(padBody.locator('div b')).toHaveCount(2);
-
- // outdent first 2 lines
- await padBody.locator('div').nth(0).selectText();
- await page.locator('.buttonicon-outdent').click()
-
- await padBody.locator('div').nth(1).selectText();
- await page.locator('.buttonicon-outdent').click()
-
- await expect(padBody.locator('ul li')).toHaveCount(0);
-
- // check if '*' is displayed
- const secondLine = padBody.locator('div').nth(1);
- await expect(secondLine).toHaveText('Second');
- });
-
- test('makes text indented and outdented', async function ({page}) {
- // get the inner iframe
-
- const padBody = await getPadBody(page);
-
- // get the first text element out of the inner iframe
- let firstTextElement = padBody.locator('div').first();
-
- // select this text element
- await firstTextElement.selectText()
-
- // get the indentation button and click it
- await page.locator('.buttonicon-indent').click()
-
- let newFirstTextElement = padBody.locator('div').first();
-
- // is there a list-indent class element now?
- await expect(newFirstTextElement.locator('ul')).toHaveCount(1);
-
- await expect(newFirstTextElement.locator('li')).toHaveCount(1);
-
- // indent again
- await page.locator('.buttonicon-indent').click()
-
- newFirstTextElement = padBody.locator('div').first();
-
-
- // is there a list-indent class element now?
- const ulList = newFirstTextElement.locator('ul').first()
- await expect(ulList).toHaveCount(1);
- // expect it to be part of a list
- expect(await ulList.getAttribute('class')).toBe('list-indent2');
-
- // make sure the text hasn't changed
- expect(await newFirstTextElement.textContent()).toBe(await firstTextElement.textContent());
-
-
- // test outdent
-
- // get the unindentation button and click it twice
- newFirstTextElement = padBody.locator('div').first();
- await newFirstTextElement.selectText()
- await page.locator('.buttonicon-outdent').click()
- await page.locator('.buttonicon-outdent').click()
-
- newFirstTextElement = padBody.locator('div').first();
-
- // is there a list-indent class element now?
- await expect(newFirstTextElement.locator('ul')).toHaveCount(0);
-
- // make sure the text hasn't changed
- expect(await newFirstTextElement.textContent()).toEqual(await firstTextElement.textContent());
- });
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
+
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("indentation button", () => {
+ test("indent text with keypress", async ({ page }) => {
+ const padBody = await getPadBody(page);
+
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
+
+ // select this text element
+ await $firstTextElement.selectText();
+
+ await page.keyboard.press("Tab");
+
+ const uls = padBody.locator("div").first().locator("ul li");
+ await expect(uls).toHaveCount(1);
+ });
+
+ test("indent text with button", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await page.locator(".buttonicon-indent").click();
+
+ const uls = padBody.locator("div").first().locator("ul");
+ await expect(uls).toHaveCount(1);
+ });
+
+ test("keeps the indent on enter for the new line", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ await page.locator(".buttonicon-indent").click();
+
+ // type a bit, make a line break and type again
+ await padBody.locator("div").first().focus();
+ await page.keyboard.type("line 1");
+ await page.keyboard.press("Enter");
+ await page.keyboard.type("line 2");
+ await page.keyboard.press("Enter");
+
+ const $newSecondLine = padBody.locator("div span").nth(1);
+
+ const hasULElement = padBody.locator("ul li");
+
+ await expect(hasULElement).toHaveCount(3);
+ await expect($newSecondLine).toHaveText("line 2");
+ });
+
+ test(
+ "indents text with spaces on enter if previous line ends " +
+ "with ':', '[', '(', or '{'",
+ async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+ // type a bit, make a line break and type again
+ const $firstTextElement = padBody.locator("div").first();
+ await writeToPad(page, "line with ':'");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "line with '['");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "line with '('");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "line with '{{}'");
+
+ await expect(padBody.locator("div").nth(3)).toHaveText("line with '{{}'");
+
+ // we validate bottom to top for easier implementation
+
+ // curly braces
+ const $lineWithCurlyBraces = padBody.locator("div").nth(3);
+ await $lineWithCurlyBraces.click();
+ await page.keyboard.press("End");
+ await page.keyboard.type("{{");
+
+ // cannot use sendkeys('{enter}') here, browser does not read the command properly
+ await page.keyboard.press("Enter");
+
+ expect(await padBody.locator("div").nth(4).textContent()).toMatch(
+ /\s{4}/,
+ ); // tab === 4 spaces
+
+ // parenthesis
+ const $lineWithParenthesis = padBody.locator("div").nth(2);
+ await $lineWithParenthesis.click();
+ await page.keyboard.press("End");
+ await page.keyboard.type("(");
+ await page.keyboard.press("Enter");
+ const $lineAfterParenthesis = padBody.locator("div").nth(3);
+ expect(await $lineAfterParenthesis.textContent()).toMatch(/\s{4}/);
+
+ // bracket
+ const $lineWithBracket = padBody.locator("div").nth(1);
+ await $lineWithBracket.click();
+ await page.keyboard.press("End");
+ await page.keyboard.type("[");
+ await page.keyboard.press("Enter");
+ const $lineAfterBracket = padBody.locator("div").nth(2);
+ expect(await $lineAfterBracket.textContent()).toMatch(/\s{4}/);
+
+ // colon
+ const $lineWithColon = padBody.locator("div").first();
+ await $lineWithColon.click();
+ await page.keyboard.press("End");
+ await page.keyboard.type(":");
+ await page.keyboard.press("Enter");
+ const $lineAfterColon = padBody.locator("div").nth(1);
+ expect(await $lineAfterColon.textContent()).toMatch(/\s{4}/);
+ },
+ );
+
+ test(
+ "appends indentation to the indent of previous line if previous line ends " +
+ "with ':', '[', '(', or '{'",
+ async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ // type a bit, make a line break and type again
+ await writeToPad(page, " line with some indentation and ':'");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "line 2");
+
+ const $lineWithColon = padBody.locator("div").first();
+ await $lineWithColon.click();
+ await page.keyboard.press("End");
+ await page.keyboard.type(":");
+ await page.keyboard.press("Enter");
+
+ const $lineAfterColon = padBody.locator("div").nth(1);
+ // previous line indentation + regular tab (4 spaces)
+ expect(await $lineAfterColon.textContent()).toMatch(/\s{6}/);
+ },
+ );
+
+ test(
+ "issue #2772 shows '*' when multiple indented lines " +
+ " receive a style and are outdented",
+ async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ const inner = padBody.locator("div").first();
+ // make sure pad has more than one line
+ await inner.click();
+ await page.keyboard.type("First");
+ await page.keyboard.press("Enter");
+ await page.keyboard.type("Second");
+
+ // indent first 2 lines
+ await padBody.locator("div").nth(0).selectText();
+ await page.locator(".buttonicon-indent").click();
+
+ await padBody.locator("div").nth(1).selectText();
+ await page.locator(".buttonicon-indent").click();
+
+ await expect(padBody.locator("ul li")).toHaveCount(2);
+
+ // apply bold
+ await padBody.locator("div").nth(0).selectText();
+ await page.locator(".buttonicon-bold").click();
+
+ await padBody.locator("div").nth(1).selectText();
+ await page.locator(".buttonicon-bold").click();
+
+ await expect(padBody.locator("div b")).toHaveCount(2);
+
+ // outdent first 2 lines
+ await padBody.locator("div").nth(0).selectText();
+ await page.locator(".buttonicon-outdent").click();
+
+ await padBody.locator("div").nth(1).selectText();
+ await page.locator(".buttonicon-outdent").click();
+
+ await expect(padBody.locator("ul li")).toHaveCount(0);
+
+ // check if '*' is displayed
+ const secondLine = padBody.locator("div").nth(1);
+ await expect(secondLine).toHaveText("Second");
+ },
+ );
+
+ test("makes text indented and outdented", async ({ page }) => {
+ // get the inner iframe
+
+ const padBody = await getPadBody(page);
+
+ // get the first text element out of the inner iframe
+ const firstTextElement = padBody.locator("div").first();
+
+ // select this text element
+ await firstTextElement.selectText();
+
+ // get the indentation button and click it
+ await page.locator(".buttonicon-indent").click();
+
+ let newFirstTextElement = padBody.locator("div").first();
+
+ // is there a list-indent class element now?
+ await expect(newFirstTextElement.locator("ul")).toHaveCount(1);
+
+ await expect(newFirstTextElement.locator("li")).toHaveCount(1);
+
+ // indent again
+ await page.locator(".buttonicon-indent").click();
+
+ newFirstTextElement = padBody.locator("div").first();
+
+ // is there a list-indent class element now?
+ const ulList = newFirstTextElement.locator("ul").first();
+ await expect(ulList).toHaveCount(1);
+ // expect it to be part of a list
+ expect(await ulList.getAttribute("class")).toBe("list-indent2");
+
+ // make sure the text hasn't changed
+ expect(await newFirstTextElement.textContent()).toBe(
+ await firstTextElement.textContent(),
+ );
+
+ // test outdent
+
+ // get the unindentation button and click it twice
+ newFirstTextElement = padBody.locator("div").first();
+ await newFirstTextElement.selectText();
+ await page.locator(".buttonicon-outdent").click();
+ await page.locator(".buttonicon-outdent").click();
+
+ newFirstTextElement = padBody.locator("div").first();
+
+ // is there a list-indent class element now?
+ await expect(newFirstTextElement.locator("ul")).toHaveCount(0);
+
+ // make sure the text hasn't changed
+ expect(await newFirstTextElement.textContent()).toEqual(
+ await firstTextElement.textContent(),
+ );
+ });
});
diff --git a/src/tests/frontend-new/specs/inner_height.spec.ts b/src/tests/frontend-new/specs/inner_height.spec.ts
index 3baa7e49b..be1598b0f 100644
--- a/src/tests/frontend-new/specs/inner_height.spec.ts
+++ b/src/tests/frontend-new/specs/inner_height.spec.ts
@@ -1,56 +1,64 @@
-'use strict';
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
-
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-test.describe('height regression after ace.js refactoring', function () {
-
- test('clientHeight should equal scrollHeight with few lines', async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- const iframe = page.locator('iframe').first()
- const scrollHeight = await iframe.evaluate((element) => {
- return element.scrollHeight;
- })
-
- const clientHeight = await iframe.evaluate((element) => {
- return element.clientHeight;
- })
-
-
- expect(clientHeight).toEqual(scrollHeight);
- });
-
- test('client height should be less than scrollHeight with many lines', async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- await writeToPad(page,'Test line\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
-
- const iframe = page.locator('iframe').first()
- const scrollHeight = await iframe.evaluate((element) => {
- return element.scrollHeight;
- })
-
- const clientHeight = await iframe.evaluate((element) => {
- return element.clientHeight;
- })
-
- // Need to poll because the heights take some time to settle.
- expect(clientHeight).toBeLessThanOrEqual(scrollHeight);
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("height regression after ace.js refactoring", () => {
+ test("clientHeight should equal scrollHeight with few lines", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ const iframe = page.locator("iframe").first();
+ const scrollHeight = await iframe.evaluate((element) => {
+ return element.scrollHeight;
+ });
+
+ const clientHeight = await iframe.evaluate((element) => {
+ return element.clientHeight;
+ });
+
+ expect(clientHeight).toEqual(scrollHeight);
+ });
+
+ test("client height should be less than scrollHeight with many lines", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ await writeToPad(
+ page,
+ "Test line\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",
+ );
+
+ const iframe = page.locator("iframe").first();
+ const scrollHeight = await iframe.evaluate((element) => {
+ return element.scrollHeight;
+ });
+
+ const clientHeight = await iframe.evaluate((element) => {
+ return element.clientHeight;
+ });
+
+ // Need to poll because the heights take some time to settle.
+ expect(clientHeight).toBeLessThanOrEqual(scrollHeight);
+ });
});
diff --git a/src/tests/frontend-new/specs/italic.spec.ts b/src/tests/frontend-new/specs/italic.spec.ts
index dc69f0e38..eead70cd5 100644
--- a/src/tests/frontend-new/specs/italic.spec.ts
+++ b/src/tests/frontend-new/specs/italic.spec.ts
@@ -1,65 +1,72 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-test.describe('italic some text', function () {
-
- test('makes text italic using button', async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
- await $firstTextElement.click()
- await writeToPad(page, 'Foo')
-
- // select this text element
- await padBody.click()
- await page.keyboard.press('Control+A');
-
- // get the bold button and click it
- const $boldButton = page.locator('.buttonicon-italic');
- await $boldButton.click();
-
- // ace creates a new dom element when you press a button, just get the first text element again
- const $newFirstTextElement = padBody.locator('div').first();
-
- // is there a element now?
- // expect it to be italic
- await expect($newFirstTextElement.locator('i')).toHaveCount(1);
-
-
- // make sure the text hasn't changed
- expect(await $newFirstTextElement.textContent()).toEqual(await $firstTextElement.textContent());
- });
-
- test('makes text italic using keypress', async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
-
- // select this text element
- await writeToPad(page, 'Foo')
-
- await page.keyboard.press('Control+A');
-
- await page.keyboard.press('Control+I');
-
- // ace creates a new dom element when you press a button, just get the first text element again
- const $newFirstTextElement = padBody.locator('div').first();
-
- // is there a element now?
- // expect it to be italic
- await expect($newFirstTextElement.locator('i')).toHaveCount(1);
-
- // make sure the text hasn't changed
- expect(await $newFirstTextElement.textContent()).toBe(await $firstTextElement.textContent());
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("italic some text", () => {
+ test("makes text italic using button", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
+ await $firstTextElement.click();
+ await writeToPad(page, "Foo");
+
+ // select this text element
+ await padBody.click();
+ await page.keyboard.press("Control+A");
+
+ // get the bold button and click it
+ const $boldButton = page.locator(".buttonicon-italic");
+ await $boldButton.click();
+
+ // ace creates a new dom element when you press a button, just get the first text element again
+ const $newFirstTextElement = padBody.locator("div").first();
+
+ // is there a element now?
+ // expect it to be italic
+ await expect($newFirstTextElement.locator("i")).toHaveCount(1);
+
+ // make sure the text hasn't changed
+ expect(await $newFirstTextElement.textContent()).toEqual(
+ await $firstTextElement.textContent(),
+ );
+ });
+
+ test("makes text italic using keypress", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
+
+ // select this text element
+ await writeToPad(page, "Foo");
+
+ await page.keyboard.press("Control+A");
+
+ await page.keyboard.press("Control+I");
+
+ // ace creates a new dom element when you press a button, just get the first text element again
+ const $newFirstTextElement = padBody.locator("div").first();
+
+ // is there a element now?
+ // expect it to be italic
+ await expect($newFirstTextElement.locator("i")).toHaveCount(1);
+
+ // make sure the text hasn't changed
+ expect(await $newFirstTextElement.textContent()).toBe(
+ await $firstTextElement.textContent(),
+ );
+ });
});
diff --git a/src/tests/frontend-new/specs/language.spec.ts b/src/tests/frontend-new/specs/language.spec.ts
index 87da86b13..942988ebe 100644
--- a/src/tests/frontend-new/specs/language.spec.ts
+++ b/src/tests/frontend-new/specs/language.spec.ts
@@ -1,88 +1,90 @@
-import {expect, test} from "@playwright/test";
-import {getPadBody, goToNewPad} from "../helper/padHelper";
-import {showSettings} from "../helper/settingsHelper";
+import { expect, test } from "@playwright/test";
+import { getPadBody, goToNewPad } from "../helper/padHelper";
+import { showSettings } from "../helper/settingsHelper";
-test.beforeEach(async ({ page, browser })=>{
- const context = await browser.newContext()
- await context.clearCookies()
- await goToNewPad(page);
-})
-
-
-
-test.describe('Language select and change', function () {
-
- // Destroy language cookies
- test('makes text german', async function ({page}) {
- // click on the settings button to make settings visible
- await showSettings(page)
-
- // click the language button
- const languageDropDown = page.locator('.nice-select').nth(1)
-
- await languageDropDown.click()
- await page.locator('.nice-select').locator('[data-value=de]').click()
- await expect(languageDropDown.locator('.current')).toHaveText('Deutsch')
-
- // select german
- await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)');
- });
-
- test('makes text English', async function ({page}) {
-
- await showSettings(page)
-
- // click the language button
- await page.locator('.nice-select').nth(1).locator('.current').click()
- await page.locator('.nice-select').locator('[data-value=de]').click()
-
- // select german
- await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)');
-
-
- // change to english
- await page.locator('.nice-select').nth(1).locator('.current').click()
- await page.locator('.nice-select').locator('[data-value=en]').click()
-
- // check if the language is now English
- await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title !== 'Fett (Strg-B)');
- });
-
- test('changes direction when picking an rtl lang', async function ({page}) {
-
- await showSettings(page)
-
- // click the language button
- await page.locator('.nice-select').nth(1).locator('.current').click()
- await page.locator('.nice-select').locator('[data-value=de]').click()
-
- // select german
- await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title === 'Fett (Strg-B)');
-
- // click the language button
- await page.locator('.nice-select').nth(1).locator('.current').click()
- // select arabic
- // $languageoption.attr('selected','selected'); // Breaks the test..
- await page.locator('.nice-select').locator('[data-value=ar]').click()
-
- await page.waitForSelector('html[dir="rtl"]')
- });
-
- test('changes direction when picking an ltr lang', async function ({page}) {
- await showSettings(page)
-
- // change to english
- const languageDropDown = page.locator('.nice-select').nth(1)
- await languageDropDown.locator('.current').click()
- await languageDropDown.locator('[data-value=en]').click()
-
- await expect(languageDropDown.locator('.current')).toHaveText('English')
-
- // check if the language is now English
- await page.locator('.buttonicon-bold').evaluate((el) => el.parentElement!.title !== 'Fett (Strg-B)');
-
-
- await page.waitForSelector('html[dir="ltr"]')
-
- });
+test.beforeEach(async ({ page, browser }) => {
+ const context = await browser.newContext();
+ await context.clearCookies();
+ await goToNewPad(page);
+});
+
+test.describe("Language select and change", () => {
+ // Destroy language cookies
+ test("makes text german", async ({ page }) => {
+ // click on the settings button to make settings visible
+ await showSettings(page);
+
+ // click the language button
+ const languageDropDown = page.locator(".nice-select").nth(1);
+
+ await languageDropDown.click();
+ await page.locator(".nice-select").locator("[data-value=de]").click();
+ await expect(languageDropDown.locator(".current")).toHaveText("Deutsch");
+
+ // select german
+ await page
+ .locator(".buttonicon-bold")
+ .evaluate((el) => el.parentElement!.title === "Fett (Strg-B)");
+ });
+
+ test("makes text English", async ({ page }) => {
+ await showSettings(page);
+
+ // click the language button
+ await page.locator(".nice-select").nth(1).locator(".current").click();
+ await page.locator(".nice-select").locator("[data-value=de]").click();
+
+ // select german
+ await page
+ .locator(".buttonicon-bold")
+ .evaluate((el) => el.parentElement!.title === "Fett (Strg-B)");
+
+ // change to english
+ await page.locator(".nice-select").nth(1).locator(".current").click();
+ await page.locator(".nice-select").locator("[data-value=en]").click();
+
+ // check if the language is now English
+ await page
+ .locator(".buttonicon-bold")
+ .evaluate((el) => el.parentElement!.title !== "Fett (Strg-B)");
+ });
+
+ test("changes direction when picking an rtl lang", async ({ page }) => {
+ await showSettings(page);
+
+ // click the language button
+ await page.locator(".nice-select").nth(1).locator(".current").click();
+ await page.locator(".nice-select").locator("[data-value=de]").click();
+
+ // select german
+ await page
+ .locator(".buttonicon-bold")
+ .evaluate((el) => el.parentElement!.title === "Fett (Strg-B)");
+
+ // click the language button
+ await page.locator(".nice-select").nth(1).locator(".current").click();
+ // select arabic
+ // $languageoption.attr('selected','selected'); // Breaks the test..
+ await page.locator(".nice-select").locator("[data-value=ar]").click();
+
+ await page.waitForSelector('html[dir="rtl"]');
+ });
+
+ test("changes direction when picking an ltr lang", async ({ page }) => {
+ await showSettings(page);
+
+ // change to english
+ const languageDropDown = page.locator(".nice-select").nth(1);
+ await languageDropDown.locator(".current").click();
+ await languageDropDown.locator("[data-value=en]").click();
+
+ await expect(languageDropDown.locator(".current")).toHaveText("English");
+
+ // check if the language is now English
+ await page
+ .locator(".buttonicon-bold")
+ .evaluate((el) => el.parentElement!.title !== "Fett (Strg-B)");
+
+ await page.waitForSelector('html[dir="ltr"]');
+ });
});
diff --git a/src/tests/frontend-new/specs/ordered_list.spec.ts b/src/tests/frontend-new/specs/ordered_list.spec.ts
index 04e996e66..77d749d4e 100644
--- a/src/tests/frontend-new/specs/ordered_list.spec.ts
+++ b/src/tests/frontend-new/specs/ordered_list.spec.ts
@@ -1,109 +1,133 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+test.describe("ordered_list.js", () => {
+ test("issue #4748 keeps numbers increment on OL", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+ await writeToPad(page, "Line 1");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "Line 2");
-test.describe('ordered_list.js', function () {
+ const $insertorderedlistButton = page.locator(
+ ".buttonicon-insertorderedlist",
+ );
+ await padBody.locator("div").first().selectText();
+ await $insertorderedlistButton.first().click();
- test('issue #4748 keeps numbers increment on OL', async function ({page}) {
- const padBody = await getPadBody(page);
- await clearPadContent(page)
- await writeToPad(page, 'Line 1')
- await page.keyboard.press('Enter')
- await writeToPad(page, 'Line 2')
+ const secondLine = padBody.locator("div").nth(1);
- const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
- await padBody.locator('div').first().selectText()
- await $insertorderedlistButton.first().click();
+ await secondLine.selectText();
+ await $insertorderedlistButton.click();
- const secondLine = padBody.locator('div').nth(1)
+ expect(await secondLine.locator("ol").getAttribute("start")).toEqual("2");
+ });
- await secondLine.selectText()
- await $insertorderedlistButton.click();
+ test("issue #1125 keeps the numbered list on enter for the new line", async ({
+ page,
+ }) => {
+ // EMULATES PASTING INTO A PAD
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+ await expect(padBody.locator("div")).toHaveCount(1);
+ const $insertorderedlistButton = page.locator(
+ ".buttonicon-insertorderedlist",
+ );
+ await $insertorderedlistButton.click();
- expect(await secondLine.locator('ol').getAttribute('start')).toEqual('2');
- });
+ // type a bit, make a line break and type again
+ const firstTextElement = padBody.locator("div").first();
+ await firstTextElement.click();
+ await writeToPad(page, "line 1");
+ await page.keyboard.press("Enter");
+ await writeToPad(page, "line 2");
+ await page.keyboard.press("Enter");
- test('issue #1125 keeps the numbered list on enter for the new line', async function ({page}) {
- // EMULATES PASTING INTO A PAD
- const padBody = await getPadBody(page);
- await clearPadContent(page)
- await expect(padBody.locator('div')).toHaveCount(1)
- const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
- await $insertorderedlistButton.click();
+ await expect(padBody.locator("div span").nth(1)).toHaveText("line 2");
- // type a bit, make a line break and type again
- const firstTextElement = padBody.locator('div').first()
- await firstTextElement.click()
- await writeToPad(page, 'line 1')
- await page.keyboard.press('Enter')
- await writeToPad(page, 'line 2')
- await page.keyboard.press('Enter')
+ const $newSecondLine = padBody.locator("div").nth(1);
+ expect(await $newSecondLine.locator("ol li").count()).toEqual(1);
+ await expect($newSecondLine.locator("ol li").nth(0)).toHaveText("line 2");
+ const hasLineNumber = await $newSecondLine
+ .locator("ol")
+ .getAttribute("start");
+ // This doesn't work because pasting in content doesn't work
+ expect(Number(hasLineNumber)).toBe(2);
+ });
+});
- await expect(padBody.locator('div span').nth(1)).toHaveText('line 2');
+test.describe("Pressing Tab in an OL increases and decreases indentation", () => {
+ test("indent and de-indent list item with keypress", async ({ page }) => {
+ const padBody = await getPadBody(page);
- const $newSecondLine = padBody.locator('div').nth(1)
- expect(await $newSecondLine.locator('ol li').count()).toEqual(1);
- await expect($newSecondLine.locator('ol li').nth(0)).toHaveText('line 2');
- const hasLineNumber = await $newSecondLine.locator('ol').getAttribute('start');
- // This doesn't work because pasting in content doesn't work
- expect(Number(hasLineNumber)).toBe(2);
- });
- });
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
- test.describe('Pressing Tab in an OL increases and decreases indentation', function () {
+ // select this text element
+ await $firstTextElement.selectText();
- test('indent and de-indent list item with keypress', async function ({page}) {
- const padBody = await getPadBody(page);
+ const $insertorderedlistButton = page.locator(
+ ".buttonicon-insertorderedlist",
+ );
+ await $insertorderedlistButton.click();
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
+ await page.keyboard.press("Tab");
- // select this text element
- await $firstTextElement.selectText()
+ await expect(
+ padBody.locator("div").first().locator(".list-number2"),
+ ).toHaveCount(1);
- const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
- await $insertorderedlistButton.click()
+ await page.keyboard.press("Shift+Tab");
- await page.keyboard.press('Tab')
+ await expect(
+ padBody.locator("div").first().locator(".list-number1"),
+ ).toHaveCount(1);
+ });
+});
- await expect(padBody.locator('div').first().locator('.list-number2')).toHaveCount(1)
+test.describe(
+ "Pressing indent/outdent button in an OL increases and " +
+ "decreases indentation and bullet / ol formatting",
+ () => {
+ test("indent and de-indent list item with indent button", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
- await page.keyboard.press('Shift+Tab')
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
+ // select this text element
+ await $firstTextElement.selectText();
- await expect(padBody.locator('div').first().locator('.list-number1')).toHaveCount(1)
- });
- });
+ const $insertorderedlistButton = page.locator(
+ ".buttonicon-insertorderedlist",
+ );
+ await $insertorderedlistButton.click();
+ const $indentButton = page.locator(".buttonicon-indent");
+ await $indentButton.dblclick(); // make it indented twice
- test.describe('Pressing indent/outdent button in an OL increases and ' +
- 'decreases indentation and bullet / ol formatting', function () {
+ const outdentButton = page.locator(".buttonicon-outdent");
- test('indent and de-indent list item with indent button', async function ({page}) {
- const padBody = await getPadBody(page);
+ await expect(
+ padBody.locator("div").first().locator(".list-number3"),
+ ).toHaveCount(1);
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
+ await outdentButton.click(); // make it deindented to 1
- // select this text element
- await $firstTextElement.selectText()
-
- const $insertorderedlistButton = page.locator('.buttonicon-insertorderedlist')
- await $insertorderedlistButton.click()
-
- const $indentButton = page.locator('.buttonicon-indent')
- await $indentButton.dblclick() // make it indented twice
-
- const outdentButton = page.locator('.buttonicon-outdent')
-
- await expect(padBody.locator('div').first().locator('.list-number3')).toHaveCount(1)
-
- await outdentButton.click(); // make it deindented to 1
-
- await expect(padBody.locator('div').first().locator('.list-number2')).toHaveCount(1)
- });
- });
+ await expect(
+ padBody.locator("div").first().locator(".list-number2"),
+ ).toHaveCount(1);
+ });
+ },
+);
diff --git a/src/tests/frontend-new/specs/redo.spec.ts b/src/tests/frontend-new/specs/redo.spec.ts
index b3df70c69..7b8b70d6c 100644
--- a/src/tests/frontend-new/specs/redo.spec.ts
+++ b/src/tests/frontend-new/specs/redo.spec.ts
@@ -1,65 +1,66 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-
-test.describe('undo button then redo button', function () {
-
-
- test('redo some typing with button', async function ({page}) {
- const padBody = await getPadBody(page);
-
- // get the first text element inside the editable space
- const $firstTextElement = padBody.locator('div span').first();
- const originalValue = await $firstTextElement.textContent(); // get the original value
- const newString = 'Foo';
-
- await $firstTextElement.focus()
- expect(await $firstTextElement.textContent()).toContain(originalValue);
- await padBody.click()
- await clearPadContent(page)
- await writeToPad(page, newString); // send line 1 to the pad
-
- const modifiedValue = await $firstTextElement.textContent(); // get the modified value
- expect(modifiedValue).not.toBe(originalValue); // expect the value to change
-
- // get undo and redo buttons // click the buttons
- await page.locator('.buttonicon-undo').click() // removes foo
- await page.locator('.buttonicon-redo').click() // resends foo
-
- await expect($firstTextElement).toHaveText(newString);
-
- const finalValue = await padBody.locator('div').first().textContent();
- expect(finalValue).toBe(modifiedValue); // expect the value to change
- });
-
- test('redo some typing with keypress', async function ({page}) {
- const padBody = await getPadBody(page);
-
- // get the first text element inside the editable space
- const $firstTextElement = padBody.locator('div span').first();
- const originalValue = await $firstTextElement.textContent(); // get the original value
- const newString = 'Foo';
-
- await padBody.click()
- await clearPadContent(page)
- await writeToPad(page, newString); // send line 1 to the pad
- const modifiedValue = await $firstTextElement.textContent(); // get the modified value
- expect(modifiedValue).not.toBe(originalValue); // expect the value to change
-
- // undo the change
- await padBody.click()
- await page.keyboard.press('Control+Z');
-
- await page.keyboard.press('Control+Y'); // redo the change
-
-
- await expect($firstTextElement).toHaveText(newString);
-
- const finalValue = await padBody.locator('div').first().textContent();
- expect(finalValue).toBe(modifiedValue); // expect the value to change
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("undo button then redo button", () => {
+ test("redo some typing with button", async ({ page }) => {
+ const padBody = await getPadBody(page);
+
+ // get the first text element inside the editable space
+ const $firstTextElement = padBody.locator("div span").first();
+ const originalValue = await $firstTextElement.textContent(); // get the original value
+ const newString = "Foo";
+
+ await $firstTextElement.focus();
+ expect(await $firstTextElement.textContent()).toContain(originalValue);
+ await padBody.click();
+ await clearPadContent(page);
+ await writeToPad(page, newString); // send line 1 to the pad
+
+ const modifiedValue = await $firstTextElement.textContent(); // get the modified value
+ expect(modifiedValue).not.toBe(originalValue); // expect the value to change
+
+ // get undo and redo buttons // click the buttons
+ await page.locator(".buttonicon-undo").click(); // removes foo
+ await page.locator(".buttonicon-redo").click(); // resends foo
+
+ await expect($firstTextElement).toHaveText(newString);
+
+ const finalValue = await padBody.locator("div").first().textContent();
+ expect(finalValue).toBe(modifiedValue); // expect the value to change
+ });
+
+ test("redo some typing with keypress", async ({ page }) => {
+ const padBody = await getPadBody(page);
+
+ // get the first text element inside the editable space
+ const $firstTextElement = padBody.locator("div span").first();
+ const originalValue = await $firstTextElement.textContent(); // get the original value
+ const newString = "Foo";
+
+ await padBody.click();
+ await clearPadContent(page);
+ await writeToPad(page, newString); // send line 1 to the pad
+ const modifiedValue = await $firstTextElement.textContent(); // get the modified value
+ expect(modifiedValue).not.toBe(originalValue); // expect the value to change
+
+ // undo the change
+ await padBody.click();
+ await page.keyboard.press("Control+Z");
+
+ await page.keyboard.press("Control+Y"); // redo the change
+
+ await expect($firstTextElement).toHaveText(newString);
+
+ const finalValue = await padBody.locator("div").first().textContent();
+ expect(finalValue).toBe(modifiedValue); // expect the value to change
+ });
});
diff --git a/src/tests/frontend-new/specs/strikethrough.spec.ts b/src/tests/frontend-new/specs/strikethrough.spec.ts
index a4f68b4a7..6f0bc25ba 100644
--- a/src/tests/frontend-new/specs/strikethrough.spec.ts
+++ b/src/tests/frontend-new/specs/strikethrough.spec.ts
@@ -1,30 +1,36 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-test.describe('strikethrough button', function () {
-
- test('makes text strikethrough', async function ({page}) {
- const padBody = await getPadBody(page);
-
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
-
- // select this text element
- await $firstTextElement.selectText()
-
- // get the strikethrough button and click it
- await page.locator('.buttonicon-strikethrough').click();
-
- // ace creates a new dom element when you press a button, just get the first text element again
-
- // is there a element now?
- await expect($firstTextElement.locator('s')).toHaveCount(1);
-
- // make sure the text hasn't changed
- expect(await $firstTextElement.textContent()).toEqual(await $firstTextElement.textContent());
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("strikethrough button", () => {
+ test("makes text strikethrough", async ({ page }) => {
+ const padBody = await getPadBody(page);
+
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
+
+ // select this text element
+ await $firstTextElement.selectText();
+
+ // get the strikethrough button and click it
+ await page.locator(".buttonicon-strikethrough").click();
+
+ // ace creates a new dom element when you press a button, just get the first text element again
+
+ // is there a element now?
+ await expect($firstTextElement.locator("s")).toHaveCount(1);
+
+ // make sure the text hasn't changed
+ expect(await $firstTextElement.textContent()).toEqual(
+ await $firstTextElement.textContent(),
+ );
+ });
});
diff --git a/src/tests/frontend-new/specs/timeslider.spec.ts b/src/tests/frontend-new/specs/timeslider.spec.ts
index 317398f18..4d0f3c05c 100644
--- a/src/tests/frontend-new/specs/timeslider.spec.ts
+++ b/src/tests/frontend-new/specs/timeslider.spec.ts
@@ -1,37 +1,40 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
-
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+});
// deactivated, we need a nice way to get the timeslider, this is ugly
-test.describe('timeslider button takes you to the timeslider of a pad', function () {
+test.describe("timeslider button takes you to the timeslider of a pad", () => {
+ test("timeslider contained in URL", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+ await writeToPad(page, "Foo"); // send line 1 to the pad
- test('timeslider contained in URL', async function ({page}) {
- const padBody = await getPadBody(page);
- await clearPadContent(page)
- await writeToPad(page, 'Foo'); // send line 1 to the pad
+ // get the first text element inside the editable space
+ const $firstTextElement = padBody.locator("div span").first();
+ const originalValue = await $firstTextElement.textContent(); // get the original value
+ await $firstTextElement.click();
+ await writeToPad(page, "Testing"); // send line 1 to the pad
- // get the first text element inside the editable space
- const $firstTextElement = padBody.locator('div span').first();
- const originalValue = await $firstTextElement.textContent(); // get the original value
- await $firstTextElement.click()
- await writeToPad(page, 'Testing'); // send line 1 to the pad
+ const modifiedValue = await $firstTextElement.textContent(); // get the modified value
+ expect(modifiedValue).not.toBe(originalValue); // expect the value to change
- const modifiedValue = await $firstTextElement.textContent(); // get the modified value
- expect(modifiedValue).not.toBe(originalValue); // expect the value to change
+ const $timesliderButton = page.locator(".buttonicon-history");
+ await $timesliderButton.click(); // So click the timeslider link
- const $timesliderButton = page.locator('.buttonicon-history');
- await $timesliderButton.click(); // So click the timeslider link
+ await page.waitForSelector("#timeslider-wrapper");
- await page.waitForSelector('#timeslider-wrapper')
+ const iFrameURL = page.url(); // get the url
+ const inTimeslider = iFrameURL.indexOf("timeslider") !== -1;
- const iFrameURL = page.url(); // get the url
- const inTimeslider = iFrameURL.indexOf('timeslider') !== -1;
-
- expect(inTimeslider).toBe(true); // expect the value to change
- });
+ expect(inTimeslider).toBe(true); // expect the value to change
+ });
});
diff --git a/src/tests/frontend-new/specs/timeslider_follow.spec.ts b/src/tests/frontend-new/specs/timeslider_follow.spec.ts
index 9f104b884..1693e5113 100644
--- a/src/tests/frontend-new/specs/timeslider_follow.spec.ts
+++ b/src/tests/frontend-new/specs/timeslider_follow.spec.ts
@@ -1,76 +1,88 @@
-'use strict';
-import {expect, Page, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
-import {gotoTimeslider} from "../helper/timeslider";
+import { Page, expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
+import { gotoTimeslider } from "../helper/timeslider";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-
-test.describe('timeslider follow', function () {
-
- // TODO needs test if content is also followed, when user a makes edits
- // while user b is in the timeslider
- test("content as it's added to timeslider", async function ({page}) {
- // send 6 revisions
- const revs = 6;
- const message = 'a\n\n\n\n\n\n\n\n\n\n';
- const newLines = message.split('\n').length;
- for (let i = 0; i < revs; i++) {
- await writeToPad(page, message)
- }
-
- await gotoTimeslider(page,0);
- expect(page.url()).toContain('#0');
-
- const originalTop = await page.evaluate(() => {
- return window.document.querySelector('#innerdocbody')!.getBoundingClientRect().top;
- });
-
- // set to follow contents as it arrives
- await page.check('#options-followContents');
- await page.click('#playpause_button_icon');
-
- // wait for the scroll
- await page.waitForTimeout(1000)
-
- const currentOffset = await page.evaluate(() => {
- return window.document.querySelector('#innerdocbody')!.getBoundingClientRect().top;
- });
-
- expect(currentOffset).toBeLessThanOrEqual(originalTop);
- });
-
- /**
- * Tests for bug described in #4389
- * The goal is to scroll to the first line that contains a change right before
- * the change is applied.
- */
- test('only to lines that exist in the pad view, regression test for #4389', async function ({page}) {
- const padBody = await getPadBody(page)
- await padBody.click()
-
- await clearPadContent(page)
-
- await writeToPad(page,'Test line\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
- '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
- await padBody.locator('div').nth(40).click();
- await writeToPad(page, 'Another test line');
-
-
- await gotoTimeslider(page, 200);
-
- // set to follow contents as it arrives
- await page.check('#options-followContents');
-
- await page.waitForTimeout(1000)
-
- const oldYPosition = await page.locator('#editorcontainerbox').evaluate((el) => {
- return el.scrollTop;
- })
- expect(oldYPosition).toBe(0);
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("timeslider follow", () => {
+ // TODO needs test if content is also followed, when user a makes edits
+ // while user b is in the timeslider
+ test("content as it's added to timeslider", async ({ page }) => {
+ // send 6 revisions
+ const revs = 6;
+ const message = "a\n\n\n\n\n\n\n\n\n\n";
+ const newLines = message.split("\n").length;
+ for (let i = 0; i < revs; i++) {
+ await writeToPad(page, message);
+ }
+
+ await gotoTimeslider(page, 0);
+ expect(page.url()).toContain("#0");
+
+ const originalTop = await page.evaluate(() => {
+ return window.document
+ .querySelector("#innerdocbody")!
+ .getBoundingClientRect().top;
+ });
+
+ // set to follow contents as it arrives
+ await page.check("#options-followContents");
+ await page.click("#playpause_button_icon");
+
+ // wait for the scroll
+ await page.waitForTimeout(1000);
+
+ const currentOffset = await page.evaluate(() => {
+ return window.document
+ .querySelector("#innerdocbody")!
+ .getBoundingClientRect().top;
+ });
+
+ expect(currentOffset).toBeLessThanOrEqual(originalTop);
+ });
+
+ /**
+ * Tests for bug described in #4389
+ * The goal is to scroll to the first line that contains a change right before
+ * the change is applied.
+ */
+ test("only to lines that exist in the pad view, regression test for #4389", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+
+ await clearPadContent(page);
+
+ await writeToPad(
+ page,
+ "Test line\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
+ "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",
+ );
+ await padBody.locator("div").nth(40).click();
+ await writeToPad(page, "Another test line");
+
+ await gotoTimeslider(page, 200);
+
+ // set to follow contents as it arrives
+ await page.check("#options-followContents");
+
+ await page.waitForTimeout(1000);
+
+ const oldYPosition = await page
+ .locator("#editorcontainerbox")
+ .evaluate((el) => {
+ return el.scrollTop;
+ });
+ expect(oldYPosition).toBe(0);
+ });
});
diff --git a/src/tests/frontend-new/specs/undo.spec.ts b/src/tests/frontend-new/specs/undo.spec.ts
index cdbc12083..1c3c38fb7 100644
--- a/src/tests/frontend-new/specs/undo.spec.ts
+++ b/src/tests/frontend-new/specs/undo.spec.ts
@@ -1,56 +1,56 @@
-'use strict';
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
-
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-
-test.describe('undo button', function () {
-
- test('undo some typing by clicking undo button', async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
-
- // get the first text element inside the editable space
- const firstTextElement = padBody.locator('div').first()
- const originalValue = await firstTextElement.textContent(); // get the original value
- await firstTextElement.focus()
-
- await writeToPad(page, 'foo'); // send line 1 to the pad
-
- const modifiedValue = await firstTextElement.textContent(); // get the modified value
- expect(modifiedValue).not.toBe(originalValue); // expect the value to change
-
- // get clear authorship button as a variable
- const undoButton = page.locator('.buttonicon-undo')
- await undoButton.click() // click the button
-
- await expect(firstTextElement).toHaveText(originalValue!);
- });
-
- test('undo some typing using a keypress', async function ({page}) {
- const padBody = await getPadBody(page);
- await padBody.click()
- await clearPadContent(page)
-
- // get the first text element inside the editable space
- const firstTextElement = padBody.locator('div').first()
- const originalValue = await firstTextElement.textContent(); // get the original value
-
- await firstTextElement.focus()
- await writeToPad(page, 'foo'); // send line 1 to the pad
- const modifiedValue = await firstTextElement.textContent(); // get the modified value
- expect(modifiedValue).not.toBe(originalValue); // expect the value to change
-
- // undo the change
- await page.keyboard.press('Control+Z');
- await page.waitForTimeout(1000)
-
- await expect(firstTextElement).toHaveText(originalValue!);
- });
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
+});
+
+test.describe("undo button", () => {
+ test("undo some typing by clicking undo button", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ // get the first text element inside the editable space
+ const firstTextElement = padBody.locator("div").first();
+ const originalValue = await firstTextElement.textContent(); // get the original value
+ await firstTextElement.focus();
+
+ await writeToPad(page, "foo"); // send line 1 to the pad
+
+ const modifiedValue = await firstTextElement.textContent(); // get the modified value
+ expect(modifiedValue).not.toBe(originalValue); // expect the value to change
+
+ // get clear authorship button as a variable
+ const undoButton = page.locator(".buttonicon-undo");
+ await undoButton.click(); // click the button
+
+ await expect(firstTextElement).toHaveText(originalValue!);
+ });
+
+ test("undo some typing using a keypress", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await padBody.click();
+ await clearPadContent(page);
+
+ // get the first text element inside the editable space
+ const firstTextElement = padBody.locator("div").first();
+ const originalValue = await firstTextElement.textContent(); // get the original value
+
+ await firstTextElement.focus();
+ await writeToPad(page, "foo"); // send line 1 to the pad
+ const modifiedValue = await firstTextElement.textContent(); // get the modified value
+ expect(modifiedValue).not.toBe(originalValue); // expect the value to change
+
+ // undo the change
+ await page.keyboard.press("Control+Z");
+ await page.waitForTimeout(1000);
+
+ await expect(firstTextElement).toHaveText(originalValue!);
+ });
});
diff --git a/src/tests/frontend-new/specs/unordered_list.spec.ts b/src/tests/frontend-new/specs/unordered_list.spec.ts
index a2465e5af..c574fe18b 100644
--- a/src/tests/frontend-new/specs/unordered_list.spec.ts
+++ b/src/tests/frontend-new/specs/unordered_list.spec.ts
@@ -1,127 +1,155 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- // create a new pad before each test run
- await goToNewPad(page);
-})
-
-test.describe('unordered_list.js', function () {
- test.describe('assign unordered list', function () {
- test('insert unordered list text then removes by outdent', async function ({page}) {
- const padBody = await getPadBody(page);
- const originalText = await padBody.locator('div').first().textContent();
-
- const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
- await $insertunorderedlistButton.click();
-
- await expect(padBody.locator('div').first()).toHaveText(originalText!);
- await expect(padBody.locator('div ul li')).toHaveCount(1);
-
- // remove indentation by bullet and ensure text string remains the same
- const $outdentButton = page.locator('.buttonicon-outdent');
- await $outdentButton.click();
- await expect(padBody.locator('div').first()).toHaveText(originalText!);
- });
- });
-
- test.describe('unassign unordered list', function () {
- // create a new pad before each test run
-
-
- test('insert unordered list text then remove by clicking list again', async function ({page}) {
- const padBody = await getPadBody(page);
- const originalText = await padBody.locator('div').first().textContent();
-
- await padBody.locator('div').first().selectText()
- const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
- await $insertunorderedlistButton.click();
-
- await expect(padBody.locator('div').first()).toHaveText(originalText!);
- await expect(padBody.locator('div ul li')).toHaveCount(1);
-
- // remove indentation by bullet and ensure text string remains the same
- await $insertunorderedlistButton.click();
- await expect(padBody.locator('div').locator('ul')).toHaveCount(0)
- });
- });
-
-
- test.describe('keep unordered list on enter key', function () {
-
- test('Keeps the unordered list on enter for the new line', async function ({page}) {
- const padBody = await getPadBody(page);
- await clearPadContent(page)
- await expect(padBody.locator('div')).toHaveCount(1)
-
- const $insertorderedlistButton = page.locator('.buttonicon-insertunorderedlist')
- await $insertorderedlistButton.click();
-
- // type a bit, make a line break and type again
- const $firstTextElement = padBody.locator('div').first();
- await $firstTextElement.click()
- await page.keyboard.type('line 1');
- await page.keyboard.press('Enter');
- await page.keyboard.type('line 2');
- await page.keyboard.press('Enter');
-
- await expect(padBody.locator('div span')).toHaveCount(2);
-
-
- const $newSecondLine = padBody.locator('div').nth(1)
- await expect($newSecondLine.locator('ul')).toHaveCount(1);
- await expect($newSecondLine).toHaveText('line 2');
- });
- });
-
- test.describe('Pressing Tab in an UL increases and decreases indentation', function () {
-
- test('indent and de-indent list item with keypress', async function ({page}) {
- const padBody = await getPadBody(page);
- await clearPadContent(page)
-
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
-
- // select this text element
- await $firstTextElement.selectText();
-
- const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
- await $insertunorderedlistButton.click();
-
- await padBody.locator('div').first().click();
- await page.keyboard.press('Home');
- await page.keyboard.press('Tab');
- await expect(padBody.locator('div').first().locator('.list-bullet2')).toHaveCount(1);
-
- await page.keyboard.press('Shift+Tab');
-
- await expect(padBody.locator('div').first().locator('.list-bullet1')).toHaveCount(1);
- });
- });
-
- test.describe('Pressing indent/outdent button in an UL increases and decreases indentation ' +
- 'and bullet / ol formatting', function () {
-
- test('indent and de-indent list item with indent button', async function ({page}) {
- const padBody = await getPadBody(page);
-
- // get the first text element out of the inner iframe
- const $firstTextElement = padBody.locator('div').first();
-
- // select this text element
- await $firstTextElement.selectText();
-
- const $insertunorderedlistButton = page.locator('.buttonicon-insertunorderedlist');
- await $insertunorderedlistButton.click();
-
- await page.locator('.buttonicon-indent').click();
-
- await expect(padBody.locator('div').first().locator('.list-bullet2')).toHaveCount(1);
- const outdentButton = page.locator('.buttonicon-outdent');
- await outdentButton.click();
-
- await expect(padBody.locator('div').first().locator('.list-bullet1')).toHaveCount(1);
- });
- });
+test.beforeEach(async ({ page }) => {
+ // create a new pad before each test run
+ await goToNewPad(page);
+});
+
+test.describe("unordered_list.js", () => {
+ test.describe("assign unordered list", () => {
+ test("insert unordered list text then removes by outdent", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
+ const originalText = await padBody.locator("div").first().textContent();
+
+ const $insertunorderedlistButton = page.locator(
+ ".buttonicon-insertunorderedlist",
+ );
+ await $insertunorderedlistButton.click();
+
+ await expect(padBody.locator("div").first()).toHaveText(originalText!);
+ await expect(padBody.locator("div ul li")).toHaveCount(1);
+
+ // remove indentation by bullet and ensure text string remains the same
+ const $outdentButton = page.locator(".buttonicon-outdent");
+ await $outdentButton.click();
+ await expect(padBody.locator("div").first()).toHaveText(originalText!);
+ });
+ });
+
+ test.describe("unassign unordered list", () => {
+ // create a new pad before each test run
+
+ test("insert unordered list text then remove by clicking list again", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
+ const originalText = await padBody.locator("div").first().textContent();
+
+ await padBody.locator("div").first().selectText();
+ const $insertunorderedlistButton = page.locator(
+ ".buttonicon-insertunorderedlist",
+ );
+ await $insertunorderedlistButton.click();
+
+ await expect(padBody.locator("div").first()).toHaveText(originalText!);
+ await expect(padBody.locator("div ul li")).toHaveCount(1);
+
+ // remove indentation by bullet and ensure text string remains the same
+ await $insertunorderedlistButton.click();
+ await expect(padBody.locator("div").locator("ul")).toHaveCount(0);
+ });
+ });
+
+ test.describe("keep unordered list on enter key", () => {
+ test("Keeps the unordered list on enter for the new line", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+ await expect(padBody.locator("div")).toHaveCount(1);
+
+ const $insertorderedlistButton = page.locator(
+ ".buttonicon-insertunorderedlist",
+ );
+ await $insertorderedlistButton.click();
+
+ // type a bit, make a line break and type again
+ const $firstTextElement = padBody.locator("div").first();
+ await $firstTextElement.click();
+ await page.keyboard.type("line 1");
+ await page.keyboard.press("Enter");
+ await page.keyboard.type("line 2");
+ await page.keyboard.press("Enter");
+
+ await expect(padBody.locator("div span")).toHaveCount(2);
+
+ const $newSecondLine = padBody.locator("div").nth(1);
+ await expect($newSecondLine.locator("ul")).toHaveCount(1);
+ await expect($newSecondLine).toHaveText("line 2");
+ });
+ });
+
+ test.describe("Pressing Tab in an UL increases and decreases indentation", () => {
+ test("indent and de-indent list item with keypress", async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
+
+ // select this text element
+ await $firstTextElement.selectText();
+
+ const $insertunorderedlistButton = page.locator(
+ ".buttonicon-insertunorderedlist",
+ );
+ await $insertunorderedlistButton.click();
+
+ await padBody.locator("div").first().click();
+ await page.keyboard.press("Home");
+ await page.keyboard.press("Tab");
+ await expect(
+ padBody.locator("div").first().locator(".list-bullet2"),
+ ).toHaveCount(1);
+
+ await page.keyboard.press("Shift+Tab");
+
+ await expect(
+ padBody.locator("div").first().locator(".list-bullet1"),
+ ).toHaveCount(1);
+ });
+ });
+
+ test.describe(
+ "Pressing indent/outdent button in an UL increases and decreases indentation " +
+ "and bullet / ol formatting",
+ () => {
+ test("indent and de-indent list item with indent button", async ({
+ page,
+ }) => {
+ const padBody = await getPadBody(page);
+
+ // get the first text element out of the inner iframe
+ const $firstTextElement = padBody.locator("div").first();
+
+ // select this text element
+ await $firstTextElement.selectText();
+
+ const $insertunorderedlistButton = page.locator(
+ ".buttonicon-insertunorderedlist",
+ );
+ await $insertunorderedlistButton.click();
+
+ await page.locator(".buttonicon-indent").click();
+
+ await expect(
+ padBody.locator("div").first().locator(".list-bullet2"),
+ ).toHaveCount(1);
+ const outdentButton = page.locator(".buttonicon-outdent");
+ await outdentButton.click();
+
+ await expect(
+ padBody.locator("div").first().locator(".list-bullet1"),
+ ).toHaveCount(1);
+ });
+ },
+ );
});
diff --git a/src/tests/frontend-new/specs/urls_become_clickable.spec.ts b/src/tests/frontend-new/specs/urls_become_clickable.spec.ts
index 0397502bc..b842ae65a 100644
--- a/src/tests/frontend-new/specs/urls_become_clickable.spec.ts
+++ b/src/tests/frontend-new/specs/urls_become_clickable.spec.ts
@@ -1,51 +1,59 @@
-import {expect, test} from "@playwright/test";
-import {clearPadContent, getPadBody, goToNewPad, writeToPad} from "../helper/padHelper";
+import { expect, test } from "@playwright/test";
+import {
+ clearPadContent,
+ getPadBody,
+ goToNewPad,
+ writeToPad,
+} from "../helper/padHelper";
-test.beforeEach(async ({ page })=>{
- await goToNewPad(page);
-})
-
-test.describe('entering a URL makes a link', function () {
- for (const url of ['https://etherpad.org', 'www.etherpad.org', 'https://www.etherpad.org']) {
- test(url, async function ({page}) {
- const padBody = await getPadBody(page);
- await clearPadContent(page)
- const url = 'https://etherpad.org';
- await writeToPad(page, url);
- await expect(padBody.locator('div').first()).toHaveText(url);
- await expect(padBody.locator('a')).toHaveText(url);
- await expect(padBody.locator('a')).toHaveAttribute('href', url);
- });
- }
+test.beforeEach(async ({ page }) => {
+ await goToNewPad(page);
});
-
-test.describe('special characters inside URL', async function () {
- for (const char of '-:@_.,~%+/?=!;()[]$\'*') {
- const url = `https://etherpad.org/${char}foo`;
- test(url, async function ({page}) {
- const padBody = await getPadBody(page);
- await clearPadContent(page)
- await padBody.click()
- await clearPadContent(page)
- await writeToPad(page, url);
- await expect(padBody.locator('div').first()).toHaveText(url);
- await expect(padBody.locator('a')).toHaveText(url);
- await expect(padBody.locator('a')).toHaveAttribute('href', url);
- });
- }
+test.describe("entering a URL makes a link", () => {
+ for (const url of [
+ "https://etherpad.org",
+ "www.etherpad.org",
+ "https://www.etherpad.org",
+ ]) {
+ test(url, async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+ const url = "https://etherpad.org";
+ await writeToPad(page, url);
+ await expect(padBody.locator("div").first()).toHaveText(url);
+ await expect(padBody.locator("a")).toHaveText(url);
+ await expect(padBody.locator("a")).toHaveAttribute("href", url);
+ });
+ }
});
-test.describe('punctuation after URL is ignored', ()=> {
- for (const char of ':.,;?!)]\'*') {
- const want = 'https://etherpad.org';
- const input = want + char;
- test(input, async function ({page}) {
- const padBody = await getPadBody(page);
- await clearPadContent(page)
- await writeToPad(page, input);
- await expect(padBody.locator('a')).toHaveCount(1);
- await expect(padBody.locator('a')).toHaveAttribute('href', want);
- });
- }
+test.describe("special characters inside URL", async () => {
+ for (const char of "-:@_.,~%+/?=!;()[]$'*") {
+ const url = `https://etherpad.org/${char}foo`;
+ test(url, async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+ await padBody.click();
+ await clearPadContent(page);
+ await writeToPad(page, url);
+ await expect(padBody.locator("div").first()).toHaveText(url);
+ await expect(padBody.locator("a")).toHaveText(url);
+ await expect(padBody.locator("a")).toHaveAttribute("href", url);
+ });
+ }
+});
+
+test.describe("punctuation after URL is ignored", () => {
+ for (const char of ":.,;?!)]'*") {
+ const want = "https://etherpad.org";
+ const input = want + char;
+ test(input, async ({ page }) => {
+ const padBody = await getPadBody(page);
+ await clearPadContent(page);
+ await writeToPad(page, input);
+ await expect(padBody.locator("a")).toHaveCount(1);
+ await expect(padBody.locator("a")).toHaveAttribute("href", want);
+ });
+ }
});
diff --git a/src/tests/frontend/cypress/cypress.config.js b/src/tests/frontend/cypress/cypress.config.js
index 3754350de..8badd6f8b 100644
--- a/src/tests/frontend/cypress/cypress.config.js
+++ b/src/tests/frontend/cypress/cypress.config.js
@@ -1,9 +1,9 @@
-const { defineConfig } = require('cypress')
+const { defineConfig } = require("cypress");
module.exports = defineConfig({
- e2e: {
- baseUrl: "http://127.0.0.1:9001",
- supportFile: false,
- specPattern: 'tests/frontend/cypress/integration/**/*.js'
- }
-})
+ e2e: {
+ baseUrl: "http://127.0.0.1:9001",
+ supportFile: false,
+ specPattern: "tests/frontend/cypress/integration/**/*.js",
+ },
+});
diff --git a/src/tests/frontend/cypress/integration/test.js b/src/tests/frontend/cypress/integration/test.js
index 893d4b669..416ecf338 100644
--- a/src/tests/frontend/cypress/integration/test.js
+++ b/src/tests/frontend/cypress/integration/test.js
@@ -1,23 +1,28 @@
-'use strict';
-
-Cypress.Commands.add('iframe', {prevSubject: 'element'},
- ($iframe) => new Cypress.Promise((resolve) => {
- $iframe.ready(() => {
- resolve($iframe.contents().find('body'));
- });
- }));
+Cypress.Commands.add(
+ "iframe",
+ { prevSubject: "element" },
+ ($iframe) =>
+ new Cypress.Promise((resolve) => {
+ $iframe.ready(() => {
+ resolve($iframe.contents().find("body"));
+ });
+ }),
+);
describe(__filename, () => {
- it('Pad content exists', () => {
- cy.visit('http://127.0.0.1:9001/p/test');
- cy.wait(10000); // wait for Minified JS to be built...
- cy.get('iframe[name="ace_outer"]', {timeout: 10000}).iframe()
- .find('.line-number:first')
- .should('have.text', '1');
- cy.get('iframe[name="ace_outer"]').iframe()
- .find('iframe[name="ace_inner"]').iframe()
- .find('.ace-line:first')
- .should('be.visible')
- .should('have.text', 'Welcome to Etherpad!');
- });
+ it("Pad content exists", () => {
+ cy.visit("http://127.0.0.1:9001/p/test");
+ cy.wait(10000); // wait for Minified JS to be built...
+ cy.get('iframe[name="ace_outer"]', { timeout: 10000 })
+ .iframe()
+ .find(".line-number:first")
+ .should("have.text", "1");
+ cy.get('iframe[name="ace_outer"]')
+ .iframe()
+ .find('iframe[name="ace_inner"]')
+ .iframe()
+ .find(".ace-line:first")
+ .should("be.visible")
+ .should("have.text", "Welcome to Etherpad!");
+ });
});
diff --git a/src/tests/frontend/easysync-helper.js b/src/tests/frontend/easysync-helper.js
index b4f770963..ff526c75d 100644
--- a/src/tests/frontend/easysync-helper.js
+++ b/src/tests/frontend/easysync-helper.js
@@ -1,222 +1,215 @@
-'use strict';
-
-const Changeset = require('../../static/js/Changeset');
-const AttributePool = require('../../static/js/AttributePool');
+const Changeset = require("../../static/js/Changeset");
+const AttributePool = require("../../static/js/AttributePool");
const randInt = (maxValue) => Math.floor(Math.random() * maxValue);
const poolOrArray = (attribs) => {
- if (attribs.getAttrib) {
- return attribs; // it's already an attrib pool
- } else {
- // assume it's an array of attrib strings to be split and added
- const p = new AttributePool();
- attribs.forEach((kv) => {
- p.putAttrib(kv.split(','));
- });
- return p;
- }
+ if (attribs.getAttrib) {
+ return attribs; // it's already an attrib pool
+ } else {
+ // assume it's an array of attrib strings to be split and added
+ const p = new AttributePool();
+ attribs.forEach((kv) => {
+ p.putAttrib(kv.split(","));
+ });
+ return p;
+ }
};
exports.poolOrArray = poolOrArray;
const randomInlineString = (len) => {
- const assem = Changeset.stringAssembler();
- for (let i = 0; i < len; i++) {
- assem.append(String.fromCharCode(randInt(26) + 97));
- }
- return assem.toString();
+ const assem = Changeset.stringAssembler();
+ for (let i = 0; i < len; i++) {
+ assem.append(String.fromCharCode(randInt(26) + 97));
+ }
+ return assem.toString();
};
const randomMultiline = (approxMaxLines, approxMaxCols) => {
- const numParts = randInt(approxMaxLines * 2) + 1;
- const txt = Changeset.stringAssembler();
- txt.append(randInt(2) ? '\n' : '');
- for (let i = 0; i < numParts; i++) {
- if ((i % 2) === 0) {
- if (randInt(10)) {
- txt.append(randomInlineString(randInt(approxMaxCols) + 1));
- } else {
- txt.append('\n');
- }
- } else {
- txt.append('\n');
- }
- }
- return txt.toString();
+ const numParts = randInt(approxMaxLines * 2) + 1;
+ const txt = Changeset.stringAssembler();
+ txt.append(randInt(2) ? "\n" : "");
+ for (let i = 0; i < numParts; i++) {
+ if (i % 2 === 0) {
+ if (randInt(10)) {
+ txt.append(randomInlineString(randInt(approxMaxCols) + 1));
+ } else {
+ txt.append("\n");
+ }
+ } else {
+ txt.append("\n");
+ }
+ }
+ return txt.toString();
};
exports.randomMultiline = randomMultiline;
const randomStringOperation = (numCharsLeft) => {
- let result;
- switch (randInt(11)) {
- case 0:
- {
- // insert char
- result = {
- insert: randomInlineString(1),
- };
- break;
- }
- case 1:
- {
- // delete char
- result = {
- remove: 1,
- };
- break;
- }
- case 2:
- {
- // skip char
- result = {
- skip: 1,
- };
- break;
- }
- case 3:
- {
- // insert small
- result = {
- insert: randomInlineString(randInt(4) + 1),
- };
- break;
- }
- case 4:
- {
- // delete small
- result = {
- remove: randInt(4) + 1,
- };
- break;
- }
- case 5:
- {
- // skip small
- result = {
- skip: randInt(4) + 1,
- };
- break;
- }
- case 6:
- {
- // insert multiline;
- result = {
- insert: randomMultiline(5, 20),
- };
- break;
- }
- case 7:
- {
- // delete multiline
- result = {
- remove: Math.round(numCharsLeft * Math.random() * Math.random()),
- };
- break;
- }
- case 8:
- {
- // skip multiline
- result = {
- skip: Math.round(numCharsLeft * Math.random() * Math.random()),
- };
- break;
- }
- case 9:
- {
- // delete to end
- result = {
- remove: numCharsLeft,
- };
- break;
- }
- case 10:
- {
- // skip to end
- result = {
- skip: numCharsLeft,
- };
- break;
- }
- }
- const maxOrig = numCharsLeft - 1;
- if ('remove' in result) {
- result.remove = Math.min(result.remove, maxOrig);
- } else if ('skip' in result) {
- result.skip = Math.min(result.skip, maxOrig);
- }
- return result;
+ let result;
+ switch (randInt(11)) {
+ case 0: {
+ // insert char
+ result = {
+ insert: randomInlineString(1),
+ };
+ break;
+ }
+ case 1: {
+ // delete char
+ result = {
+ remove: 1,
+ };
+ break;
+ }
+ case 2: {
+ // skip char
+ result = {
+ skip: 1,
+ };
+ break;
+ }
+ case 3: {
+ // insert small
+ result = {
+ insert: randomInlineString(randInt(4) + 1),
+ };
+ break;
+ }
+ case 4: {
+ // delete small
+ result = {
+ remove: randInt(4) + 1,
+ };
+ break;
+ }
+ case 5: {
+ // skip small
+ result = {
+ skip: randInt(4) + 1,
+ };
+ break;
+ }
+ case 6: {
+ // insert multiline;
+ result = {
+ insert: randomMultiline(5, 20),
+ };
+ break;
+ }
+ case 7: {
+ // delete multiline
+ result = {
+ remove: Math.round(numCharsLeft * Math.random() * Math.random()),
+ };
+ break;
+ }
+ case 8: {
+ // skip multiline
+ result = {
+ skip: Math.round(numCharsLeft * Math.random() * Math.random()),
+ };
+ break;
+ }
+ case 9: {
+ // delete to end
+ result = {
+ remove: numCharsLeft,
+ };
+ break;
+ }
+ case 10: {
+ // skip to end
+ result = {
+ skip: numCharsLeft,
+ };
+ break;
+ }
+ }
+ const maxOrig = numCharsLeft - 1;
+ if ("remove" in result) {
+ result.remove = Math.min(result.remove, maxOrig);
+ } else if ("skip" in result) {
+ result.skip = Math.min(result.skip, maxOrig);
+ }
+ return result;
};
const randomTwoPropAttribs = (opcode) => {
- // assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
- if (opcode === '-' || randInt(3)) {
- return '';
- } else if (randInt(3)) { // eslint-disable-line no-dupe-else-if
- if (opcode === '+' || randInt(2)) {
- return `*${Changeset.numToString(randInt(2) * 2 + 1)}`;
- } else {
- return `*${Changeset.numToString(randInt(2) * 2)}`;
- }
- } else if (opcode === '+' || randInt(4) === 0) {
- return '*1*3';
- } else {
- return ['*0*2', '*0*3', '*1*2'][randInt(3)];
- }
+ // assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
+ if (opcode === "-" || randInt(3)) {
+ return "";
+ } else if (randInt(3)) {
+ // eslint-disable-line no-dupe-else-if
+ if (opcode === "+" || randInt(2)) {
+ return `*${Changeset.numToString(randInt(2) * 2 + 1)}`;
+ } else {
+ return `*${Changeset.numToString(randInt(2) * 2)}`;
+ }
+ } else if (opcode === "+" || randInt(4) === 0) {
+ return "*1*3";
+ } else {
+ return ["*0*2", "*0*3", "*1*2"][randInt(3)];
+ }
};
const randomTestChangeset = (origText, withAttribs) => {
- const charBank = Changeset.stringAssembler();
- let textLeft = origText; // always keep final newline
- const outTextAssem = Changeset.stringAssembler();
- const opAssem = Changeset.smartOpAssembler();
- const oldLen = origText.length;
+ const charBank = Changeset.stringAssembler();
+ let textLeft = origText; // always keep final newline
+ const outTextAssem = Changeset.stringAssembler();
+ const opAssem = Changeset.smartOpAssembler();
+ const oldLen = origText.length;
- const nextOp = new Changeset.Op();
+ const nextOp = new Changeset.Op();
- const appendMultilineOp = (opcode, txt) => {
- nextOp.opcode = opcode;
- if (withAttribs) {
- nextOp.attribs = randomTwoPropAttribs(opcode);
- }
- txt.replace(/\n|[^\n]+/g, (t) => {
- if (t === '\n') {
- nextOp.chars = 1;
- nextOp.lines = 1;
- opAssem.append(nextOp);
- } else {
- nextOp.chars = t.length;
- nextOp.lines = 0;
- opAssem.append(nextOp);
- }
- return '';
- });
- };
+ const appendMultilineOp = (opcode, txt) => {
+ nextOp.opcode = opcode;
+ if (withAttribs) {
+ nextOp.attribs = randomTwoPropAttribs(opcode);
+ }
+ txt.replace(/\n|[^\n]+/g, (t) => {
+ if (t === "\n") {
+ nextOp.chars = 1;
+ nextOp.lines = 1;
+ opAssem.append(nextOp);
+ } else {
+ nextOp.chars = t.length;
+ nextOp.lines = 0;
+ opAssem.append(nextOp);
+ }
+ return "";
+ });
+ };
- const doOp = () => {
- const o = randomStringOperation(textLeft.length);
- if (o.insert) {
- const txt = o.insert;
- charBank.append(txt);
- outTextAssem.append(txt);
- appendMultilineOp('+', txt);
- } else if (o.skip) {
- const txt = textLeft.substring(0, o.skip);
- textLeft = textLeft.substring(o.skip);
- outTextAssem.append(txt);
- appendMultilineOp('=', txt);
- } else if (o.remove) {
- const txt = textLeft.substring(0, o.remove);
- textLeft = textLeft.substring(o.remove);
- appendMultilineOp('-', txt);
- }
- };
+ const doOp = () => {
+ const o = randomStringOperation(textLeft.length);
+ if (o.insert) {
+ const txt = o.insert;
+ charBank.append(txt);
+ outTextAssem.append(txt);
+ appendMultilineOp("+", txt);
+ } else if (o.skip) {
+ const txt = textLeft.substring(0, o.skip);
+ textLeft = textLeft.substring(o.skip);
+ outTextAssem.append(txt);
+ appendMultilineOp("=", txt);
+ } else if (o.remove) {
+ const txt = textLeft.substring(0, o.remove);
+ textLeft = textLeft.substring(o.remove);
+ appendMultilineOp("-", txt);
+ }
+ };
- while (textLeft.length > 1) doOp();
- for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
- const outText = `${outTextAssem.toString()}\n`;
- opAssem.endDocument();
- const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString());
- Changeset.checkRep(cs);
- return [cs, outText];
+ while (textLeft.length > 1) doOp();
+ for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
+ const outText = `${outTextAssem.toString()}\n`;
+ opAssem.endDocument();
+ const cs = Changeset.pack(
+ oldLen,
+ outText.length,
+ opAssem.toString(),
+ charBank.toString(),
+ );
+ Changeset.checkRep(cs);
+ return [cs, outText];
};
exports.randomTestChangeset = randomTestChangeset;
diff --git a/src/tests/frontend/helper.js b/src/tests/frontend/helper.js
index 18981897e..1650faf14 100644
--- a/src/tests/frontend/helper.js
+++ b/src/tests/frontend/helper.js
@@ -1,336 +1,390 @@
-'use strict';
-
const helper = {};
(() => {
- let $iframe;
+ let $iframe;
- helper.randomString = (len) => {
- const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
- let randomstring = '';
- for (let i = 0; i < len; i++) {
- const rnum = Math.floor(Math.random() * chars.length);
- randomstring += chars.substring(rnum, rnum + 1);
- }
- return randomstring;
- };
+ helper.randomString = (len) => {
+ const chars =
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ let randomstring = "";
+ for (let i = 0; i < len; i++) {
+ const rnum = Math.floor(Math.random() * chars.length);
+ randomstring += chars.substring(rnum, rnum + 1);
+ }
+ return randomstring;
+ };
- helper.getFrameJQuery = async ($iframe, includeSendkeys = false) => {
- const win = $iframe[0].contentWindow;
- const doc = win.document;
+ helper.getFrameJQuery = async ($iframe, includeSendkeys = false) => {
+ const win = $iframe[0].contentWindow;
+ const doc = win.document;
- const load = async (url) => {
- const elem = doc.createElement('script');
- elem.setAttribute('src', url);
- const p = new Promise((resolve, reject) => {
- const handler = (evt) => {
- elem.removeEventListener('load', handler);
- elem.removeEventListener('error', handler);
- if (evt.type === 'error') return reject(new Error(`failed to load ${url}`));
- resolve();
- };
- elem.addEventListener('load', handler);
- elem.addEventListener('error', handler);
- });
- doc.head.appendChild(elem);
- await p;
- };
+ const load = async (url) => {
+ const elem = doc.createElement("script");
+ elem.setAttribute("src", url);
+ const p = new Promise((resolve, reject) => {
+ const handler = (evt) => {
+ elem.removeEventListener("load", handler);
+ elem.removeEventListener("error", handler);
+ if (evt.type === "error")
+ return reject(new Error(`failed to load ${url}`));
+ resolve();
+ };
+ elem.addEventListener("load", handler);
+ elem.addEventListener("error", handler);
+ });
+ doc.head.appendChild(elem);
+ await p;
+ };
- if (!win.$) await load('../../static/js/vendors/jquery.js');
- // sendkeys.js depends on jQuery, so it cannot be loaded until jQuery has finished loading. (In
- // other words, do not load both jQuery and sendkeys inside a Promise.all() call.)
- if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js');
+ if (!win.$) await load("../../static/js/vendors/jquery.js");
+ // sendkeys.js depends on jQuery, so it cannot be loaded until jQuery has finished loading. (In
+ // other words, do not load both jQuery and sendkeys inside a Promise.all() call.)
+ if (!win.bililiteRange && includeSendkeys)
+ await load("../tests/frontend/lib/sendkeys.js");
- win.$.window = win;
- win.$.document = doc;
+ win.$.window = win;
+ win.$.document = doc;
- return win.$;
- };
+ return win.$;
+ };
- helper.clearSessionCookies = () => {
- window.Cookies.remove('token');
- window.Cookies.remove('language');
- };
+ helper.clearSessionCookies = () => {
+ window.Cookies.remove("token");
+ window.Cookies.remove("language");
+ };
- // Can only happen when the iframe exists, so we're doing it separately from other cookies
- helper.clearPadPrefCookie = () => {
- const {padcookie} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_cookie');
- padcookie.clear();
- };
+ // Can only happen when the iframe exists, so we're doing it separately from other cookies
+ helper.clearPadPrefCookie = () => {
+ const { padcookie } = helper.padChrome$.window.require(
+ "ep_etherpad-lite/static/js/pad_cookie",
+ );
+ padcookie.clear();
+ };
- // Overwrite all prefs in pad cookie.
- helper.setPadPrefCookie = (prefs) => {
- const {padcookie} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_cookie');
- padcookie.clear();
- for (const [key, value] of Object.entries(prefs)) padcookie.setPref(key, value);
- };
+ // Overwrite all prefs in pad cookie.
+ helper.setPadPrefCookie = (prefs) => {
+ const { padcookie } = helper.padChrome$.window.require(
+ "ep_etherpad-lite/static/js/pad_cookie",
+ );
+ padcookie.clear();
+ for (const [key, value] of Object.entries(prefs))
+ padcookie.setPref(key, value);
+ };
- // Functionality for knowing what key event type is required for tests
- let evtType = 'keydown';
- // if it's IE require keypress
- if (window.navigator.userAgent.indexOf('MSIE') > -1) {
- evtType = 'keypress';
- }
- // Edge also requires keypress.
- if (window.navigator.userAgent.indexOf('Edge') > -1) {
- evtType = 'keypress';
- }
- // Opera also requires keypress.
- if (window.navigator.userAgent.indexOf('OPR') > -1) {
- evtType = 'keypress';
- }
- helper.evtType = evtType;
+ // Functionality for knowing what key event type is required for tests
+ let evtType = "keydown";
+ // if it's IE require keypress
+ if (window.navigator.userAgent.indexOf("MSIE") > -1) {
+ evtType = "keypress";
+ }
+ // Edge also requires keypress.
+ if (window.navigator.userAgent.indexOf("Edge") > -1) {
+ evtType = "keypress";
+ }
+ // Opera also requires keypress.
+ if (window.navigator.userAgent.indexOf("OPR") > -1) {
+ evtType = "keypress";
+ }
+ helper.evtType = evtType;
- // Deprecated; use helper.aNewPad() instead.
- helper.newPad = (opts, id) => {
- if (!id) id = `FRONTEND_TEST_${helper.randomString(20)}`;
- opts = Object.assign({id}, typeof opts === 'function' ? {cb: opts} : opts);
- const {cb = (err) => { if (err != null) throw err; }} = opts;
- delete opts.cb;
- helper.aNewPad(opts).then((id) => cb(null, id), (err) => cb(err || new Error(err)));
- return id;
- };
+ // Deprecated; use helper.aNewPad() instead.
+ helper.newPad = (opts, id) => {
+ if (!id) id = `FRONTEND_TEST_${helper.randomString(20)}`;
+ opts = Object.assign(
+ { id },
+ typeof opts === "function" ? { cb: opts } : opts,
+ );
+ const {
+ cb = (err) => {
+ if (err != null) throw err;
+ },
+ } = opts;
+ delete opts.cb;
+ helper.aNewPad(opts).then(
+ (id) => cb(null, id),
+ (err) => cb(err || new Error(err)),
+ );
+ return id;
+ };
- helper.aNewPad = async (opts = {}) => {
- opts = Object.assign({
- _retry: 0,
- clearCookies: true,
- id: `FRONTEND_TEST_${helper.randomString(20)}`,
- hookFns: {},
- }, opts);
+ helper.aNewPad = async (opts = {}) => {
+ opts = Object.assign(
+ {
+ _retry: 0,
+ clearCookies: true,
+ id: `FRONTEND_TEST_${helper.randomString(20)}`,
+ hookFns: {},
+ },
+ opts,
+ );
- // Set up socket.io spying as early as possible.
- /** chat messages received */
- helper.chatMessages = [];
- /** changeset commits from the server */
- helper.commits = [];
- /** userInfo messages from the server */
- helper.userInfos = [];
- if (opts.hookFns._socketCreated == null) opts.hookFns._socketCreated = [];
- opts.hookFns._socketCreated.unshift(() => helper.spyOnSocketIO());
+ // Set up socket.io spying as early as possible.
+ /** chat messages received */
+ helper.chatMessages = [];
+ /** changeset commits from the server */
+ helper.commits = [];
+ /** userInfo messages from the server */
+ helper.userInfos = [];
+ if (opts.hookFns._socketCreated == null) opts.hookFns._socketCreated = [];
+ opts.hookFns._socketCreated.unshift(() => helper.spyOnSocketIO());
- // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah.
- let encodedParams;
- if (opts.params) {
- encodedParams = `?${$.param(opts.params)}`;
- }
- let hash;
- if (opts.hash) {
- hash = `#${opts.hash}`;
- }
+ // if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah.
+ let encodedParams;
+ if (opts.params) {
+ encodedParams = `?${$.param(opts.params)}`;
+ }
+ let hash;
+ if (opts.hash) {
+ hash = `#${opts.hash}`;
+ }
- // clear cookies
- if (opts.clearCookies) {
- helper.clearSessionCookies();
- }
+ // clear cookies
+ if (opts.clearCookies) {
+ helper.clearSessionCookies();
+ }
- $iframe = $(``);
+ $iframe = $(
+ ``,
+ );
- // clean up inner iframe references
- helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null;
+ // clean up inner iframe references
+ helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null;
- // remove old iframe
- $('#iframe-container iframe').remove();
- // set new iframe
- $('#iframe-container').append($iframe);
- await Promise.all([
- new Promise((resolve) => $iframe.one('load', resolve)),
- // Install the hook functions as early as possible because some of them fire right away.
- new Promise((resolve, reject) => {
- if ($iframe[0].contentWindow._postPluginUpdateForTestingDone) {
- return reject(new Error(
- 'failed to set _postPluginUpdateForTesting before it would have been called'));
- }
- $iframe[0].contentWindow._postPluginUpdateForTesting = () => {
- const {hooks} =
- $iframe[0].contentWindow.require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
- for (const [hookName, hookFns] of Object.entries(opts.hookFns)) {
- if (hooks[hookName] == null) hooks[hookName] = [];
- hooks[hookName].push(
- ...hookFns.map((hookFn) => ({hook_name: hookName, hook_fn: hookFn})));
- }
- resolve();
- };
- }),
- ]);
- helper.padChrome$ = await helper.getFrameJQuery($('#iframe-container iframe'), true);
- helper.padChrome$.padeditor =
- helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor;
- if (opts.clearCookies) {
- helper.clearPadPrefCookie();
- }
- if (opts.padPrefs) {
- helper.setPadPrefCookie(opts.padPrefs);
- }
- const $loading = helper.padChrome$('#editorloadingbox');
- const $container = helper.padChrome$('#editorcontainer');
- try {
- await helper.waitForPromise(
- () => !$loading.is(':visible') && $container.hasClass('initialized'), 10000);
- } catch (err) {
- if (opts._retry++ >= 4) throw new Error('Pad never loaded');
- return await helper.aNewPad(opts);
- }
- helper.padOuter$ =
- await helper.getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'), false);
- helper.padInner$ =
- await helper.getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'), true);
+ // remove old iframe
+ $("#iframe-container iframe").remove();
+ // set new iframe
+ $("#iframe-container").append($iframe);
+ await Promise.all([
+ new Promise((resolve) => $iframe.one("load", resolve)),
+ // Install the hook functions as early as possible because some of them fire right away.
+ new Promise((resolve, reject) => {
+ if ($iframe[0].contentWindow._postPluginUpdateForTestingDone) {
+ return reject(
+ new Error(
+ "failed to set _postPluginUpdateForTesting before it would have been called",
+ ),
+ );
+ }
+ $iframe[0].contentWindow._postPluginUpdateForTesting = () => {
+ const { hooks } = $iframe[0].contentWindow.require(
+ "ep_etherpad-lite/static/js/pluginfw/plugin_defs",
+ );
+ for (const [hookName, hookFns] of Object.entries(opts.hookFns)) {
+ if (hooks[hookName] == null) hooks[hookName] = [];
+ hooks[hookName].push(
+ ...hookFns.map((hookFn) => ({
+ hook_name: hookName,
+ hook_fn: hookFn,
+ })),
+ );
+ }
+ resolve();
+ };
+ }),
+ ]);
+ helper.padChrome$ = await helper.getFrameJQuery(
+ $("#iframe-container iframe"),
+ true,
+ );
+ helper.padChrome$.padeditor = helper.padChrome$.window.require(
+ "ep_etherpad-lite/static/js/pad_editor",
+ ).padeditor;
+ if (opts.clearCookies) {
+ helper.clearPadPrefCookie();
+ }
+ if (opts.padPrefs) {
+ helper.setPadPrefCookie(opts.padPrefs);
+ }
+ const $loading = helper.padChrome$("#editorloadingbox");
+ const $container = helper.padChrome$("#editorcontainer");
+ try {
+ await helper.waitForPromise(
+ () => !$loading.is(":visible") && $container.hasClass("initialized"),
+ 10000,
+ );
+ } catch (err) {
+ if (opts._retry++ >= 4) throw new Error("Pad never loaded");
+ return await helper.aNewPad(opts);
+ }
+ helper.padOuter$ = await helper.getFrameJQuery(
+ helper.padChrome$('iframe[name="ace_outer"]'),
+ false,
+ );
+ helper.padInner$ = await helper.getFrameJQuery(
+ helper.padOuter$('iframe[name="ace_inner"]'),
+ true,
+ );
- // disable all animations, this makes tests faster and easier
- helper.padChrome$.fx.off = true;
- helper.padOuter$.fx.off = true;
- helper.padInner$.fx.off = true;
+ // disable all animations, this makes tests faster and easier
+ helper.padChrome$.fx.off = true;
+ helper.padOuter$.fx.off = true;
+ helper.padInner$.fx.off = true;
- // Don't return opts.id -- the server might have redirected the browser to a transformed version
- // of the requested pad ID.
- return helper.padChrome$.window.clientVars.padId;
- };
+ // Don't return opts.id -- the server might have redirected the browser to a transformed version
+ // of the requested pad ID.
+ return helper.padChrome$.window.clientVars.padId;
+ };
- helper.newAdmin = async (page) => {
- // define the iframe
- $iframe = $(``);
+ helper.newAdmin = async (page) => {
+ // define the iframe
+ $iframe = $(``);
- // clean up inner iframe references
- helper.admin$ = null;
+ // clean up inner iframe references
+ helper.admin$ = null;
- // remove old iframe
- $('#iframe-container iframe').remove();
- // set new iframe
- $('#iframe-container').append($iframe);
- $iframe.one('load', async () => {
- helper.admin$ = await helper.getFrameJQuery($('#iframe-container iframe'), false);
- });
- };
+ // remove old iframe
+ $("#iframe-container iframe").remove();
+ // set new iframe
+ $("#iframe-container").append($iframe);
+ $iframe.one("load", async () => {
+ helper.admin$ = await helper.getFrameJQuery(
+ $("#iframe-container iframe"),
+ false,
+ );
+ });
+ };
- helper.waitFor = (conditionFunc, timeoutTime = 1900, intervalTime = 10) => {
- // Create an Error object to use if the condition is never satisfied. This is created here so
- // that the Error has a useful stack trace associated with it.
- const timeoutError =
- new Error(`waitFor condition never became true ${conditionFunc.toString()}`);
- const deferred = new $.Deferred();
+ helper.waitFor = (conditionFunc, timeoutTime = 1900, intervalTime = 10) => {
+ // Create an Error object to use if the condition is never satisfied. This is created here so
+ // that the Error has a useful stack trace associated with it.
+ const timeoutError = new Error(
+ `waitFor condition never became true ${conditionFunc.toString()}`,
+ );
+ const deferred = new $.Deferred();
- const _fail = deferred.fail.bind(deferred);
- let listenForFail = false;
- deferred.fail = (...args) => {
- listenForFail = true;
- return _fail(...args);
- };
+ const _fail = deferred.fail.bind(deferred);
+ let listenForFail = false;
+ deferred.fail = (...args) => {
+ listenForFail = true;
+ return _fail(...args);
+ };
- const check = async () => {
- try {
- if (!await conditionFunc()) return;
- deferred.resolve();
- } catch (err) {
- deferred.reject(err);
- }
- clearInterval(intervalCheck);
- clearTimeout(timeout);
- };
+ const check = async () => {
+ try {
+ if (!(await conditionFunc())) return;
+ deferred.resolve();
+ } catch (err) {
+ deferred.reject(err);
+ }
+ clearInterval(intervalCheck);
+ clearTimeout(timeout);
+ };
- const intervalCheck = setInterval(check, intervalTime);
+ const intervalCheck = setInterval(check, intervalTime);
- const timeout = setTimeout(() => {
- clearInterval(intervalCheck);
- deferred.reject(timeoutError);
+ const timeout = setTimeout(() => {
+ clearInterval(intervalCheck);
+ deferred.reject(timeoutError);
- if (!listenForFail) {
- throw timeoutError;
- }
- }, timeoutTime);
+ if (!listenForFail) {
+ throw timeoutError;
+ }
+ }, timeoutTime);
- // Check right away to avoid an unnecessary sleep if the condition is already true.
- check();
+ // Check right away to avoid an unnecessary sleep if the condition is already true.
+ check();
- return deferred;
- };
+ return deferred;
+ };
- /**
- * Same as `waitFor` but using Promises
- *
- * @returns {Promise}
- *
- */
- // Note: waitFor() has a strange API: On timeout it rejects, but it also throws an uncatchable
- // exception unless .fail() has been called. That uncatchable exception is disabled here by
- // passing a no-op function to .fail().
- helper.waitForPromise = async (...args) => await helper.waitFor(...args).fail(() => {});
+ /**
+ * Same as `waitFor` but using Promises
+ *
+ * @returns {Promise}
+ *
+ */
+ // Note: waitFor() has a strange API: On timeout it rejects, but it also throws an uncatchable
+ // exception unless .fail() has been called. That uncatchable exception is disabled here by
+ // passing a no-op function to .fail().
+ helper.waitForPromise = async (...args) =>
+ await helper.waitFor(...args).fail(() => {});
- helper.selectLines = ($startLine, $endLine, startOffset, endOffset) => {
- // if no offset is provided, use beginning of start line and end of end line
- startOffset = startOffset || 0;
- endOffset = endOffset === undefined ? $endLine.text().length : endOffset;
+ helper.selectLines = ($startLine, $endLine, startOffset, endOffset) => {
+ // if no offset is provided, use beginning of start line and end of end line
+ startOffset = startOffset || 0;
+ endOffset = endOffset === undefined ? $endLine.text().length : endOffset;
- const inner$ = helper.padInner$;
- const selection = inner$.document.getSelection();
- const range = selection.getRangeAt(0);
+ const inner$ = helper.padInner$;
+ const selection = inner$.document.getSelection();
+ const range = selection.getRangeAt(0);
- const start = getTextNodeAndOffsetOf($startLine, startOffset);
- const end = getTextNodeAndOffsetOf($endLine, endOffset);
+ const start = getTextNodeAndOffsetOf($startLine, startOffset);
+ const end = getTextNodeAndOffsetOf($endLine, endOffset);
- range.setStart(start.node, start.offset);
- range.setEnd(end.node, end.offset);
+ range.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
- selection.removeAllRanges();
- selection.addRange(range);
- };
+ selection.removeAllRanges();
+ selection.addRange(range);
+ };
- // Temporarily reduces minimum time between commits and calls the provided function with a single
- // argument: a function that immediately incorporates all pad edits (as opposed to waiting for the
- // idle timer to fire).
- helper.withFastCommit = async (fn) => {
- const incorp = () => helper.padChrome$.padeditor.ace.callWithAce(
- (ace) => ace.ace_inCallStackIfNecessary('helper.edit', () => ace.ace_fastIncorp()));
- const cc = helper.padChrome$.window.pad.collabClient;
- const {commitDelay} = cc;
- cc.commitDelay = 0;
- try {
- return await fn(incorp);
- } finally {
- cc.commitDelay = commitDelay;
- }
- };
+ // Temporarily reduces minimum time between commits and calls the provided function with a single
+ // argument: a function that immediately incorporates all pad edits (as opposed to waiting for the
+ // idle timer to fire).
+ helper.withFastCommit = async (fn) => {
+ const incorp = () =>
+ helper.padChrome$.padeditor.ace.callWithAce((ace) =>
+ ace.ace_inCallStackIfNecessary("helper.edit", () =>
+ ace.ace_fastIncorp(),
+ ),
+ );
+ const cc = helper.padChrome$.window.pad.collabClient;
+ const { commitDelay } = cc;
+ cc.commitDelay = 0;
+ try {
+ return await fn(incorp);
+ } finally {
+ cc.commitDelay = commitDelay;
+ }
+ };
- const getTextNodeAndOffsetOf = ($targetLine, targetOffsetAtLine) => {
- const $textNodes = $targetLine.find('*').contents().filter(function () {
- return this.nodeType === Node.TEXT_NODE;
- });
+ const getTextNodeAndOffsetOf = ($targetLine, targetOffsetAtLine) => {
+ const $textNodes = $targetLine
+ .find("*")
+ .contents()
+ .filter(function () {
+ return this.nodeType === Node.TEXT_NODE;
+ });
- // search node where targetOffsetAtLine is reached, and its 'inner offset'
- let textNodeWhereOffsetIs = null;
- let offsetBeforeTextNode = 0;
- let offsetInsideTextNode = 0;
- $textNodes.each((index, element) => {
- const elementTotalOffset = element.textContent.length;
- textNodeWhereOffsetIs = element;
- offsetInsideTextNode = targetOffsetAtLine - offsetBeforeTextNode;
+ // search node where targetOffsetAtLine is reached, and its 'inner offset'
+ let textNodeWhereOffsetIs = null;
+ let offsetBeforeTextNode = 0;
+ let offsetInsideTextNode = 0;
+ $textNodes.each((index, element) => {
+ const elementTotalOffset = element.textContent.length;
+ textNodeWhereOffsetIs = element;
+ offsetInsideTextNode = targetOffsetAtLine - offsetBeforeTextNode;
- const foundTextNode = offsetBeforeTextNode + elementTotalOffset >= targetOffsetAtLine;
- if (foundTextNode) {
- return false; // stop .each by returning false
- }
+ const foundTextNode =
+ offsetBeforeTextNode + elementTotalOffset >= targetOffsetAtLine;
+ if (foundTextNode) {
+ return false; // stop .each by returning false
+ }
- offsetBeforeTextNode += elementTotalOffset;
- });
+ offsetBeforeTextNode += elementTotalOffset;
+ });
- // edge cases
- if (textNodeWhereOffsetIs == null) {
- // there was no text node inside $targetLine, so it is an empty line ( ).
- // Use beginning of line
- textNodeWhereOffsetIs = $targetLine.get(0);
- offsetInsideTextNode = 0;
- }
- // avoid errors if provided targetOffsetAtLine is higher than line offset (maxOffset).
- // Use max allowed instead
- const maxOffset = textNodeWhereOffsetIs.textContent.length;
- offsetInsideTextNode = Math.min(offsetInsideTextNode, maxOffset);
+ // edge cases
+ if (textNodeWhereOffsetIs == null) {
+ // there was no text node inside $targetLine, so it is an empty line ( ).
+ // Use beginning of line
+ textNodeWhereOffsetIs = $targetLine.get(0);
+ offsetInsideTextNode = 0;
+ }
+ // avoid errors if provided targetOffsetAtLine is higher than line offset (maxOffset).
+ // Use max allowed instead
+ const maxOffset = textNodeWhereOffsetIs.textContent.length;
+ offsetInsideTextNode = Math.min(offsetInsideTextNode, maxOffset);
- return {
- node: textNodeWhereOffsetIs,
- offset: offsetInsideTextNode,
- };
- };
+ return {
+ node: textNodeWhereOffsetIs,
+ offset: offsetInsideTextNode,
+ };
+ };
- /* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/
- window.console = window.console || {};
- window.console.log = window.console.log || (() => {});
+ /* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/
+ window.console = window.console || {};
+ window.console.log = window.console.log || (() => {});
})();
diff --git a/src/tests/frontend/helper/methods.js b/src/tests/frontend/helper/methods.js
index 97ea6a643..8d167d80a 100644
--- a/src/tests/frontend/helper/methods.js
+++ b/src/tests/frontend/helper/methods.js
@@ -1,22 +1,20 @@
-'use strict';
-
/**
* Spys on socket.io messages and saves them into several arrays
* that are visible in tests
*/
helper.spyOnSocketIO = () => {
- helper.contentWindow().pad.socket.on('message', (msg) => {
- if (msg.type !== 'COLLABROOM') return;
- if (msg.data.type === 'ACCEPT_COMMIT') {
- helper.commits.push(msg);
- } else if (msg.data.type === 'USER_NEWINFO') {
- helper.userInfos.push(msg);
- } else if (msg.data.type === 'CHAT_MESSAGE') {
- helper.chatMessages.push(msg.data.message);
- } else if (msg.data.type === 'CHAT_MESSAGES') {
- helper.chatMessages.push(...msg.data.messages);
- }
- });
+ helper.contentWindow().pad.socket.on("message", (msg) => {
+ if (msg.type !== "COLLABROOM") return;
+ if (msg.data.type === "ACCEPT_COMMIT") {
+ helper.commits.push(msg);
+ } else if (msg.data.type === "USER_NEWINFO") {
+ helper.userInfos.push(msg);
+ } else if (msg.data.type === "CHAT_MESSAGE") {
+ helper.chatMessages.push(msg.data.message);
+ } else if (msg.data.type === "CHAT_MESSAGES") {
+ helper.chatMessages.push(...msg.data.messages);
+ }
+ });
};
/**
@@ -32,13 +30,16 @@ helper.spyOnSocketIO = () => {
*
*/
helper.edit = async (message, line) => {
- const editsNum = helper.commits.length;
- line = line ? line - 1 : 0;
- await helper.withFastCommit(async (incorp) => {
- helper.linesDiv()[line].sendkeys(message);
- incorp();
- await helper.waitForPromise(() => editsNum + 1 === helper.commits.length, 10000);
- });
+ const editsNum = helper.commits.length;
+ line = line ? line - 1 : 0;
+ await helper.withFastCommit(async (incorp) => {
+ helper.linesDiv()[line].sendkeys(message);
+ incorp();
+ await helper.waitForPromise(
+ () => editsNum + 1 === helper.commits.length,
+ 10000,
+ );
+ });
};
/**
@@ -49,7 +50,13 @@ helper.edit = async (message, line) => {
*
* @returns {Array.} array of divs
*/
-helper.linesDiv = () => helper.padInner$('.ace-line').map(function () { return $(this); }).get();
+helper.linesDiv = () =>
+ helper
+ .padInner$(".ace-line")
+ .map(function () {
+ return $(this);
+ })
+ .get();
/**
* The pad text as an array of lines
@@ -64,8 +71,9 @@ helper.textLines = () => helper.linesDiv().map((div) => div.text());
*
* @returns {string}
*/
-helper.defaultText =
- () => helper.padChrome$.window.clientVars.collab_client_vars.initialAttributedText.text;
+helper.defaultText = () =>
+ helper.padChrome$.window.clientVars.collab_client_vars.initialAttributedText
+ .text;
/**
* Sends a chat `message` via `sendKeys`
@@ -82,9 +90,11 @@ helper.defaultText =
* @returns {Promise}
*/
helper.sendChatMessage = async (message) => {
- const noOfChatMessages = helper.chatMessages.length;
- helper.padChrome$('#chatinput').sendkeys(message);
- await helper.waitForPromise(() => noOfChatMessages + 1 === helper.chatMessages.length);
+ const noOfChatMessages = helper.chatMessages.length;
+ helper.padChrome$("#chatinput").sendkeys(message);
+ await helper.waitForPromise(
+ () => noOfChatMessages + 1 === helper.chatMessages.length,
+ );
};
/**
@@ -93,9 +103,9 @@ helper.sendChatMessage = async (message) => {
* @returns {Promise}
*/
helper.showSettings = async () => {
- if (helper.isSettingsShown()) return;
- helper.settingsButton().trigger('click');
- await helper.waitForPromise(() => helper.isSettingsShown(), 2000);
+ if (helper.isSettingsShown()) return;
+ helper.settingsButton().trigger("click");
+ await helper.waitForPromise(() => helper.isSettingsShown(), 2000);
};
/**
@@ -105,9 +115,9 @@ helper.showSettings = async () => {
* @todo untested
*/
helper.hideSettings = async () => {
- if (!helper.isSettingsShown()) return;
- helper.settingsButton().trigger('click');
- await helper.waitForPromise(() => !helper.isSettingsShown(), 2000);
+ if (!helper.isSettingsShown()) return;
+ helper.settingsButton().trigger("click");
+ await helper.waitForPromise(() => !helper.isSettingsShown(), 2000);
};
/**
@@ -117,10 +127,10 @@ helper.hideSettings = async () => {
* @returns {Promise}
*/
helper.enableStickyChatviaSettings = async () => {
- const stickyChat = helper.padChrome$('#options-stickychat');
- if (!helper.isSettingsShown() || stickyChat.is(':checked')) return;
- stickyChat.trigger('click');
- await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
+ const stickyChat = helper.padChrome$("#options-stickychat");
+ if (!helper.isSettingsShown() || stickyChat.is(":checked")) return;
+ stickyChat.trigger("click");
+ await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
};
/**
@@ -130,10 +140,10 @@ helper.enableStickyChatviaSettings = async () => {
* @returns {Promise}
*/
helper.disableStickyChatviaSettings = async () => {
- const stickyChat = helper.padChrome$('#options-stickychat');
- if (!helper.isSettingsShown() || !stickyChat.is(':checked')) return;
- stickyChat.trigger('click');
- await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
+ const stickyChat = helper.padChrome$("#options-stickychat");
+ if (!helper.isSettingsShown() || !stickyChat.is(":checked")) return;
+ stickyChat.trigger("click");
+ await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
};
/**
@@ -143,10 +153,10 @@ helper.disableStickyChatviaSettings = async () => {
* @returns {Promise}
*/
helper.enableStickyChatviaIcon = async () => {
- const stickyChat = helper.padChrome$('#titlesticky');
- if (!helper.isChatboxShown() || helper.isChatboxSticky()) return;
- stickyChat.trigger('click');
- await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
+ const stickyChat = helper.padChrome$("#titlesticky");
+ if (!helper.isChatboxShown() || helper.isChatboxSticky()) return;
+ stickyChat.trigger("click");
+ await helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
};
/**
@@ -156,9 +166,9 @@ helper.enableStickyChatviaIcon = async () => {
* @returns {Promise}
*/
helper.disableStickyChatviaIcon = async () => {
- if (!helper.isChatboxShown() || !helper.isChatboxSticky()) return;
- helper.titlecross().trigger('click');
- await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
+ if (!helper.isChatboxShown() || !helper.isChatboxSticky()) return;
+ helper.titlecross().trigger("click");
+ await helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
};
/**
@@ -174,11 +184,14 @@ helper.disableStickyChatviaIcon = async () => {
* goto rev 0 and then via the same method to rev 5. Use buttons instead
*/
helper.gotoTimeslider = async (revision) => {
- revision = Number.isInteger(revision) ? `#${revision}` : '';
- helper.padChrome$.window.location.href =
- `${helper.padChrome$.window.location.pathname}/timeslider${revision}`;
- await helper.waitForPromise(() => helper.timesliderTimerTime() &&
- !Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()), 10000);
+ revision = Number.isInteger(revision) ? `#${revision}` : "";
+ helper.padChrome$.window.location.href = `${helper.padChrome$.window.location.pathname}/timeslider${revision}`;
+ await helper.waitForPromise(
+ () =>
+ helper.timesliderTimerTime() &&
+ !Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()),
+ 10000,
+ );
};
/**
@@ -189,14 +202,14 @@ helper.gotoTimeslider = async (revision) => {
* @param {number} X coordinate
*/
helper.sliderClick = (X) => {
- const sliderBar = helper.sliderBar();
- const edown = new jQuery.Event('mousedown');
- const eup = new jQuery.Event('mouseup');
- edown.clientX = eup.clientX = X;
- edown.clientY = eup.clientY = sliderBar.offset().top;
+ const sliderBar = helper.sliderBar();
+ const edown = new jQuery.Event("mousedown");
+ const eup = new jQuery.Event("mouseup");
+ edown.clientX = eup.clientX = X;
+ edown.clientY = eup.clientY = sliderBar.offset().top;
- sliderBar.trigger(edown);
- sliderBar.trigger(eup);
+ sliderBar.trigger(edown);
+ sliderBar.trigger(eup);
};
/**
@@ -204,26 +217,34 @@ helper.sliderClick = (X) => {
*
* @returns {Array.} lines of text
*/
-helper.timesliderTextLines = () => helper.contentWindow().$('.ace-line').map(function () {
- return $(this).text();
-}).get();
+helper.timesliderTextLines = () =>
+ helper
+ .contentWindow()
+ .$(".ace-line")
+ .map(function () {
+ return $(this).text();
+ })
+ .get();
-helper.padIsEmpty = () => (
- !helper.padInner$.document.getSelection().isCollapsed ||
- (helper.padInner$('div').length === 1 && helper.padInner$('div').first().html() === ' '));
+helper.padIsEmpty = () =>
+ !helper.padInner$.document.getSelection().isCollapsed ||
+ (helper.padInner$("div").length === 1 &&
+ helper.padInner$("div").first().html() === " ");
helper.clearPad = async () => {
- if (helper.padIsEmpty()) return;
- const commitsBefore = helper.commits.length;
- const lines = helper.linesDiv();
- helper.selectLines(lines[0], lines[lines.length - 1]);
- await helper.waitForPromise(() => !helper.padInner$.document.getSelection().isCollapsed);
- const e = new helper.padInner$.Event(helper.evtType);
- e.keyCode = 8; // delete key
- await helper.withFastCommit(async (incorp) => {
- helper.padInner$('#innerdocbody').trigger(e);
- incorp();
- await helper.waitForPromise(helper.padIsEmpty);
- await helper.waitForPromise(() => helper.commits.length > commitsBefore);
- });
+ if (helper.padIsEmpty()) return;
+ const commitsBefore = helper.commits.length;
+ const lines = helper.linesDiv();
+ helper.selectLines(lines[0], lines[lines.length - 1]);
+ await helper.waitForPromise(
+ () => !helper.padInner$.document.getSelection().isCollapsed,
+ );
+ const e = new helper.padInner$.Event(helper.evtType);
+ e.keyCode = 8; // delete key
+ await helper.withFastCommit(async (incorp) => {
+ helper.padInner$("#innerdocbody").trigger(e);
+ incorp();
+ await helper.waitForPromise(helper.padIsEmpty);
+ await helper.waitForPromise(() => helper.commits.length > commitsBefore);
+ });
};
diff --git a/src/tests/frontend/helper/multipleUsers.js b/src/tests/frontend/helper/multipleUsers.js
index 831bf403e..8aa657935 100644
--- a/src/tests/frontend/helper/multipleUsers.js
+++ b/src/tests/frontend/helper/multipleUsers.js
@@ -1,93 +1,103 @@
-'use strict';
+const getCookies = () =>
+ helper.padChrome$.window.require("ep_etherpad-lite/static/js/pad_utils")
+ .Cookies;
-const getCookies =
- () => helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_utils').Cookies;
+const setToken = (token) => getCookies().set("token", token);
-const setToken = (token) => getCookies().set('token', token);
-
-const getToken = () => getCookies().get('token');
+const getToken = () => getCookies().get("token");
const startActingLike = (user) => {
- helper.padChrome$ = user.padChrome$;
- helper.padOuter$ = user.padOuter$;
- helper.padInner$ = user.padInner$;
- if (helper.padChrome$) setToken(user.token);
+ helper.padChrome$ = user.padChrome$;
+ helper.padOuter$ = user.padOuter$;
+ helper.padInner$ = user.padInner$;
+ if (helper.padChrome$) setToken(user.token);
};
-const clearToken = () => getCookies().remove('token');
+const clearToken = () => getCookies().remove("token");
helper.multipleUsers = {
- _user0: null,
- _user1: null,
+ _user0: null,
+ _user1: null,
- // open the same pad on different frames (allows concurrent editions to pad)
- async init() {
- this._user0 = {
- $frame: $('#iframe-container iframe'),
- token: getToken(),
- // we'll switch between pads, need to store current values of helper.pad*
- // to be able to restore those values later
- padChrome$: helper.padChrome$,
- padOuter$: helper.padOuter$,
- padInner$: helper.padInner$,
- };
- this._user1 = {};
- // Force generation of a new token.
- clearToken();
- // need to perform as the other user, otherwise we'll get the userdup error message
- await this.performAsOtherUser(this._createUser1Frame.bind(this));
- },
+ // open the same pad on different frames (allows concurrent editions to pad)
+ async init() {
+ this._user0 = {
+ $frame: $("#iframe-container iframe"),
+ token: getToken(),
+ // we'll switch between pads, need to store current values of helper.pad*
+ // to be able to restore those values later
+ padChrome$: helper.padChrome$,
+ padOuter$: helper.padOuter$,
+ padInner$: helper.padInner$,
+ };
+ this._user1 = {};
+ // Force generation of a new token.
+ clearToken();
+ // need to perform as the other user, otherwise we'll get the userdup error message
+ await this.performAsOtherUser(this._createUser1Frame.bind(this));
+ },
- async performAsOtherUser(action) {
- startActingLike(this._user1);
- await action();
- startActingLike(this._user0);
- },
+ async performAsOtherUser(action) {
+ startActingLike(this._user1);
+ await action();
+ startActingLike(this._user0);
+ },
- close() {
- this._user0.$frame.attr('style', ''); // make the default ocopy the full height
- this._user1.$frame.remove();
- },
+ close() {
+ this._user0.$frame.attr("style", ""); // make the default ocopy the full height
+ this._user1.$frame.remove();
+ },
- async _loadJQueryForUser1Frame() {
- this._user1.padChrome$ = await helper.getFrameJQuery(this._user1.$frame, true);
- this._user1.padOuter$ =
- await helper.getFrameJQuery(this._user1.padChrome$('iframe[name="ace_outer"]'), false);
- this._user1.padInner$ =
- await helper.getFrameJQuery(this._user1.padOuter$('iframe[name="ace_inner"]'), true);
+ async _loadJQueryForUser1Frame() {
+ this._user1.padChrome$ = await helper.getFrameJQuery(
+ this._user1.$frame,
+ true,
+ );
+ this._user1.padOuter$ = await helper.getFrameJQuery(
+ this._user1.padChrome$('iframe[name="ace_outer"]'),
+ false,
+ );
+ this._user1.padInner$ = await helper.getFrameJQuery(
+ this._user1.padOuter$('iframe[name="ace_inner"]'),
+ true,
+ );
- // update helper vars now that they are available
- helper.padChrome$ = this._user1.padChrome$;
- helper.padOuter$ = this._user1.padOuter$;
- helper.padInner$ = this._user1.padInner$;
- },
+ // update helper vars now that they are available
+ helper.padChrome$ = this._user1.padChrome$;
+ helper.padOuter$ = this._user1.padOuter$;
+ helper.padInner$ = this._user1.padInner$;
+ },
- async _createUser1Frame() {
- this._user0.$frame.css({height: '50%'});
- this._user1.$frame = $('