From ec21010167817c7d5268d08aee136f37a6462051 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 12:37:41 -0500 Subject: [PATCH 01/15] Bumped 11 vulnerable transitive deps via pnpm.overrides (#27569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Adds 11 entries to the root `pnpm.overrides` block to force vulnerable transitive deps forward, without touching any direct deps. All replacement versions are pinned with `^x.y.z` to keep upgrades within the existing major. **Modules:** `@tootallnate/once`, `clean-css`, `debug` (×2 ranges), `diff` (×2 ranges), `handlebars`, `minimatch` (×2 ranges), `qs`, `tmp` **Audit delta:** `pnpm audit` 153 → 123 (−1 crit, −14 high, −4 mod, −11 low) ## Notes - `handlebars` and `tmp` are direct deps in `ghost/core` but already match the override-target version, so this is a no-op for direct deps. - Transitive major-version jumps (`clean-css 3 → 4`, `tmp 0.0.x → 0.2.5`, `diff 1 → 3`, `minimatch 0 → 3`) are confined to the Ember admin dev/build toolchain (`broccoli-clean-css`, `mocha 2.5.3`, `sane`, `external-editor`, `fixturify-project`); none reach production runtime, public apps, or `ghost/core/server/`. - Lockfile shrank by ~110 lines from override deduplication. --- package.json | 13 ++- pnpm-lock.yaml | 263 +++++++++++++------------------------------------ 2 files changed, 83 insertions(+), 193 deletions(-) diff --git a/package.json b/package.json index 857ce21078f..4927c02429d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,18 @@ "eslint-plugin-ghost>@typescript-eslint/utils": "8.49.0", "ember-svg-jar>cheerio": "1.0.0-rc.12", "juice>cheerio": "0.22.0", - "lodash.template": "4.5.0" + "lodash.template": "4.5.0", + "@tootallnate/once@<3.0.1": "^3.0.1", + "clean-css@<4.1.11": "^4.1.11", + "debug@>=4.0.0 <4.3.1": "^4.3.1", + "debug@<2.6.9": "^2.6.9", + "diff@<3.5.1": "^3.5.1", + "diff@>=6.0.0 <8.0.3": "^8.0.3", + "handlebars@>=4.0.0 <=4.7.8": "^4.7.9", + "minimatch@<3.1.4": "^3.1.4", + "minimatch@>=9.0.0 <9.0.7": "^9.0.7", + "qs@>=6.7.0 <=6.14.1": "^6.14.2", + "tmp@<=0.2.3": "^0.2.4" }, "onlyBuiltDependencies": [ "@swc/core", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91e64bc22d7..f2a93f4f71b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,17 @@ overrides: ember-svg-jar>cheerio: 1.0.0-rc.12 juice>cheerio: 0.22.0 lodash.template: 4.5.0 + '@tootallnate/once@<3.0.1': ^3.0.1 + clean-css@<4.1.11: ^4.1.11 + debug@>=4.0.0 <4.3.1: ^4.3.1 + debug@<2.6.9: ^2.6.9 + diff@<3.5.1: ^3.5.1 + diff@>=6.0.0 <8.0.3: ^8.0.3 + handlebars@>=4.0.0 <=4.7.8: ^4.7.9 + minimatch@<3.1.4: ^3.1.4 + minimatch@>=9.0.0 <9.0.7: ^9.0.7 + qs@>=6.7.0 <=6.14.1: ^6.14.2 + tmp@<=0.2.3: ^0.2.4 importers: @@ -8359,9 +8370,9 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tootallnate/once@1.1.2': - resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} - engines: {node: '>= 6'} + '@tootallnate/once@3.0.1': + resolution: {integrity: sha512-VyMVKRrpHTT8PnotUeV8L/mDaMwD5DaAKCFLP73zAqAtvF0FCqky+Ki7BYbFCYQmqFyTe9316Ed5zS70QUR9eg==} + engines: {node: '>= 10'} '@tryghost/adapter-base-cache@0.1.23': resolution: {integrity: sha512-uOViNKfsG9X9+woQpUSCcDJhGsjU1O/SjpiXvUwCBBL+UzSp0b2J/kt0kOEhk5Rivr+LqTv/9Ijp876WSf0TFQ==} @@ -11148,11 +11159,6 @@ packages: clean-css-promise@0.1.1: resolution: {integrity: sha512-tzWkANXMD70ETa/wAu2TXAAxYWS0ZjVUFM2dVik8RQBoAbGMFJv4iVluz3RpcoEbo++fX4RV/BXfgGoOjp8o3Q==} - clean-css@3.4.28: - resolution: {integrity: sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==} - engines: {node: '>=0.10.0'} - hasBin: true - clean-css@4.2.4: resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} engines: {node: '>= 4.0'} @@ -11395,10 +11401,6 @@ packages: resolution: {integrity: sha512-CD452fnk0jQyk3NfnK+KkR/hUPoHt5pVaKHogtyyv3N0U4QfAal9W0/rXLOg/vVZgQKa7jdtXypKs1YAip11uQ==} engines: {node: '>= 0.6.x'} - commander@2.8.1: - resolution: {integrity: sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==} - engines: {node: '>= 0.6.x'} - commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -11521,7 +11523,7 @@ packages: haml-coffee: ^1.14.1 hamlet: ^0.3.3 hamljs: ^0.6.2 - handlebars: ^4.7.6 + handlebars: ^4.7.9 hogan.js: ^3.0.2 htmling: ^0.0.8 jade: ^1.11.0 @@ -11686,7 +11688,7 @@ packages: haml-coffee: ^1.14.1 hamlet: ^0.3.3 hamljs: ^0.6.2 - handlebars: ^4.7.6 + handlebars: ^4.7.9 hogan.js: ^3.0.2 htmling: ^0.0.8 jazz: ^0.0.18 @@ -12295,14 +12297,6 @@ packages: resolution: {integrity: sha512-+8rNw7zjXNRntMoJyp5211Y4W3nkhCCMBO7qe8Pht/9NscMklHwyTXMLUzk84YUDSksg87XRmK/LCzJdJ4eU7Q==} engines: {node: '>= 8'} - debug@2.2.0: - resolution: {integrity: sha512-X0rGvJcskG1c3TgSCPqHJ0XJgwlcvOC7elJ5Y0hYuKBZoVqWpAMfLOeIh2UI/DCQ5ruodIjvsugZtjUYUw2pUw==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -12319,15 +12313,6 @@ packages: supports-color: optional: true - debug@4.1.1: - resolution: {integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==} - deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797) - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.1: resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} engines: {node: '>=6.0'} @@ -12553,8 +12538,8 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - diff@1.4.0: - resolution: {integrity: sha512-VzVc42hMZbYU9Sx/ltb7KYuQ6pqAw+cbFWVy4XKdkuEL2CFaRLGEnISPs7YdzaUGpi+CpIqvRmu7hPQ4T7EQ5w==} + diff@3.5.1: + resolution: {integrity: sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==} engines: {node: '>=0.3.1'} diff@4.0.4: @@ -12565,10 +12550,6 @@ packages: resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} engines: {node: '>=0.3.1'} - diff@7.0.0: - resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} - engines: {node: '>=0.3.1'} - diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -14614,9 +14595,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graceful-readlink@1.0.1: - resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} - graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -14638,11 +14616,6 @@ packages: gulp-sort@2.0.0: resolution: {integrity: sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - handlebars@4.7.9: resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} @@ -16739,9 +16712,6 @@ packages: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} - lru-cache@2.7.3: - resolution: {integrity: sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -17142,10 +17112,6 @@ packages: minimalistic-crypto-utils@1.0.1: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} - minimatch@0.3.0: - resolution: {integrity: sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==} - deprecated: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue - minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -17161,10 +17127,6 @@ packages: resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -17341,9 +17303,6 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} - ms@0.7.1: - resolution: {integrity: sha512-lRLiIR9fSNpnP6TC4v8+4OU7oStC01esuNowdQ34L+Gk8e5Puoc88IqJ+XAY/B3Mn2ZKis8l8HX90oU8ivzUHg==} - ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -19215,10 +19174,6 @@ packages: (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - qs@6.14.2: resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} @@ -20145,9 +20100,6 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - sigmund@1.0.1: - resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==} - signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -21043,18 +20995,6 @@ packages: resolution: {integrity: sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==} hasBin: true - tmp@0.0.28: - resolution: {integrity: sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==} - engines: {node: '>=0.4.0'} - - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - - tmp@0.1.0: - resolution: {integrity: sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==} - engines: {node: '>=6'} - tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -28869,7 +28809,7 @@ snapshots: dependencies: '@stdlib/fs-resolve-parent-path': 0.2.3 '@stdlib/utils-convert-path': 0.2.3 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) resolve: 1.22.11 transitivePeerDependencies: - supports-color @@ -29493,7 +29433,7 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tootallnate/once@1.1.2': {} + '@tootallnate/once@3.0.1': {} '@tryghost/adapter-base-cache@0.1.23': {} @@ -30484,7 +30424,7 @@ snapshots: '@types/minimatch@6.0.0': dependencies: - minimatch: 10.2.4 + minimatch: 3.1.5 '@types/minimist@1.2.5': {} @@ -31835,7 +31775,7 @@ snapshots: async-disk-cache@1.3.5: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) heimdalljs: 0.2.6 istextorbinary: 2.1.0 mkdirp: 0.5.6 @@ -31853,7 +31793,7 @@ snapshots: async-promise-queue@1.0.5: dependencies: async: 2.6.4 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) transitivePeerDependencies: - supports-color @@ -31944,7 +31884,7 @@ snapshots: babel-types: 6.26.0 babylon: 6.18.0 convert-source-map: 1.9.0 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) json5: 0.5.1 lodash: 4.17.23 minimatch: 3.1.5 @@ -32541,7 +32481,7 @@ snapshots: babel-runtime: 6.26.0 babel-types: 6.26.0 babylon: 6.18.0 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) globals: 9.18.0 invariant: 2.2.4 lodash: 4.17.23 @@ -32689,13 +32629,13 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.13.0 + qs: 6.15.0 raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 @@ -32706,7 +32646,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -32946,7 +32886,7 @@ snapshots: broccoli-config-replace@1.1.3: dependencies: broccoli-plugin: 1.3.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) fs-extra: 0.24.0 transitivePeerDependencies: - supports-color @@ -32977,7 +32917,7 @@ snapshots: broccoli-kitchen-sink-helpers: 0.3.1 broccoli-plugin: 1.3.1 copy-dereference: 1.0.0 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) mkdirp: 0.5.6 promise-map-series: 0.2.3 rsvp: 3.6.2 @@ -32993,7 +32933,7 @@ snapshots: array-equal: 1.0.2 blank-object: 1.0.2 broccoli-plugin: 1.3.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) exists-sync: 0.0.4 fast-ordered-set: 1.0.3 fs-tree-diff: 0.5.9 @@ -33012,7 +32952,7 @@ snapshots: array-equal: 1.0.2 blank-object: 1.0.2 broccoli-plugin: 1.3.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) fast-ordered-set: 1.0.3 fs-tree-diff: 0.5.9 heimdalljs: 0.2.6 @@ -33030,7 +32970,7 @@ snapshots: array-equal: 1.0.2 blank-object: 1.0.2 broccoli-plugin: 1.3.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) fast-ordered-set: 1.0.3 fs-tree-diff: 0.5.9 heimdalljs: 0.2.6 @@ -33359,7 +33299,7 @@ snapshots: resolve-path: 1.4.0 rimraf: 3.0.2 sane: 4.1.0 - tmp: 0.0.33 + tmp: 0.2.5 tree-sync: 2.1.0 underscore.string: 3.3.6 watch-detector: 1.0.2 @@ -33711,7 +33651,7 @@ snapshots: can-symlink@1.0.0: dependencies: - tmp: 0.0.28 + tmp: 0.2.5 caniuse-api@3.0.0: dependencies: @@ -33959,14 +33899,9 @@ snapshots: clean-css-promise@0.1.1: dependencies: array-to-error: 1.1.1 - clean-css: 3.4.28 + clean-css: 4.2.4 pinkie-promise: 2.0.1 - clean-css@3.4.28: - dependencies: - commander: 2.8.1 - source-map: 0.4.4 - clean-css@4.2.4: dependencies: source-map: 0.6.1 @@ -34179,10 +34114,6 @@ snapshots: commander@2.3.0: {} - commander@2.8.1: - dependencies: - graceful-readlink: 1.0.1 - commander@4.1.1: {} commander@5.1.0: {} @@ -34221,7 +34152,7 @@ snapshots: dependencies: bytes: 3.1.2 compressible: 2.0.18 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -34282,7 +34213,7 @@ snapshots: connect@3.7.0: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) finalhandler: 1.1.2 parseurl: 1.3.3 utils-merge: 1.0.1 @@ -34905,24 +34836,16 @@ snapshots: null-prototype-object: 1.2.6 pretty-ms: 7.0.1 - debug@2.2.0(supports-color@1.2.0): + debug@2.6.9(supports-color@1.2.0): dependencies: - ms: 0.7.1 + ms: 2.0.0 optionalDependencies: supports-color: 1.2.0 - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@3.2.7: dependencies: ms: 2.1.3 - debug@4.1.1: - dependencies: - ms: 2.1.3 - debug@4.3.1: dependencies: ms: 2.1.2 @@ -35111,14 +35034,12 @@ snapshots: diff-sequences@29.6.3: {} - diff@1.4.0: {} + diff@3.5.1: {} diff@4.0.4: {} diff@5.2.2: {} - diff@7.0.0: {} - diff@8.0.4: {} diffie-hellman@5.0.3: @@ -35447,7 +35368,7 @@ snapshots: debug: 4.4.3(supports-color@5.5.0) fs-extra: 10.1.0 fs-tree-diff: 2.0.1 - handlebars: 4.7.8 + handlebars: 4.7.9 is-subdir: 1.2.0 js-string-escape: 1.0.1 lodash: 4.17.23 @@ -35768,7 +35689,7 @@ snapshots: broccoli-funnel: 1.2.0 broccoli-merge-trees: 1.2.4 broccoli-source: 1.1.0 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) lodash: 4.17.23 resolve: 1.22.11 transitivePeerDependencies: @@ -37599,7 +37520,7 @@ snapshots: expand-brackets@2.1.4: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) define-property: 0.2.5 extend-shallow: 2.0.1 posix-character-classes: 0.1.1 @@ -37645,7 +37566,7 @@ snapshots: express-end@0.0.8: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) transitivePeerDependencies: - supports-color @@ -37687,7 +37608,7 @@ snapshots: dependencies: cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) depd: 2.0.0 on-headers: 1.1.0 parseurl: 1.3.3 @@ -37707,7 +37628,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.1 cookie-signature: 1.0.6 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -37721,7 +37642,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.15.0 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -37743,7 +37664,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -37820,7 +37741,7 @@ snapshots: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 - tmp: 0.0.33 + tmp: 0.2.5 extglob@2.0.4: dependencies: @@ -38003,7 +37924,7 @@ snapshots: finalhandler@1.1.2: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) encodeurl: 1.0.2 escape-html: 1.0.3 on-finished: 2.3.0 @@ -38015,7 +37936,7 @@ snapshots: finalhandler@1.3.1: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -38132,12 +38053,12 @@ snapshots: fixturify-project@1.10.0: dependencies: fixturify: 1.3.0 - tmp: 0.0.33 + tmp: 0.2.5 fixturify-project@2.1.1: dependencies: fixturify: 2.1.1 - tmp: 0.0.33 + tmp: 0.2.5 type-fest: 0.11.0 fixturify@1.3.0: @@ -38595,7 +38516,7 @@ snapshots: glob@3.2.11: dependencies: inherits: 2.0.4 - minimatch: 0.3.0 + minimatch: 3.1.5 glob@5.0.15: dependencies: @@ -38766,8 +38687,6 @@ snapshots: graceful-fs@4.2.11: {} - graceful-readlink@1.0.1: {} - graphemer@1.4.0: {} graphql@16.13.2: {} @@ -38807,15 +38726,6 @@ snapshots: dependencies: through2: 2.0.5 - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - handlebars@4.7.9: dependencies: minimist: 1.2.8 @@ -38947,7 +38857,7 @@ snapshots: heimdalljs-logger@0.1.10: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) heimdalljs: 0.2.6 transitivePeerDependencies: - supports-color @@ -39128,7 +39038,7 @@ snapshots: http-proxy-agent@4.0.1: dependencies: - '@tootallnate/once': 1.1.2 + '@tootallnate/once': 3.0.1 agent-base: 6.0.2 debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: @@ -40833,7 +40743,7 @@ snapshots: dependencies: colorette: 1.1.0 commander: 4.1.1 - debug: 4.1.1 + debug: 4.4.3(supports-color@5.5.0) esm: 3.2.25 getopts: 2.2.5 inherits: 2.0.4 @@ -40960,7 +40870,7 @@ snapshots: leek@0.0.24: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) lodash.assign: 3.2.0 rsvp: 3.6.2 transitivePeerDependencies: @@ -41474,8 +41384,6 @@ snapshots: lru-cache@11.3.5: {} - lru-cache@2.7.3: {} - lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -41964,11 +41872,6 @@ snapshots: minimalistic-crypto-utils@1.0.1: {} - minimatch@0.3.0: - dependencies: - lru-cache: 2.7.3 - sigmund: 1.0.1 - minimatch@10.2.4: dependencies: brace-expansion: 5.0.5 @@ -41985,10 +41888,6 @@ snapshots: dependencies: brace-expansion: 2.0.3 - minimatch@9.0.3: - dependencies: - brace-expansion: 2.0.3 - minimatch@9.0.9: dependencies: brace-expansion: 2.0.3 @@ -42144,7 +42043,7 @@ snapshots: browser-stdout: 1.3.1 chokidar: 4.0.3 debug: 4.4.3(supports-color@8.1.1) - diff: 7.0.0 + diff: 8.0.4 escape-string-regexp: 4.0.0 find-up: 5.0.0 glob: 10.5.0 @@ -42166,8 +42065,8 @@ snapshots: mocha@2.5.3: dependencies: commander: 2.3.0 - debug: 2.2.0(supports-color@1.2.0) - diff: 1.4.0 + debug: 2.6.9(supports-color@1.2.0) + diff: 3.5.1 escape-string-regexp: 1.0.2 glob: 3.2.11 growl: 1.9.2 @@ -42196,7 +42095,7 @@ snapshots: morgan@1.10.1: dependencies: basic-auth: 2.0.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) depd: 2.0.0 on-finished: 2.3.0 on-headers: 1.1.0 @@ -42216,8 +42115,6 @@ snapshots: mrmime@2.0.1: {} - ms@0.7.1: {} - ms@2.0.0: {} ms@2.1.2: {} @@ -42805,7 +42702,7 @@ snapshots: jest-diff: 30.3.0 jsonc-parser: 3.2.0 lines-and-columns: 2.0.3 - minimatch: 9.0.3 + minimatch: 9.0.9 node-machine-id: 1.1.12 npm-run-path: 4.0.1 open: 8.4.2 @@ -44423,10 +44320,6 @@ snapshots: q@1.5.1: {} - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - qs@6.14.2: dependencies: side-channel: 1.1.0 @@ -45425,7 +45318,7 @@ snapshots: send@0.19.0: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) depd: 2.0.0 destroy: 1.2.0 encodeurl: 1.0.2 @@ -45617,15 +45510,13 @@ snapshots: siginfo@2.0.0: {} - sigmund@1.0.1: {} - signal-exit@3.0.7: {} signal-exit@4.1.0: {} silent-error@1.1.1: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) transitivePeerDependencies: - supports-color @@ -45757,7 +45648,7 @@ snapshots: snapdragon@0.8.2: dependencies: base: 0.11.2 - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) define-property: 0.2.5 extend-shallow: 2.0.1 map-cache: 0.2.2 @@ -46061,7 +45952,7 @@ snapshots: stream-parser@0.3.1: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) transitivePeerDependencies: - supports-color @@ -46480,7 +46371,7 @@ snapshots: sync-disk-cache@1.3.4: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) heimdalljs: 0.2.6 mkdirp: 0.5.6 rimraf: 2.7.1 @@ -46876,18 +46767,6 @@ snapshots: dependencies: tldts-core: 7.0.27 - tmp@0.0.28: - dependencies: - os-tmpdir: 1.0.2 - - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - - tmp@0.1.0: - dependencies: - rimraf: 2.7.1 - tmp@0.2.5: {} tmpl@1.0.5: {} @@ -46989,7 +46868,7 @@ snapshots: tree-sync@1.4.0: dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@1.2.0) fs-tree-diff: 0.5.9 mkdirp: 0.5.6 quick-temp: 0.1.9 @@ -48063,7 +47942,7 @@ snapshots: dependencies: heimdalljs-logger: 0.1.10 silent-error: 1.1.1 - tmp: 0.1.0 + tmp: 0.2.5 transitivePeerDependencies: - supports-color From 94f9b7341d7da50f8e7513cea8715c9e259b0f3f Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Mon, 27 Apr 2026 19:21:44 +0100 Subject: [PATCH 02/15] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20incorrect=20toolba?= =?UTF-8?q?r=20and=20popup=20positioning=20in=20editor=20(#27575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://linear.app/ghost/issue/DES-1355/ closes https://linear.app/ghost/issue/DES-1354/ - bumped Koenig dependencies, including core editor app that includes fix for Tailwind V4/V3 conflicts that doubled up on negative translate positioning styles --- apps/admin-x-framework/package.json | 2 +- apps/admin-x-settings/package.json | 2 +- apps/admin/package.json | 2 +- ghost/admin/package.json | 6 +- ghost/core/package.json | 24 +-- pnpm-lock.yaml | 313 +++++++++++++--------------- 6 files changed, 165 insertions(+), 184 deletions(-) diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index c26b7af07b3..c9488651280 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -76,7 +76,7 @@ "@playwright/test": "1.59.1", "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "14.3.1", - "@tryghost/koenig-lexical": "1.8.0", + "@tryghost/koenig-lexical": "1.8.1", "@types/react": "18.3.28", "@types/react-dom": "18.3.7", "@vitejs/plugin-react": "4.7.0", diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 4f657d9095d..7ecc453f9a6 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -47,7 +47,7 @@ "@tanstack/react-query": "4.36.1", "@tryghost/color-utils": "0.2.16", "@tryghost/i18n": "workspace:*", - "@tryghost/kg-unsplash-selector": "0.4.0", + "@tryghost/kg-unsplash-selector": "0.4.1", "@tryghost/limit-service": "1.5.2", "@tryghost/nql": "0.12.10", "@tryghost/timezone-data": "0.4.18", diff --git a/apps/admin/package.json b/apps/admin/package.json index ef5cf3e08e9..c0b0c23510e 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -16,7 +16,7 @@ "@tryghost/activitypub": "workspace:*", "@tryghost/admin-x-framework": "workspace:*", "@tryghost/admin-x-settings": "workspace:*", - "@tryghost/koenig-lexical": "1.8.0", + "@tryghost/koenig-lexical": "1.8.1", "@tryghost/posts": "workspace:*", "@tryghost/shade": "workspace:*", "@tryghost/stats": "workspace:*", diff --git a/ghost/admin/package.json b/ghost/admin/package.json index de9d7ba183a..0aa0cb632cc 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -48,9 +48,9 @@ "@tryghost/color-utils": "0.2.16", "@tryghost/ember-promise-modals": "2.0.1", "@tryghost/helpers": "1.1.103", - "@tryghost/kg-clean-basic-html": "4.3.0", - "@tryghost/kg-converters": "1.2.0", - "@tryghost/koenig-lexical": "1.8.0", + "@tryghost/kg-clean-basic-html": "4.3.1", + "@tryghost/kg-converters": "1.2.1", + "@tryghost/koenig-lexical": "1.8.1", "@tryghost/limit-service": "1.5.2", "@tryghost/members-csv": "2.0.5", "@tryghost/nql": "0.12.10", diff --git a/ghost/core/package.json b/ghost/core/package.json index f27b7770e40..899c93e2845 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -107,17 +107,17 @@ "@tryghost/i18n": "workspace:*", "@tryghost/image-transform": "1.4.13", "@tryghost/job-manager": "1.0.9", - "@tryghost/kg-card-factory": "5.2.0", - "@tryghost/kg-clean-basic-html": "4.3.0", - "@tryghost/kg-converters": "1.2.0", - "@tryghost/kg-default-atoms": "5.2.0", - "@tryghost/kg-default-cards": "10.3.0", - "@tryghost/kg-default-nodes": "2.1.0", - "@tryghost/kg-default-transforms": "1.3.0", - "@tryghost/kg-html-to-lexical": "1.3.0", - "@tryghost/kg-lexical-html-renderer": "1.4.0", - "@tryghost/kg-markdown-html-renderer": "7.2.0", - "@tryghost/kg-mobiledoc-html-renderer": "7.2.0", + "@tryghost/kg-card-factory": "5.2.1", + "@tryghost/kg-clean-basic-html": "4.3.1", + "@tryghost/kg-converters": "1.2.1", + "@tryghost/kg-default-atoms": "5.2.1", + "@tryghost/kg-default-cards": "10.3.1", + "@tryghost/kg-default-nodes": "2.1.1", + "@tryghost/kg-default-transforms": "1.3.1", + "@tryghost/kg-html-to-lexical": "1.3.1", + "@tryghost/kg-lexical-html-renderer": "1.4.1", + "@tryghost/kg-markdown-html-renderer": "7.2.1", + "@tryghost/kg-mobiledoc-html-renderer": "7.2.1", "@tryghost/limit-service": "1.5.2", "@tryghost/logging": "4.1.0", "@tryghost/members-csv": "2.0.5", @@ -246,7 +246,7 @@ "lodash.template": "4.5.0" }, "optionalDependencies": { - "@tryghost/html-to-mobiledoc": "3.3.0", + "@tryghost/html-to-mobiledoc": "3.3.1", "sqlite3": "5.1.7" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2a93f4f71b..206415f598e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,8 +195,8 @@ importers: specifier: workspace:* version: link:../admin-x-settings '@tryghost/koenig-lexical': - specifier: 1.8.0 - version: 1.8.0 + specifier: 1.8.1 + version: 1.8.1 '@tryghost/posts': specifier: workspace:* version: link:../posts @@ -510,8 +510,8 @@ importers: specifier: 14.3.1 version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tryghost/koenig-lexical': - specifier: 1.8.0 - version: 1.8.0 + specifier: 1.8.1 + version: 1.8.1 '@types/react': specifier: 18.3.28 version: 18.3.28 @@ -588,8 +588,8 @@ importers: specifier: workspace:* version: link:../../ghost/i18n '@tryghost/kg-unsplash-selector': - specifier: 0.4.0 - version: 0.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 0.4.1 + version: 0.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tryghost/limit-service': specifier: 1.5.2 version: 1.5.2 @@ -1624,14 +1624,14 @@ importers: specifier: 1.1.103 version: 1.1.103 '@tryghost/kg-clean-basic-html': - specifier: 4.3.0 - version: 4.3.0 + specifier: 4.3.1 + version: 4.3.1 '@tryghost/kg-converters': - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.2.1 + version: 1.2.1 '@tryghost/koenig-lexical': - specifier: 1.8.0 - version: 1.8.0 + specifier: 1.8.1 + version: 1.8.1 '@tryghost/limit-service': specifier: 1.5.2 version: 1.5.2 @@ -2020,38 +2020,38 @@ importers: specifier: 1.0.9 version: 1.0.9 '@tryghost/kg-card-factory': - specifier: 5.2.0 - version: 5.2.0 + specifier: 5.2.1 + version: 5.2.1 '@tryghost/kg-clean-basic-html': - specifier: 4.3.0 - version: 4.3.0 + specifier: 4.3.1 + version: 4.3.1 '@tryghost/kg-converters': - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.2.1 + version: 1.2.1 '@tryghost/kg-default-atoms': - specifier: 5.2.0 - version: 5.2.0 + specifier: 5.2.1 + version: 5.2.1 '@tryghost/kg-default-cards': - specifier: 10.3.0 - version: 10.3.0(encoding@0.1.13) + specifier: 10.3.1 + version: 10.3.1(encoding@0.1.13) '@tryghost/kg-default-nodes': - specifier: 2.1.0 - version: 2.1.0(@noble/hashes@1.8.0) + specifier: 2.1.1 + version: 2.1.1(@noble/hashes@1.8.0) '@tryghost/kg-default-transforms': - specifier: 1.3.0 - version: 1.3.0(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + specifier: 1.3.1 + version: 1.3.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) '@tryghost/kg-html-to-lexical': - specifier: 1.3.0 - version: 1.3.0(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + specifier: 1.3.1 + version: 1.3.1(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) '@tryghost/kg-lexical-html-renderer': - specifier: 1.4.0 - version: 1.4.0(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + specifier: 1.4.1 + version: 1.4.1(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) '@tryghost/kg-markdown-html-renderer': - specifier: 7.2.0 - version: 7.2.0 + specifier: 7.2.1 + version: 7.2.1 '@tryghost/kg-mobiledoc-html-renderer': - specifier: 7.2.0 - version: 7.2.0 + specifier: 7.2.1 + version: 7.2.1 '@tryghost/limit-service': specifier: 1.5.2 version: 1.5.2 @@ -2568,8 +2568,8 @@ importers: version: 5.9.3 optionalDependencies: '@tryghost/html-to-mobiledoc': - specifier: 3.3.0 - version: 3.3.0(@noble/hashes@1.8.0) + specifier: 3.3.1 + version: 3.3.1(@noble/hashes@1.8.0) sqlite3: specifier: 5.1.7 version: 5.1.7 @@ -3660,13 +3660,6 @@ packages: resolution: {integrity: sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==} engines: {node: '>=4.0.0'} - '@csstools/css-calc@3.1.1': - resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-calc@3.2.0': resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} engines: {node: '>=20.19.0'} @@ -3674,13 +3667,6 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.0.2': - resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.1.0': resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} engines: {node: '>=20.19.0'} @@ -8476,8 +8462,8 @@ packages: '@tryghost/helpers@1.1.103': resolution: {integrity: sha512-ako7nvUKySOaTMSZXUXnrg6pVTkxW1ZVKbJj0t0HGG+ndt0pjLBdnzdZW3BMGEj4Qvf0MWxGdUcLrW/OFyHzsg==} - '@tryghost/html-to-mobiledoc@3.3.0': - resolution: {integrity: sha512-Hw/nu9QC5gu1bNvQ3+ls6vEUUDDQDC5S13Zqw96OU2ripPcTOG60ngOEGBhP6eCkU1vwXs8ShAQn/FBIRNLvqQ==} + '@tryghost/html-to-mobiledoc@3.3.1': + resolution: {integrity: sha512-VH9JVk/2Mt+FTjNUQYT4zbO3XkDGNiuBFuF+f2HYFB050c12FftTPtCF578Lj+zUm+slZLljaJMW+czl7aH/7g==} '@tryghost/html-to-plaintext@1.0.8': resolution: {integrity: sha512-CIXoOlmpp9SVrb0BPQ/+cBaSHFpGv53Gew3VFLw0w1v1q04+0e0FRKjl8Cv6oUl+c1I9LwxbwdcV6rxRCgVbIw==} @@ -8497,61 +8483,61 @@ packages: '@tryghost/job-manager@1.0.9': resolution: {integrity: sha512-Wlwr1R8oeU6HLB7RB1g6VxAH6VEZIaXTVV2GdvCbipK+TA0MIo6YOQUgrIky25RpyRqKXR0UuDIfXDB4+S+GgQ==} - '@tryghost/kg-card-factory@5.2.0': - resolution: {integrity: sha512-hjhUusuSDZUl4ZCNVx37od6ejjy6nrhjZm6lhhEdF1vXX2sjhc0j7KjT5sjPHxCxV0gN+Td3TAAg21RRiKh4tg==} + '@tryghost/kg-card-factory@5.2.1': + resolution: {integrity: sha512-6fS2dmL5h+UIwTH4rJRJxDiOF5UzJtHCh+pGoEZhG0g+JUoguBW1/e1uf8N6xRIO2VsNnQuYoHdKq+RbLqrFVA==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-clean-basic-html@4.3.0': - resolution: {integrity: sha512-wPSV1zs3JBFt6b7iyL7LHx0A4twDacgo7t7PRiR6dA/t6U5ekI5JSs1Ko9boggjzhwK+N9y/xDCOlcbwo6K5dw==} + '@tryghost/kg-clean-basic-html@4.3.1': + resolution: {integrity: sha512-yYAheQjp26TYHgl7UuZj8sQ+e+XdEp3YMF/qgMoInbnJAsAFhIFoLe2EQ7tYbk4YKsmLCDCgnW1iazuB21XrSg==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-converters@1.2.0': - resolution: {integrity: sha512-OfUWM3PSM72vKmhMndGBL2tG/Lxka+l5+8j6zF6LpvQw8pAUNJvC4YQ1G53qg89hVSlX0myJQtjBMJNZ5wsn7g==} + '@tryghost/kg-converters@1.2.1': + resolution: {integrity: sha512-MaBJMMJZ8k3PH/BD3DhO6Dt7SEUPDi+NlUGv5zbZQHb96mD1H9jIsYl38vYhSiYRDU1T0YIrVS3UuQS8O5UpuQ==} - '@tryghost/kg-default-atoms@5.2.0': - resolution: {integrity: sha512-TxDfierd78p6y343trWZICInDV/gB942eq7eG48d5GWiy9Fhi2Iy19UYZAeX8n+X4u6SOKbWGVEhmwjvsF0KMw==} + '@tryghost/kg-default-atoms@5.2.1': + resolution: {integrity: sha512-NEF489Y4Q9y4jGL+4x017T9O6hwp2CAIb9bRVejtegilA6VhMSSO9uzhEIkmoso1RH7vahub0mocMtOrv7bHjQ==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-default-cards@10.3.0': - resolution: {integrity: sha512-eaDePk+Eoek+V1qS/i3uDawpm2xQezwqba5XiAH2BSmRS9gZTVqb1lWlRdL+QXmcqaIlbPWPxrYHSM/JjbA38Q==} + '@tryghost/kg-default-cards@10.3.1': + resolution: {integrity: sha512-L6nbmpP9/DmocJRIqHmSd00nV+knem+0I6SlEaPlXjvpGlesVMX+fsDP0I67CkKBBelvmlkvK4MzT2zJp5e9nQ==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-default-nodes@2.1.0': - resolution: {integrity: sha512-9XpYBur2i5S4Mtnc1LPlnHM0v9z3p8XhUWzA3CR+f08ecZQmlnsby3vIIhhf2dvmvlWHGN5oWh/Z+6VwW+uQPw==} + '@tryghost/kg-default-nodes@2.1.1': + resolution: {integrity: sha512-OLabnd0jKkW0hRQENXoWm0XhLRkzMdUdw4zB01RCUQiQ3rKdWeHjibDB8xlRoUqHdVj9MBC+NVjEGw9oic85Mw==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-default-transforms@1.3.0': - resolution: {integrity: sha512-i1EQtbFg5VVxOaSAEZC+1dEqVDUBDOqzve4g2GzEA9s+axFGRWqwnu+vj5f8XLGWX3Jcb8qTS8pzPouMgWqN9w==} + '@tryghost/kg-default-transforms@1.3.1': + resolution: {integrity: sha512-GL23u0CACoU6XvetzKvxnhfVZE93pznbePQ9pbIeiYuKnitMovuUo40G553kMHvQ7C1NK7YkozhocBsLGkl0RQ==} - '@tryghost/kg-html-to-lexical@1.3.0': - resolution: {integrity: sha512-7Ly2wFxeYcZw4eCKZPtpZ6/hHmR6qgZy1w+Sb5UyNxJqBVLFnOMdSN6QC7r6IONJunEVbRNCrqNRxLZlMTFuvw==} + '@tryghost/kg-html-to-lexical@1.3.1': + resolution: {integrity: sha512-IjLONFywAmwqcPMfX20YAkaPq4fgOv0ME5ahboirkdztAjCGXhY6oq9bgkrrZPNdbAtl6LyMvZ1w3wI1Hq2jeg==} - '@tryghost/kg-lexical-html-renderer@1.4.0': - resolution: {integrity: sha512-9qGIo8eNbrBBxY4TIvVvkQbdqjJFU9KNwqp37FSm69nLm4MNKX4hnKlPF2nINdiUgFtzFxCu+4wmH4htaO7U3g==} + '@tryghost/kg-lexical-html-renderer@1.4.1': + resolution: {integrity: sha512-mPbhGQSYNrYENm3caQeD70bAjlE13ct+hp+GvShDTuGE+DsrRiif0MIL12tV9Hp2iFwhutlwFpMXRQXwI+2lIQ==} - '@tryghost/kg-markdown-html-renderer@7.2.0': - resolution: {integrity: sha512-ZPf0o0VdfmkBqUuwLAhmeZCtM7EAJEUDvGyAMQy/WPvI0TSs+Z3narE3AL1JFwx/MffBwUt/qvwO4HVOcHgQrQ==} + '@tryghost/kg-markdown-html-renderer@7.2.1': + resolution: {integrity: sha512-1PH7L/k+aEzFTYnvikH5U0qEetcglOZcGjqbfVXL6wD66BUzxeKOeLq51lLc00fjKLZtZa2Qr4zj1DqObPO3Mw==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-mobiledoc-html-renderer@7.2.0': - resolution: {integrity: sha512-N9XwfmlZuWdZf6FEcmM495N+S7erJXqbqiH1cXDKnD4I4orL6HcYpYcmaeTJ4A1CGZ7wEcUTjt2BIrSP4pz0tg==} + '@tryghost/kg-mobiledoc-html-renderer@7.2.1': + resolution: {integrity: sha512-7qTnIWCzBZEEjZQpGKlCaJ9kbYrhnmG6/ttlOtFHUrhbfoIYts1uiVRXiP0VB3xELIPuu34aFdslpX2ANIzhCQ==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-parser-plugins@4.3.0': - resolution: {integrity: sha512-uMI+JKG3K4PgVOADjWkxr3Tz4UwSEUs0ZycaDC2bjunrBBrLe7A2fHjMVuPbs4gIz7QZKMV0jRtucCZkBT7PvQ==} + '@tryghost/kg-parser-plugins@4.3.1': + resolution: {integrity: sha512-E8M+jlPmuU9OHa1SWtU5Pc+zP5dbZetXrNVuxlzlBdysAUh9MPfz1C9bxaZ7um7TuqNechl6t2WSUJezueYN2g==} engines: {node: ^22.13.1} - '@tryghost/kg-unsplash-selector@0.4.0': - resolution: {integrity: sha512-c/tUcQddM6YVazJeRmXJOS1dEWL1v6Fr5is5VvdfwtDAaQHp32iFjeb4d0H3miyfJ3DYXXEDtt1l5iQ3dOdDVw==} + '@tryghost/kg-unsplash-selector@0.4.1': + resolution: {integrity: sha512-9W2cuTBr3xo5SsCPC3ujxWrLj1q23xmLRLIaWgxYyyPTCXj2XP3Bu1aOOd9u6y2hkS3TdUt6ma3LPTi8K/IHEg==} peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 - '@tryghost/kg-utils@1.1.0': - resolution: {integrity: sha512-PrOmK9zoEVraQZ9IOza5sUiExLifbNjmcJJmXRr5clUg/Zx1SUWWGcH49bb12+uqTYGLqc7LknvsqB14euuBYw==} + '@tryghost/kg-utils@1.1.1': + resolution: {integrity: sha512-qUEQwA4RB0DXwBKaxWknRGiYFdzpW/LYugsgFyhxSZ4k/EX8eWmZSUNaLK0LodCr+I5+VNuFoCllngvM1IhR6A==} - '@tryghost/koenig-lexical@1.8.0': - resolution: {integrity: sha512-AD0PBZAx1nnw48T2xbALwTtaJuJWcAw4awJOWI975Xh/ZV2rupNmmdzgpfdHke6pJhvhbJbXeBMOzb/AsRYKXA==} + '@tryghost/koenig-lexical@1.8.1': + resolution: {integrity: sha512-cVIM2Iknhz/cZFiczgpX6oBWmeJwo0MIkG6NR6VLxncS6QnF+EJloHGrjQW7j2Uge3ly2BpRWHsJP4YV0PtpzA==} '@tryghost/limit-service@1.5.2': resolution: {integrity: sha512-BTSnMpVMRauSkxi0yL/V2qVy9nRlAL9maALFAe5MDNTSCvg4zjV2b2S4o6+tgCyGDRkMPeePfrgSuH2YTTbD5Q==} @@ -8644,6 +8630,9 @@ packages: '@tryghost/string@0.3.2': resolution: {integrity: sha512-tPgTU4o/4m1TBMoSChHm4n7XvXq+x6GJ//B0MdVPK57Rb0GEThsV347C2Fr5+1q71EdICrLbFPYh0vAqEM/eoA==} + '@tryghost/string@0.3.3': + resolution: {integrity: sha512-VCJKoj+bHd5TKpNu02ce1lMLTDfg+Uv077i8TdijequrmcEayZTYSLVnNJ1320tu9+2rB+yzdadS/3BCTtACEA==} + '@tryghost/timezone-data@0.4.18': resolution: {integrity: sha512-yawVt4pBmbsw2Uwsxd/8rjwyucQX6cefHH48X5Qe5eMQH3mnla5Q+gJ+By9a3Sza1qECvhIG3jUhDM3cLhILzQ==} @@ -8656,8 +8645,8 @@ packages: '@tryghost/url-utils@5.1.2': resolution: {integrity: sha512-GGZRfdVdk7jE1puqfBCeaAMeQ/Gd0NUkbqhj4gRSCWKW97JDpoiu5Fwyutt3vq3U3X4ZsfLfKs0NtAfUez0oog==} - '@tryghost/url-utils@5.2.2': - resolution: {integrity: sha512-KlDLms43V9j3LAft3bhHYuywhaAob9fO1htbSOH++hGEP9nQLL9FHLYqeuWzXvKTb+ZjGZ+HNRpeErc7G6dgCw==} + '@tryghost/url-utils@5.2.3': + resolution: {integrity: sha512-1+1sUfJuXlD0OWYiM/N7mNp2BSNCKb0PVTBjER6bEq/owa8Et9W2MDMQ1H3/dIOvlSq+7swGCoKX/IrXNeaWmA==} '@tryghost/validator@0.2.22': resolution: {integrity: sha512-dmobNVEKXMi3K4OdAXLBFwa78hVgy8cYvCPJgfV4h3NtYIyRHqwARr3upT3/ASpVBpKGVBu7XexfSv+tibzsuQ==} @@ -22375,16 +22364,16 @@ snapshots: '@asamuzakjp/css-color@4.1.2': dependencies: - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.7 + lru-cache: 11.3.5 '@asamuzakjp/css-color@5.0.1': dependencies: - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 lru-cache: 11.2.7 @@ -24085,23 +24074,11 @@ snapshots: '@csstools/convert-colors@1.4.0': {} - '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/color-helpers': 6.0.2 - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/color-helpers': 6.0.2 @@ -24561,7 +24538,7 @@ snapshots: js-string-escape: 1.0.1 jsdom: 16.7.0 json-stable-stringify: 1.3.0 - lodash: 4.17.23 + lodash: 4.18.1 pkg-up: 2.0.0 resolve: 1.22.11 resolve-package-path: 1.2.7 @@ -24584,7 +24561,7 @@ snapshots: '@embroider/core': 0.29.0 assert-never: 1.4.0 ember-cli-babel: 7.26.11 - lodash: 4.17.23 + lodash: 4.18.1 resolve: 1.22.11 semver: 7.7.4 transitivePeerDependencies: @@ -24598,7 +24575,7 @@ snapshots: '@embroider/shared-internals': 0.41.0 assert-never: 1.4.0 ember-cli-babel: 7.26.11 - lodash: 4.17.23 + lodash: 4.18.1 resolve: 1.22.11 semver: 7.7.4 transitivePeerDependencies: @@ -24624,7 +24601,7 @@ snapshots: babel-import-util: 3.0.1 ember-cli-babel: 7.26.11 find-up: 5.0.0 - lodash: 4.17.23 + lodash: 4.18.1 resolve: 1.22.11 semver: 7.7.4 transitivePeerDependencies: @@ -24634,7 +24611,7 @@ snapshots: dependencies: ember-rfc176-data: 0.3.18 fs-extra: 7.0.1 - lodash: 4.17.23 + lodash: 4.18.1 pkg-up: 3.1.0 resolve-package-path: 1.2.7 semver: 7.7.4 @@ -24646,7 +24623,7 @@ snapshots: ember-rfc176-data: 0.3.18 fs-extra: 9.1.0 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 resolve-package-path: 4.0.3 semver: 7.7.4 typescript-memoize: 1.1.1 @@ -24693,7 +24670,7 @@ snapshots: fs-extra: 9.1.0 is-subdir: 1.2.0 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 minimatch: 3.1.5 pkg-entry-points: 1.1.1 resolve-package-path: 4.0.3 @@ -26088,7 +26065,7 @@ snapshots: agent-base: 7.1.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - lru-cache: 11.2.7 + lru-cache: 11.3.5 socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color @@ -29641,9 +29618,9 @@ snapshots: dependencies: lodash-es: 4.17.23 - '@tryghost/html-to-mobiledoc@3.3.0(@noble/hashes@1.8.0)': + '@tryghost/html-to-mobiledoc@3.3.1(@noble/hashes@1.8.0)': dependencies: - '@tryghost/kg-parser-plugins': 4.3.0 + '@tryghost/kg-parser-plugins': 4.3.1 '@tryghost/mobiledoc-kit': 0.12.4-ghost.1 jsdom: 29.0.2(@noble/hashes@1.8.0) transitivePeerDependencies: @@ -29697,36 +29674,36 @@ snapshots: - '@75lb/nature' - supports-color - '@tryghost/kg-card-factory@5.2.0': {} + '@tryghost/kg-card-factory@5.2.1': {} - '@tryghost/kg-clean-basic-html@4.3.0': {} + '@tryghost/kg-clean-basic-html@4.3.1': {} - '@tryghost/kg-converters@1.2.0': + '@tryghost/kg-converters@1.2.1': dependencies: lodash: 4.18.1 - '@tryghost/kg-default-atoms@5.2.0': {} + '@tryghost/kg-default-atoms@5.2.1': {} - '@tryghost/kg-default-cards@10.3.0(encoding@0.1.13)': + '@tryghost/kg-default-cards@10.3.1(encoding@0.1.13)': dependencies: - '@tryghost/kg-markdown-html-renderer': 7.2.0 - '@tryghost/kg-utils': 1.1.0 - '@tryghost/string': 0.3.2 - '@tryghost/url-utils': 5.2.2 + '@tryghost/kg-markdown-html-renderer': 7.2.1 + '@tryghost/kg-utils': 1.1.1 + '@tryghost/string': 0.3.3 + '@tryghost/url-utils': 5.2.3 handlebars: 4.7.9 juice: 9.1.0(encoding@0.1.13) luxon: 3.7.2 transitivePeerDependencies: - encoding - '@tryghost/kg-default-nodes@2.1.0(@noble/hashes@1.8.0)': + '@tryghost/kg-default-nodes@2.1.1(@noble/hashes@1.8.0)': dependencies: '@lexical/clipboard': 0.13.1(lexical@0.13.1) '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) '@lexical/selection': 0.13.1(lexical@0.13.1) '@lexical/utils': 0.13.1(lexical@0.13.1) - '@tryghost/kg-clean-basic-html': 4.3.0 - '@tryghost/kg-markdown-html-renderer': 7.2.0 + '@tryghost/kg-clean-basic-html': 4.3.1 + '@tryghost/kg-markdown-html-renderer': 7.2.1 clsx: 2.1.1 html-minifier: 4.0.0 jsdom: 29.0.2(@noble/hashes@1.8.0) @@ -29737,12 +29714,12 @@ snapshots: - '@noble/hashes' - canvas - '@tryghost/kg-default-transforms@1.3.0(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': + '@tryghost/kg-default-transforms@1.3.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': dependencies: '@lexical/list': 0.13.1(lexical@0.13.1) '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) '@lexical/utils': 0.13.1(lexical@0.13.1) - '@tryghost/kg-default-nodes': 2.1.0(@noble/hashes@1.8.0) + '@tryghost/kg-default-nodes': 2.1.1(@noble/hashes@1.8.0) lexical: 0.13.1 transitivePeerDependencies: - '@lexical/clipboard' @@ -29750,7 +29727,7 @@ snapshots: - '@noble/hashes' - canvas - '@tryghost/kg-html-to-lexical@1.3.0(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': + '@tryghost/kg-html-to-lexical@1.3.1(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': dependencies: '@lexical/clipboard': 0.13.1(lexical@0.13.1) '@lexical/headless': 0.13.1(lexical@0.13.1) @@ -29758,8 +29735,8 @@ snapshots: '@lexical/link': 0.13.1(lexical@0.13.1) '@lexical/list': 0.13.1(lexical@0.13.1) '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) - '@tryghost/kg-default-nodes': 2.1.0(@noble/hashes@1.8.0) - '@tryghost/kg-default-transforms': 1.3.0(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + '@tryghost/kg-default-nodes': 2.1.1(@noble/hashes@1.8.0) + '@tryghost/kg-default-transforms': 1.3.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) jsdom: 29.0.2(@noble/hashes@1.8.0) lexical: 0.13.1 transitivePeerDependencies: @@ -29768,7 +29745,7 @@ snapshots: - '@noble/hashes' - canvas - '@tryghost/kg-lexical-html-renderer@1.4.0(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': + '@tryghost/kg-lexical-html-renderer@1.4.1(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': dependencies: '@lexical/clipboard': 0.13.1(lexical@0.13.1) '@lexical/code': 0.13.1(lexical@0.13.1) @@ -29776,8 +29753,8 @@ snapshots: '@lexical/link': 0.13.1(lexical@0.13.1) '@lexical/list': 0.13.1(lexical@0.13.1) '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) - '@tryghost/kg-default-nodes': 2.1.0(@noble/hashes@1.8.0) - '@tryghost/kg-default-transforms': 1.3.0(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + '@tryghost/kg-default-nodes': 2.1.1(@noble/hashes@1.8.0) + '@tryghost/kg-default-transforms': 1.3.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) jsdom: 29.0.2(@noble/hashes@1.8.0) lexical: 0.13.1 transitivePeerDependencies: @@ -29786,9 +29763,9 @@ snapshots: - '@noble/hashes' - canvas - '@tryghost/kg-markdown-html-renderer@7.2.0': + '@tryghost/kg-markdown-html-renderer@7.2.1': dependencies: - '@tryghost/kg-utils': 1.1.0 + '@tryghost/kg-utils': 1.1.1 markdown-it: 14.1.1 markdown-it-footnote: 4.0.0 markdown-it-image-lazy-loading: 2.0.1 @@ -29798,27 +29775,27 @@ snapshots: markdown-it-sup: 2.0.0 semver: 7.7.4 - '@tryghost/kg-mobiledoc-html-renderer@7.2.0': + '@tryghost/kg-mobiledoc-html-renderer@7.2.1': dependencies: - '@tryghost/kg-utils': 1.1.0 + '@tryghost/kg-utils': 1.1.1 mobiledoc-dom-renderer: 0.7.2 simple-dom: 1.4.0 - '@tryghost/kg-parser-plugins@4.3.0': + '@tryghost/kg-parser-plugins@4.3.1': dependencies: - '@tryghost/kg-clean-basic-html': 4.3.0 + '@tryghost/kg-clean-basic-html': 4.3.1 optional: true - '@tryghost/kg-unsplash-selector@0.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tryghost/kg-unsplash-selector@0.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tryghost/kg-utils@1.1.0': + '@tryghost/kg-utils@1.1.1': dependencies: semver: 7.7.4 - '@tryghost/koenig-lexical@1.8.0': {} + '@tryghost/koenig-lexical@1.8.1': {} '@tryghost/limit-service@1.5.2': dependencies: @@ -29870,7 +29847,7 @@ snapshots: '@tryghost/mongo-knex@0.9.4': dependencies: debug: 4.4.3(supports-color@5.5.0) - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -30065,6 +30042,10 @@ snapshots: dependencies: unidecode: 1.1.0 + '@tryghost/string@0.3.3': + dependencies: + unidecode: 1.1.0 + '@tryghost/timezone-data@0.4.18': {} '@tryghost/tpl@0.1.40': @@ -30083,10 +30064,10 @@ snapshots: remark-footnotes: 1.0.0 unist-util-visit: 2.0.3 - '@tryghost/url-utils@5.2.2': + '@tryghost/url-utils@5.2.3': dependencies: cheerio: 1.2.0 - lodash: 4.17.23 + lodash: 4.18.1 moment: 2.24.0 moment-timezone: 0.5.45 remark: 11.0.2 @@ -31801,7 +31782,7 @@ snapshots: async@2.6.4: dependencies: - lodash: 4.17.23 + lodash: 4.18.1 async@3.2.3: {} @@ -31886,7 +31867,7 @@ snapshots: convert-source-map: 1.9.0 debug: 2.6.9(supports-color@1.2.0) json5: 0.5.1 - lodash: 4.17.23 + lodash: 4.18.1 minimatch: 3.1.5 path-is-absolute: 1.0.1 private: 0.1.8 @@ -31902,7 +31883,7 @@ snapshots: babel-types: 6.26.0 detect-indent: 4.0.0 jsesc: 1.3.0 - lodash: 4.17.23 + lodash: 4.18.1 source-map: 0.5.7 trim-right: 1.0.1 @@ -31934,7 +31915,7 @@ snapshots: babel-helper-function-name: 6.24.1 babel-runtime: 6.26.0 babel-types: 6.26.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -31975,7 +31956,7 @@ snapshots: dependencies: babel-runtime: 6.26.0 babel-types: 6.26.0 - lodash: 4.17.23 + lodash: 4.18.1 babel-helper-remap-async-to-generator@6.24.1: dependencies: @@ -32082,7 +32063,7 @@ snapshots: babel-plugin-filter-imports@4.0.0: dependencies: '@babel/types': 7.29.0 - lodash: 4.17.23 + lodash: 4.18.1 babel-plugin-htmlbars-inline-precompile@1.0.0: {} @@ -32228,7 +32209,7 @@ snapshots: babel-template: 6.26.0 babel-traverse: 6.26.0 babel-types: 6.26.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -32453,7 +32434,7 @@ snapshots: babel-runtime: 6.26.0 core-js: 2.6.12 home-or-tmp: 2.0.0 - lodash: 4.17.23 + lodash: 4.18.1 mkdirp: 0.5.6 source-map-support: 0.4.18 transitivePeerDependencies: @@ -32470,7 +32451,7 @@ snapshots: babel-traverse: 6.26.0 babel-types: 6.26.0 babylon: 6.18.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -32484,7 +32465,7 @@ snapshots: debug: 2.6.9(supports-color@1.2.0) globals: 9.18.0 invariant: 2.2.4 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -32492,7 +32473,7 @@ snapshots: dependencies: babel-runtime: 6.26.0 esutils: 2.0.3 - lodash: 4.17.23 + lodash: 4.18.1 to-fast-properties: 1.0.3 babel6-plugin-strip-class-callcheck@6.0.0: {} @@ -33530,7 +33511,7 @@ snapshots: '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 glob: 13.0.6 - lru-cache: 11.2.7 + lru-cache: 11.3.5 minipass: 7.1.3 minipass-collect: 2.0.1 minipass-flush: 1.0.7 @@ -34713,9 +34694,9 @@ snapshots: cssstyle@5.3.7: dependencies: '@asamuzakjp/css-color': 4.1.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) css-tree: 3.2.1 - lru-cache: 11.2.7 + lru-cache: 11.3.5 cssstyle@6.2.0: dependencies: @@ -35330,7 +35311,7 @@ snapshots: fs-tree-diff: 2.0.1 handlebars: 4.7.9 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 mkdirp: 0.5.6 resolve-package-path: 3.1.0 rimraf: 2.7.1 @@ -37227,7 +37208,7 @@ snapshots: esquery: 1.7.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 - lodash: 4.17.23 + lodash: 4.18.1 pluralize: 8.0.0 read-pkg-up: 7.0.1 regexp-tree: 0.1.27 @@ -39275,7 +39256,7 @@ snapshots: cli-width: 2.2.1 external-editor: 3.1.0 figures: 2.0.0 - lodash: 4.17.23 + lodash: 4.18.1 mute-stream: 0.0.7 run-async: 2.4.1 rxjs: 6.6.7 @@ -39291,7 +39272,7 @@ snapshots: cli-width: 3.0.0 external-editor: 3.1.0 figures: 3.2.0 - lodash: 4.17.23 + lodash: 4.18.1 mute-stream: 0.0.8 run-async: 2.4.1 rxjs: 6.6.7 @@ -40456,7 +40437,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - parse5: 8.0.0 + parse5: 8.0.1 saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 6.0.1 @@ -42223,7 +42204,7 @@ snapshots: najax@1.0.7: dependencies: jquery-deferred: 0.3.1 - lodash: 4.17.23 + lodash: 4.18.1 qs: 6.15.0 named-placeholders@1.1.6: @@ -43183,7 +43164,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.2.7 + lru-cache: 11.3.5 minipass: 7.1.3 path-to-regexp@0.1.12: {} @@ -48137,7 +48118,7 @@ snapshots: whatwg-url@8.7.0: dependencies: - lodash: 4.17.23 + lodash: 4.18.1 tr46: 2.1.0 webidl-conversions: 6.1.0 @@ -48206,7 +48187,7 @@ snapshots: wide-align@1.1.5: dependencies: - string-width: 4.2.3 + string-width: 1.0.2 word-wrap@1.2.5: {} From 02cc89f88bb53d753bf84dae9ee01e50c37298fa Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 14:01:28 -0500 Subject: [PATCH 03/15] Added tar override to clear 6 high-severity advisories (#27577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Adds a single override to root `pnpm.overrides`: ```json "tar@<7.5.11": "^7.5.11" ``` Forces `sqlite3`'s internal `node-gyp 8` chain — which previously pulled the deprecated `tar@6.2.1` — onto `tar@7.5.13`. After the override, all `tar` versions in the tree collapse to a single `7.5.13`. ## On the 6 → 7 major jump This crosses a major boundary. Two mitigations apply: - The actual consumer in this chain is `node-gyp`'s prebuild-extraction step. It calls `tar.x()` (and the streaming variants), which haven't changed across 6 → 7. - The version-engine bump in tar 7 (Node 10+ → Node 18+) is satisfied — `ghost/core` requires `node ^22.13.1`. The 6.x line of `tar` has no fixed version (publisher marked it deprecated), so an in-major fix isn't possible. The override is removable when `sqlite3` is bumped to a release that ships with `node-gyp >= 11`. ## Audit delta `pnpm audit`: 123 → 117 (`−6 high`). --- package.json | 1 + pnpm-lock.yaml | 29 ++++------------------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 4927c02429d..80043949975 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "minimatch@<3.1.4": "^3.1.4", "minimatch@>=9.0.0 <9.0.7": "^9.0.7", "qs@>=6.7.0 <=6.14.1": "^6.14.2", + "tar@<7.5.11": "^7.5.11", "tmp@<=0.2.3": "^0.2.4" }, "onlyBuiltDependencies": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 206415f598e..99d40b42928 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,7 @@ overrides: minimatch@<3.1.4: ^3.1.4 minimatch@>=9.0.0 <9.0.7: ^9.0.7 qs@>=6.7.0 <=6.14.1: ^6.14.2 + tar@<7.5.11: ^7.5.11 tmp@<=0.2.3: ^0.2.4 importers: @@ -17176,10 +17177,6 @@ packages: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -20798,11 +20795,6 @@ packages: tar-stream@3.1.8: resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - tar@7.5.13: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} @@ -33500,7 +33492,7 @@ snapshots: promise-inflight: 1.0.1(bluebird@3.7.2) rimraf: 3.0.2 ssri: 8.0.1 - tar: 6.2.1 + tar: 7.5.13 unique-filename: 1.1.1 transitivePeerDependencies: - bluebird @@ -41939,9 +41931,6 @@ snapshots: minipass@4.2.8: {} - minipass@5.0.0: - optional: true - minipass@7.1.3: {} minizlib@2.1.2: @@ -42375,7 +42364,7 @@ snapshots: npmlog: 6.0.2 rimraf: 3.0.2 semver: 7.7.4 - tar: 6.2.1 + tar: 7.5.13 which: 2.0.2 transitivePeerDependencies: - bluebird @@ -45799,7 +45788,7 @@ snapshots: bindings: 1.5.0 node-addon-api: 7.1.1 prebuild-install: 7.1.3 - tar: 6.2.1 + tar: 7.5.13 optionalDependencies: node-gyp: 8.4.1 transitivePeerDependencies: @@ -46467,16 +46456,6 @@ snapshots: - bare-buffer - react-native-b4a - tar@6.2.1: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - optional: true - tar@7.5.13: dependencies: '@isaacs/fs-minipass': 4.0.1 From b592a263a655b7fb341c7973ea4a4fc5ebb10652 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 14:02:33 -0500 Subject: [PATCH 04/15] Bumped multer 2.0.2 -> 2.1.1 in ghost/core (#27574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Bumps `multer` in `ghost/core` from `2.0.2` to `2.1.1` to clear three high-severity advisories. Minor version within the 2.x major — no API changes; `multer()`, `upload.single()`, `upload.fields()`, and `multer.MulterError` behave identically. ## Advisories cleared | CVE | severity | fixed in | |---|---|---| | CVE-2026-3304 | high | 2.1.0 | | CVE-2026-2359 | high | 2.1.0 | | CVE-2026-3520 | high | 2.1.1 | `pnpm audit` total: 123 → 120 (`−3 high`). --- ghost/core/package.json | 2 +- pnpm-lock.yaml | 18 ++---------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/ghost/core/package.json b/ghost/core/package.json index 899c93e2845..4ec704dd045 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -217,7 +217,7 @@ "mingo": "2.5.3", "moment": "2.24.0", "moment-timezone": "0.5.45", - "multer": "2.0.2", + "multer": "2.1.1", "mysql2": "3.18.1", "nconf": "0.13.0", "node-fetch": "2.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99d40b42928..ea91f60035e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2351,8 +2351,8 @@ importers: specifier: 0.5.45 version: 0.5.45 multer: - specifier: 2.0.2 - version: 2.0.2 + specifier: 2.1.1 + version: 2.1.1 mysql2: specifier: 3.18.1 version: 3.18.1(@types/node@22.19.17) @@ -17308,10 +17308,6 @@ packages: typescript: optional: true - multer@2.0.2: - resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} - engines: {node: '>= 10.16.0'} - multer@2.1.1: resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} engines: {node: '>= 10.16.0'} @@ -42116,16 +42112,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - multer@2.0.2: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - mkdirp: 0.5.6 - object-assign: 4.1.1 - type-is: 1.6.18 - xtend: 4.0.2 - multer@2.1.1: dependencies: append-field: 1.0.0 From 8c8d12334bee1d46a4d37765205feb712aa39add Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 27 Apr 2026 14:20:14 -0500 Subject: [PATCH 05/15] Removed some useless test code (#27572) no ref --- .../test/unit/frontend/services/sitemap/generator.test.js | 6 ------ .../email-analytics/email-analytics-service.test.js | 3 --- .../services/link-tracking/post-link-repository.test.js | 3 --- 3 files changed, 12 deletions(-) diff --git a/ghost/core/test/unit/frontend/services/sitemap/generator.test.js b/ghost/core/test/unit/frontend/services/sitemap/generator.test.js index af960177982..251c0bf2e56 100644 --- a/ghost/core/test/unit/frontend/services/sitemap/generator.test.js +++ b/ghost/core/test/unit/frontend/services/sitemap/generator.test.js @@ -357,12 +357,6 @@ describe('Generators', function () { }); }); - describe('TagGenerator', function () { - beforeEach(function () { - generator = new TagGenerator(); - }); - }); - describe('UserGenerator', function () { beforeEach(function () { generator = new UserGenerator(); diff --git a/ghost/core/test/unit/server/services/email-analytics/email-analytics-service.test.js b/ghost/core/test/unit/server/services/email-analytics/email-analytics-service.test.js index fdcb017ac76..a14e059d093 100644 --- a/ghost/core/test/unit/server/services/email-analytics/email-analytics-service.test.js +++ b/ghost/core/test/unit/server/services/email-analytics/email-analytics-service.test.js @@ -988,9 +988,6 @@ describe('EmailAnalyticsService', function () { }); }); - describe('processEvent', function () { - }); - describe('aggregateStats', function () { describe('with batching enabled', function () { let service; diff --git a/ghost/core/test/unit/server/services/link-tracking/post-link-repository.test.js b/ghost/core/test/unit/server/services/link-tracking/post-link-repository.test.js index 164ff3c9385..57a21db3f19 100644 --- a/ghost/core/test/unit/server/services/link-tracking/post-link-repository.test.js +++ b/ghost/core/test/unit/server/services/link-tracking/post-link-repository.test.js @@ -25,9 +25,6 @@ describe('UNIT: PostLinkRepository class', function () { }); }); - beforeEach(function () { - }); - afterEach(function () { sinon.restore(); }); From 30ab483ac3377b1e5f4e7ac4ed535e3740f73d9c Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 27 Apr 2026 14:20:22 -0500 Subject: [PATCH 06/15] Wait for Handlebars function in `{{#recommendations}}` test (#27571) no ref `hbs.cachePartials` is async, but we weren't waiting for it. Let's start doing that, which should make this test more reliable. --- .../core/test/unit/frontend/helpers/recommendations.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ghost/core/test/unit/frontend/helpers/recommendations.test.js b/ghost/core/test/unit/frontend/helpers/recommendations.test.js index efc1e62f1a8..579b968e0d3 100644 --- a/ghost/core/test/unit/frontend/helpers/recommendations.test.js +++ b/ghost/core/test/unit/frontend/helpers/recommendations.test.js @@ -7,6 +7,7 @@ const configUtils = require('../../../utils/config-utils'); const {html} = require('common-tags'); const loggingLib = require('@tryghost/logging'); const proxy = require('../../../../core/frontend/services/proxy'); +const {promisify} = require('node:util'); const recommendations = require('../../../../core/frontend/helpers/recommendations'); const foreach = require('../../../../core/frontend/helpers/foreach'); @@ -20,14 +21,15 @@ function trimSpaces(string) { describe('{{#recommendations}} helper', function () { let logging; - before(function () { + before(async function () { models.init(); hbs.express4({ partialsDir: [configUtils.config.get('paths').helperTemplates] }); - hbs.cachePartials(); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); // The recommendation template expects this helper hbs.registerHelper('foreach', foreach); From 4fc76e2ab8fce1cedecba0acbe5fccbed3b3ba7e Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 14:56:43 -0500 Subject: [PATCH 07/15] Added @xmldom/xmldom override to clear 5 high-severity advisories (#27576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Adds a single override to root `pnpm.overrides`: ```json "@xmldom/xmldom@<0.8.13": "^0.8.13" ``` `@xmldom/xmldom` is transitive only — pulled in by `ghost/admin > testem`. Resolved version after the override is `0.8.13`, a patch-level bump within the 0.8.x line; no API surface change. The override is removable when `testem` is bumped to a version that declares `@xmldom/xmldom >= 0.8.13` directly. ## Audit delta `pnpm audit`: 123 → 118 (`−5 high`). ## Test plan - [x] `pnpm install` clean; `@xmldom/xmldom@0.8.13` resolved (single version, no major drift) - [x] `ghost/admin` Ember test suite (testem is the consumer) — 1065 / 1065 passing - [ ] CI green --- package.json | 3 ++- pnpm-lock.yaml | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 80043949975..0f8dfd3d901 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,8 @@ "minimatch@>=9.0.0 <9.0.7": "^9.0.7", "qs@>=6.7.0 <=6.14.1": "^6.14.2", "tar@<7.5.11": "^7.5.11", - "tmp@<=0.2.3": "^0.2.4" + "tmp@<=0.2.3": "^0.2.4", + "@xmldom/xmldom@<0.8.13": "^0.8.13" }, "onlyBuiltDependencies": [ "@swc/core", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea91f60035e..e6b05499b2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,7 @@ overrides: qs@>=6.7.0 <=6.14.1: ^6.14.2 tar@<7.5.11: ^7.5.11 tmp@<=0.2.3: ^0.2.4 + '@xmldom/xmldom@<0.8.13': ^0.8.13 importers: @@ -9457,10 +9458,9 @@ packages: '@webcontainer/env@1.1.1': resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} - '@xmldom/xmldom@0.8.11': - resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} - deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -31300,7 +31300,7 @@ snapshots: '@webcontainer/env@1.1.1': {} - '@xmldom/xmldom@0.8.11': {} + '@xmldom/xmldom@0.8.13': {} '@xtuc/ieee754@1.2.0': {} @@ -46540,7 +46540,7 @@ snapshots: testem@3.19.1(@babel/core@7.29.0)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): dependencies: - '@xmldom/xmldom': 0.8.11 + '@xmldom/xmldom': 0.8.13 backbone: 1.6.1 charm: 1.0.2 commander: 2.20.3 From f59bb2a5794d0b00f3d5a440a8038cef02a1ca88 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 27 Apr 2026 15:01:42 -0500 Subject: [PATCH 08/15] Switched to promises for image dimension tests (#27570) no ref _I recommend reviewing this with whitespace changes disabled._ This is a test-only change. --- .../frontend/meta/image-dimensions.test.js | 266 +++++++++--------- 1 file changed, 126 insertions(+), 140 deletions(-) diff --git a/ghost/core/test/unit/frontend/meta/image-dimensions.test.js b/ghost/core/test/unit/frontend/meta/image-dimensions.test.js index 9853e49fb00..9c5c92ed595 100644 --- a/ghost/core/test/unit/frontend/meta/image-dimensions.test.js +++ b/ghost/core/test/unit/frontend/meta/image-dimensions.test.js @@ -16,7 +16,7 @@ describe('getImageDimensions', function () { sinon.restore(); }); - it('should return dimension for images', function (done) { + it('should return dimension for images', async function () { const metaData = { coverImage: { url: 'http://mysite.com/content/image/mypostcoverimage.jpg' @@ -44,33 +44,31 @@ describe('getImageDimensions', function () { getCachedImageSizeFromUrl: sizeOfStub }); - getImageDimensions(metaData).then(function (result) { - assertExists(result); - sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); - assert('dimensions' in result.coverImage); - assert('url' in result.coverImage); - assert.equal(result.coverImage.dimensions.width, 50); - assert.equal(result.coverImage.dimensions.height, 50); - assert('dimensions' in result.authorImage); - assert('url' in result.authorImage); - assert.equal(result.authorImage.dimensions.width, 50); - assert.equal(result.authorImage.dimensions.height, 50); - assert('dimensions' in result.ogImage); - assert('url' in result.ogImage); - assert.equal(result.ogImage.dimensions.width, 50); - assert.equal(result.ogImage.dimensions.height, 50); - assert('dimensions' in result.site.logo); - assert('url' in result.site.logo); - assert.equal(result.site.logo.dimensions.width, 50); - assert.equal(result.site.logo.dimensions.height, 50); - done(); - }).catch(done); + const result = await getImageDimensions(metaData); + assertExists(result); + sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); + assert('dimensions' in result.coverImage); + assert('url' in result.coverImage); + assert.equal(result.coverImage.dimensions.width, 50); + assert.equal(result.coverImage.dimensions.height, 50); + assert('dimensions' in result.authorImage); + assert('url' in result.authorImage); + assert.equal(result.authorImage.dimensions.width, 50); + assert.equal(result.authorImage.dimensions.height, 50); + assert('dimensions' in result.ogImage); + assert('url' in result.ogImage); + assert.equal(result.ogImage.dimensions.width, 50); + assert.equal(result.ogImage.dimensions.height, 50); + assert('dimensions' in result.site.logo); + assert('url' in result.site.logo); + assert.equal(result.site.logo.dimensions.width, 50); + assert.equal(result.site.logo.dimensions.height, 50); }); - it('should return metaData if url is undefined or null', function (done) { + it('should return metaData if url is undefined or null', async function () { const metaData = { coverImage: { url: undefined @@ -97,25 +95,23 @@ describe('getImageDimensions', function () { getCachedImageSizeFromUrl: sizeOfStub }); - getImageDimensions(metaData).then(function (result) { - assertExists(result); - sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); - assert(!('dimensions' in result.coverImage)); - assert('url' in result.coverImage); - assert(!('dimensions' in result.authorImage)); - assert('url' in result.authorImage); - assert(!('dimensions' in result.ogImage)); - assert('url' in result.ogImage); - assert(!('dimensions' in result.site.logo)); - assert('url' in result.site.logo); - done(); - }).catch(done); + const result = await getImageDimensions(metaData); + assertExists(result); + sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); + assert(!('dimensions' in result.coverImage)); + assert('url' in result.coverImage); + assert(!('dimensions' in result.authorImage)); + assert('url' in result.authorImage); + assert(!('dimensions' in result.ogImage)); + assert('url' in result.ogImage); + assert(!('dimensions' in result.site.logo)); + assert('url' in result.site.logo); }); - it('should fake image dimension for publisher.logo if file is too big and square', function (done) { + it('should fake image dimension for publisher.logo if file is too big and square', async function () { const metaData = { coverImage: { url: 'http://mysite.com/content/image/mypostcoverimage.jpg' @@ -143,33 +139,31 @@ describe('getImageDimensions', function () { getCachedImageSizeFromUrl: sizeOfStub }); - getImageDimensions(metaData).then(function (result) { - assertExists(result); - sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); - assert('url' in result.coverImage); - assert('dimensions' in result.coverImage); - assert.equal(result.coverImage.dimensions.height, 480); - assert.equal(result.coverImage.dimensions.width, 480); - assert('url' in result.site.logo); - assert('dimensions' in result.site.logo); - assert.equal(result.site.logo.dimensions.height, 60); - assert.equal(result.site.logo.dimensions.width, 60); - assert('url' in result.authorImage); - assert('dimensions' in result.authorImage); - assert.equal(result.authorImage.dimensions.height, 480); - assert.equal(result.authorImage.dimensions.width, 480); - assert('url' in result.ogImage); - assert('dimensions' in result.ogImage); - assert.equal(result.ogImage.dimensions.height, 480); - assert.equal(result.ogImage.dimensions.width, 480); - done(); - }).catch(done); + const result = await getImageDimensions(metaData); + assertExists(result); + sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); + assert('url' in result.coverImage); + assert('dimensions' in result.coverImage); + assert.equal(result.coverImage.dimensions.height, 480); + assert.equal(result.coverImage.dimensions.width, 480); + assert('url' in result.site.logo); + assert('dimensions' in result.site.logo); + assert.equal(result.site.logo.dimensions.height, 60); + assert.equal(result.site.logo.dimensions.width, 60); + assert('url' in result.authorImage); + assert('dimensions' in result.authorImage); + assert.equal(result.authorImage.dimensions.height, 480); + assert.equal(result.authorImage.dimensions.width, 480); + assert('url' in result.ogImage); + assert('dimensions' in result.ogImage); + assert.equal(result.ogImage.dimensions.height, 480); + assert.equal(result.ogImage.dimensions.width, 480); }); - it('should not fake dimension for publisher.logo if a logo is too big but not square', function (done) { + it('should not fake dimension for publisher.logo if a logo is too big but not square', async function () { const metaData = { coverImage: { url: 'http://mysite.com/content/image/mypostcoverimage.jpg' @@ -197,31 +191,29 @@ describe('getImageDimensions', function () { getCachedImageSizeFromUrl: sizeOfStub }); - getImageDimensions(metaData).then(function (result) { - assertExists(result); - sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); - sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); - assert('dimensions' in result.coverImage); - assert('url' in result.coverImage); - assert.equal(result.coverImage.dimensions.height, 480); - assert.equal(result.coverImage.dimensions.width, 80); - assert('dimensions' in result.authorImage); - assert('url' in result.authorImage); - assert.equal(result.authorImage.dimensions.height, 480); - assert.equal(result.authorImage.dimensions.width, 80); - assert('dimensions' in result.ogImage); - assert('url' in result.ogImage); - assert.equal(result.ogImage.dimensions.height, 480); - assert.equal(result.ogImage.dimensions.width, 80); - assert('url' in result.site.logo); - assert(!('dimensions' in result.site.logo)); - done(); - }).catch(done); + const result = await getImageDimensions(metaData); + assertExists(result); + sinon.assert.calledWith(sizeOfStub, metaData.coverImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.authorImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.ogImage.url); + sinon.assert.calledWith(sizeOfStub, metaData.site.logo.url); + assert('dimensions' in result.coverImage); + assert('url' in result.coverImage); + assert.equal(result.coverImage.dimensions.height, 480); + assert.equal(result.coverImage.dimensions.width, 80); + assert('dimensions' in result.authorImage); + assert('url' in result.authorImage); + assert.equal(result.authorImage.dimensions.height, 480); + assert.equal(result.authorImage.dimensions.width, 80); + assert('dimensions' in result.ogImage); + assert('url' in result.ogImage); + assert.equal(result.ogImage.dimensions.height, 480); + assert.equal(result.ogImage.dimensions.width, 80); + assert('url' in result.site.logo); + assert(!('dimensions' in result.site.logo)); }); - it('should adjust image sizes to a max width', function (done) { + it('should adjust image sizes to a max width', async function () { const originalMetaData = { coverImage: { url: 'http://mysite.com/content/images/mypostcoverimage.jpg' @@ -257,34 +249,32 @@ describe('getImageDimensions', function () { getCachedImageSizeFromUrl: sizeOfStub }); - getImageDimensions(metaData).then(function (result) { - assertExists(result); - sinon.assert.calledWith(sizeOfStub, originalMetaData.coverImage.url); - sinon.assert.calledWith(sizeOfStub, originalMetaData.authorImage.url); - sinon.assert.calledWith(sizeOfStub, originalMetaData.ogImage.url); - sinon.assert.calledWith(sizeOfStub, originalMetaData.twitterImage); - sinon.assert.calledWith(sizeOfStub, originalMetaData.site.logo.url); - assert('url' in result.coverImage); - assert.equal(result.coverImage.url, 'http://mysite.com/content/images/size/w1200/mypostcoverimage.jpg'); - assert('dimensions' in result.coverImage); - assert.equal(result.coverImage.dimensions.width, 1200); - assert.equal(result.coverImage.dimensions.height, 720); - assert('url' in result.authorImage); - assert.equal(result.authorImage.url, 'http://mysite.com/content/images/size/w1200/me.jpg'); - assert('dimensions' in result.authorImage); - assert.equal(result.authorImage.dimensions.width, 1200); - assert.equal(result.authorImage.dimensions.height, 720); - assert('url' in result.ogImage); - assert.equal(result.ogImage.url, 'http://mysite.com/content/images/size/w1200/super-facebook-image.jpg'); - assert('dimensions' in result.ogImage); - assert.equal(result.ogImage.dimensions.width, 1200); - assert.equal(result.ogImage.dimensions.height, 720); - assert.equal(result.twitterImage, 'http://mysite.com/content/images/size/w1200/super-twitter-image.jpg'); - done(); - }).catch(done); + const result = await getImageDimensions(metaData); + assertExists(result); + sinon.assert.calledWith(sizeOfStub, originalMetaData.coverImage.url); + sinon.assert.calledWith(sizeOfStub, originalMetaData.authorImage.url); + sinon.assert.calledWith(sizeOfStub, originalMetaData.ogImage.url); + sinon.assert.calledWith(sizeOfStub, originalMetaData.twitterImage); + sinon.assert.calledWith(sizeOfStub, originalMetaData.site.logo.url); + assert('url' in result.coverImage); + assert.equal(result.coverImage.url, 'http://mysite.com/content/images/size/w1200/mypostcoverimage.jpg'); + assert('dimensions' in result.coverImage); + assert.equal(result.coverImage.dimensions.width, 1200); + assert.equal(result.coverImage.dimensions.height, 720); + assert('url' in result.authorImage); + assert.equal(result.authorImage.url, 'http://mysite.com/content/images/size/w1200/me.jpg'); + assert('dimensions' in result.authorImage); + assert.equal(result.authorImage.dimensions.width, 1200); + assert.equal(result.authorImage.dimensions.height, 720); + assert('url' in result.ogImage); + assert.equal(result.ogImage.url, 'http://mysite.com/content/images/size/w1200/super-facebook-image.jpg'); + assert('dimensions' in result.ogImage); + assert.equal(result.ogImage.dimensions.width, 1200); + assert.equal(result.ogImage.dimensions.height, 720); + assert.equal(result.twitterImage, 'http://mysite.com/content/images/size/w1200/super-twitter-image.jpg'); }); - it('does not append image size prefix to external images', function (done) { + it('does not append image size prefix to external images', async function () { const originalMetaData = { coverImage: { url: 'http://anothersite.com/some/storage/mypostcoverimage.jpg' @@ -315,21 +305,19 @@ describe('getImageDimensions', function () { getCachedImageSizeFromUrl: sizeOfStub }); - getImageDimensions(metaData).then(function (result) { - assertExists(result); - assert('url' in result.coverImage); - assert.equal(result.coverImage.url, 'http://anothersite.com/some/storage/mypostcoverimage.jpg'); - assert('url' in result.authorImage); - assert.equal(result.authorImage.url, 'http://anothersite.com/some/storage/me.jpg'); - assert('url' in result.ogImage); - assert.equal(result.ogImage.url, 'http://anothersite.com/some/storage/super-facebook-image.jpg'); - assert('url' in result.site.logo); - assert.equal(result.site.logo.url, 'http://anothersite.com/some/storage/logo.jpg'); - done(); - }).catch(done); + const result = await getImageDimensions(metaData); + assertExists(result); + assert('url' in result.coverImage); + assert.equal(result.coverImage.url, 'http://anothersite.com/some/storage/mypostcoverimage.jpg'); + assert('url' in result.authorImage); + assert.equal(result.authorImage.url, 'http://anothersite.com/some/storage/me.jpg'); + assert('url' in result.ogImage); + assert.equal(result.ogImage.url, 'http://anothersite.com/some/storage/super-facebook-image.jpg'); + assert('url' in result.site.logo); + assert.equal(result.site.logo.url, 'http://anothersite.com/some/storage/logo.jpg'); }); - it('appends image size prefix to CDN-hosted content images', function (done) { + it('appends image size prefix to CDN-hosted content images', async function () { const originalMetaData = { coverImage: { url: 'https://storage.ghost.is/c/6f/a3/site/content/images/2026/02/cover.jpg' @@ -360,15 +348,13 @@ describe('getImageDimensions', function () { getCachedImageSizeFromUrl: sizeOfStub }); - getImageDimensions(metaData).then(function (result) { - assertExists(result); - assert.equal(result.coverImage.url, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/cover.jpg'); - assert.equal(result.authorImage.url, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/author.jpg'); - assert.equal(result.ogImage.url, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/og.jpg'); - assert.equal(result.twitterImage, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/twitter.jpg'); - // logo dimensions are computed but logo URL is not resized in this path - assert.equal(result.site.logo.url, originalMetaData.site.logo.url); - done(); - }).catch(done); + const result = await getImageDimensions(metaData); + assertExists(result); + assert.equal(result.coverImage.url, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/cover.jpg'); + assert.equal(result.authorImage.url, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/author.jpg'); + assert.equal(result.ogImage.url, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/og.jpg'); + assert.equal(result.twitterImage, 'https://storage.ghost.is/c/6f/a3/site/content/images/size/w1200/2026/02/twitter.jpg'); + // logo dimensions are computed but logo URL is not resized in this path + assert.equal(result.site.logo.url, originalMetaData.site.logo.url); }); }); From db6bfab8476fbf70dd5d8f9b1c249b87154a2e10 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 27 Apr 2026 15:10:46 -0500 Subject: [PATCH 09/15] Removed unnecessary `done`s from several tests (#27580) no ref This is a test-only change. I think it's useful on its own, but will make upcoming changes (like migrating to Vitest) a bit easier. --- .../test/legacy/site/dynamic-routing.test.js | 423 ++++++------ .../unit/frontend/helpers/comments.test.js | 8 +- .../test/unit/frontend/helpers/get.test.js | 275 ++++---- .../test/unit/frontend/meta/schema.test.js | 12 +- .../frontend/meta/structured-data.test.js | 9 +- .../frontend/services/data/fetch-data.test.js | 174 +++-- .../routing/controllers/channel.test.js | 98 ++- .../routing/controllers/collection.test.js | 121 ++-- .../routing/controllers/previews.test.js | 18 +- .../unit/frontend/services/rss/cache.test.js | 31 +- .../services/rss/generate-feed.test.js | 231 +++---- .../frontend/services/rss/renderer.test.js | 77 +-- .../web/routers/serve-favicon.test.js | 35 +- .../test/unit/server/data/db/backup.test.js | 26 +- .../unit/server/data/exporter/index.test.js | 220 +++---- .../data/importer/handlers/image.test.js | 103 ++- .../unit/server/data/importer/index.test.js | 329 +++++----- .../schema/fixtures/fixture-manager.test.js | 160 ++--- .../unit/server/lib/image/image-size.test.js | 325 ++++----- .../unit/server/lib/package-json/read.test.js | 273 ++++---- .../core/test/unit/server/models/post.test.js | 618 ++++++++---------- .../test/unit/server/models/session.test.js | 78 ++- .../core/test/unit/server/models/user.test.js | 88 +-- .../services/permissions/can-this.test.js | 456 ++++++------- .../server/services/permissions/index.test.js | 40 +- .../services/permissions/providers.test.js | 253 ++++--- .../server/services/url/url-service.test.js | 15 +- 27 files changed, 1999 insertions(+), 2497 deletions(-) diff --git a/ghost/core/test/legacy/site/dynamic-routing.test.js b/ghost/core/test/legacy/site/dynamic-routing.test.js index a17c6aeca97..521e785c8be 100644 --- a/ghost/core/test/legacy/site/dynamic-routing.test.js +++ b/ghost/core/test/legacy/site/dynamic-routing.test.js @@ -16,19 +16,14 @@ const themeEngine = require('../../../core/frontend/services/theme-engine'); let request; describe('Dynamic Routing', function () { - function doEnd(done) { - return function (err, res) { - if (err) { - return done(err); - } - - assert.equal(res.headers['x-cache-invalidate'], undefined); - assert.equal(res.headers['X-CSRF-Token'], undefined); - assert.equal(res.headers['set-cookie'], undefined); - assertExists(res.headers.date); - - done(); - }; + /** + * @param {Readonly>} headers + */ + function assertCorrectHeaders(headers) { + assert.equal(headers['x-cache-invalidate'], undefined); + assert.equal(headers['X-CSRF-Token'], undefined); + assert.equal(headers['set-cookie'], undefined); + assertExists(headers.date); } before(async function () { @@ -47,37 +42,26 @@ describe('Dynamic Routing', function () { }); describe('Collection Index', function () { - it('should respond with html', function (done) { - request.get('/') + it('should respond with html', async function () { + const res = await request.get('/') .expect('Content-Type', /html/) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } + .expect(200); - const $ = cheerio.load(res.text); + assertCorrectHeaders(res.headers); - assert.equal(res.headers['x-cache-invalidate'], undefined); - assert.equal(res.headers['X-CSRF-Token'], undefined); - assert.equal(res.headers['set-cookie'], undefined); - assertExists(res.headers.date); - - assert.equal($('title').text(), 'Ghost'); - assert.equal($('body.home-template').length, 1); - assert.equal($('article.post').length, 7); - - done(); - }); + const $ = cheerio.load(res.text); + assert.equal($('title').text(), 'Ghost'); + assert.equal($('body.home-template').length, 1); + assert.equal($('article.post').length, 7); }); - it('should not have a third page', function (done) { - request.get('/page/3/') + it('should not have a third page', async function () { + const res = await request.get('/page/3/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); }); @@ -90,22 +74,22 @@ describe('Dynamic Routing', function () { }); }); - it('should render page with slug permalink', function (done) { - request.get('/static-page-test/') + it('should render page with slug permalink', async function () { + const res = await request.get('/static-page-test/') .expect('Content-Type', /html/) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); + .expect(200); + assertCorrectHeaders(res.headers); }); - it('should not render page with dated permalink', function (done) { + it('should not render page with dated permalink', async function () { const date = moment().format('YYYY/MM/DD'); - request.get('/' + date + '/static-page-test/') + const res = await request.get('/' + date + '/static-page-test/') .expect('Content-Type', /html/) .expect('Cache-Control', testUtils.cacheRules.noCache) - .expect(404) - .end(doEnd(done)); + .expect(404); + assertCorrectHeaders(res.headers); }); }); @@ -118,97 +102,86 @@ describe('Dynamic Routing', function () { after(testUtils.teardownDb); - it('should return HTML for valid route', function (done) { - request.get('/tag/getting-started/') + it('should return HTML for valid route', async function () { + const res = await request.get('/tag/getting-started/') .expect(200) .expect('Content-Type', /html/) .expect('Content-Type', /html/) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(function (err, res) { - if (err) { - return done(err); - } - - const $ = cheerio.load(res.text); - - assert.equal(res.headers['x-cache-invalidate'], undefined); - assert.equal(res.headers['X-CSRF-Token'], undefined); - assert.equal(res.headers['set-cookie'], undefined); - assertExists(res.headers.date); + .expect(200); - assert.equal($('body').attr('class'), 'tag-template tag-getting-started has-sans-title has-sans-body'); - assert.equal($('article.post').length, 5); + assertCorrectHeaders(res.headers); - done(); - }); + const $ = cheerio.load(res.text); + assert.equal($('body').attr('class'), 'tag-template tag-getting-started has-sans-title has-sans-body'); + assert.equal($('article.post').length, 5); }); - it('should 404 for /tag/ route', function (done) { - request.get('/tag/') + it('should 404 for /tag/ route', async function () { + const res = await request.get('/tag/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('should 404 for unknown tag', function (done) { - request.get('/tag/spectacular/') + it('should 404 for unknown tag', async function () { + const res = await request.get('/tag/spectacular/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('should 404 for unknown tag with invalid characters', function (done) { - request.get('/tag/~$pectacular~/') + it('should 404 for unknown tag with invalid characters', async function () { + const res = await request.get('/tag/~$pectacular~/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); describe('RSS', function () { - it('should redirect without slash', function (done) { - request.get('/tag/getting-started/rss') + it('should redirect without slash', async function () { + const res = await request.get('/tag/getting-started/rss') .expect('Location', '/tag/getting-started/rss/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should respond with xml', function (done) { - request.get('/tag/getting-started/rss/') + it('should respond with xml', async function () { + const res = await request.get('/tag/getting-started/rss/') .expect('Content-Type', /xml/) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); + .expect(200); + assertCorrectHeaders(res.headers); }); }); describe('Edit', function () { - it('should redirect without slash', function (done) { - request.get('/tag/getting-started/edit') + it('should redirect without slash', async function () { + const res = await request.get('/tag/getting-started/edit') .expect('Location', '/tag/getting-started/edit/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should redirect to tag settings', function (done) { - request.get('/tag/getting-started/edit/') + it('should redirect to tag settings', async function () { + const res = await request.get('/tag/getting-started/edit/') .expect('Location', /\/ghost\/#\/tags\/getting-started\//) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(302) - .end(doEnd(done)); + .expect(302); + assertCorrectHeaders(res.headers); }); - it('should 404 for non-edit parameter', function (done) { - request.get('/tag/getting-started/notedit/') + it('should 404 for non-edit parameter', async function () { + const res = await request.get('/tag/getting-started/notedit/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); }); @@ -231,19 +204,19 @@ describe('Dynamic Routing', function () { request = supertest.agent(config.get('url')); }); - it('should redirect without slash', function (done) { - request.get('/tag/getting-started/edit') + it('should redirect without slash', async function () { + const res = await request.get('/tag/getting-started/edit') .expect('Location', '/tag/getting-started/edit/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should not redirect to admin', function (done) { - request.get('/tag/getting-started/edit/') + it('should not redirect to admin', async function () { + const res = await request.get('/tag/getting-started/edit/') .expect(404) - .expect('Cache-Control', testUtils.cacheRules.noCache) - .end(doEnd(done)); + .expect('Cache-Control', testUtils.cacheRules.noCache); + assertCorrectHeaders(res.headers); }); }); }); @@ -265,150 +238,148 @@ describe('Dynamic Routing', function () { const ownerSlug = 'ghost-owner'; - before(function (done) { - testUtils.teardownDb().then(function () { - // we initialise data, but not a user. No user should be required for navigating the frontend - return testUtils.initData(); - }).then(function () { - return testUtils.fixtures.overrideOwnerUser(ownerSlug); - }).then(function (insertedUser) { - return testUtils.fixtures.insertPosts([ + before(async function () { + await testUtils.teardownDb(); + // we initialise data, but not a user. No user should be required for navigating the frontend + await testUtils.initData(); + + { + const insertedUser = await testUtils.fixtures.overrideOwnerUser(ownerSlug); + await testUtils.fixtures.insertPosts([ testUtils.DataGenerator.forKnex.createPost({ authors: [{ id: insertedUser.id }] }) ]); - }).then(function () { - return testUtils.fixtures.insertOneUser(lockedUser); - }).then(function (insertedUser) { - return testUtils.fixtures.insertPosts([ + } + + { + const insertedUser = await testUtils.fixtures.insertOneUser(lockedUser); + await testUtils.fixtures.insertPosts([ testUtils.DataGenerator.forKnex.createPost({ authors: [{ id: insertedUser.id }] }) ]); - }).then(() => { - return testUtils.fixtures.insertOneUser(suspendedUser); - }).then(function (insertedUser) { - return testUtils.fixtures.insertPosts([ + } + + { + const insertedUser = await testUtils.fixtures.insertOneUser(suspendedUser); + await testUtils.fixtures.insertPosts([ testUtils.DataGenerator.forKnex.createPost({ authors: [{ id: insertedUser.id }] }) ]); - }).then(function () { - done(); - }).catch(done); + } }); after(testUtils.teardownDb); - it('should 404 for /author/ route', function (done) { - request.get('/author/') + it('should 404 for /author/ route', async function () { + const res = await request.get('/author/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('should 404 for unknown author', function (done) { - request.get('/author/spectacular/') + it('should 404 for unknown author', async function () { + const res = await request.get('/author/spectacular/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('should 404 for unknown author with invalid characters', function (done) { - request.get('/author/ghost!user^/') + it('should 404 for unknown author with invalid characters', async function () { + const res = await request.get('/author/ghost!user^/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('[success] author is locked', function (done) { - request.get('/author/' + lockedUser.slug + '/') + it('[success] author is locked', async function () { + const res = await request.get('/author/' + lockedUser.slug + '/') .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); + .expect(200); + assertCorrectHeaders(res.headers); }); - it('[success] author is suspended', function (done) { - request.get('/author/' + suspendedUser.slug + '/') + it('[success] author is suspended', async function () { + const res = await request.get('/author/' + suspendedUser.slug + '/') .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); + .expect(200); + assertCorrectHeaders(res.headers); }); - it('[failure] ghost owner before blog setup', function (done) { - testUtils.fixtures.changeOwnerUserStatus({ + it('[failure] ghost owner before blog setup', async function () { + await testUtils.fixtures.changeOwnerUserStatus({ slug: ownerSlug, status: 'inactive' - }).then(function () { - request.get('/author/ghost-owner/') - .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); - }).catch(done); + }); + const res = await request.get('/author/ghost-owner/') + .expect('Cache-Control', testUtils.cacheRules.public) + .expect(200); + assertCorrectHeaders(res.headers); }); - it('[success] ghost owner after blog setup', function (done) { - testUtils.fixtures.changeOwnerUserStatus({ + it('[success] ghost owner after blog setup', async function () { + await testUtils.fixtures.changeOwnerUserStatus({ slug: ownerSlug, status: 'active' - }).then(function () { - request.get('/author/ghost-owner/') - .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); }); + const res = await request.get('/author/ghost-owner/') + .expect('Cache-Control', testUtils.cacheRules.public) + .expect(200); + assertCorrectHeaders(res.headers); }); describe('RSS', function () { - it('should redirect without slash', function (done) { - request.get('/author/ghost-owner/rss') + it('should redirect without slash', async function () { + const res = await request.get('/author/ghost-owner/rss') .expect('Location', '/author/ghost-owner/rss/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should respond with xml', function (done) { - request.get('/author/ghost-owner/rss/') + it('should respond with xml', async function () { + const res = await request.get('/author/ghost-owner/rss/') .expect('Content-Type', /xml/) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); + .expect(200); + assertCorrectHeaders(res.headers); }); }); describe('Edit', function () { - it('should redirect without slash', function (done) { - request.get('/author/ghost-owner/edit') + it('should redirect without slash', async function () { + const res = await request.get('/author/ghost-owner/edit') .expect('Location', '/author/ghost-owner/edit/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should redirect to editor', function (done) { - request.get('/author/ghost-owner/edit/') + it('should redirect to editor', async function () { + const res = await request.get('/author/ghost-owner/edit/') .expect('Location', /\/ghost\/#\/settings\/staff\/ghost-owner\//) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(302) - .end(doEnd(done)); + .expect(302); + assertCorrectHeaders(res.headers); }); - it('should 404 for something that isn\'t edit', function (done) { - request.get('/author/ghost-owner/notedit/') + it('should 404 for something that isn\'t edit', async function () { + const res = await request.get('/author/ghost-owner/notedit/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); }); @@ -431,104 +402,98 @@ describe('Dynamic Routing', function () { request = supertest.agent(config.get('url')); }); - it('should redirect without slash', function (done) { - request.get('/author/ghost-owner/edit') + it('should redirect without slash', async function () { + const res = await request.get('/author/ghost-owner/edit') .expect('Location', '/author/ghost-owner/edit/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should not redirect to admin', function (done) { - request.get('/author/ghost-owner/edit/') + it('should not redirect to admin', async function () { + const res = await request.get('/author/ghost-owner/edit/') .expect('Cache-Control', testUtils.cacheRules.noCache) - .expect(404) - .end(doEnd(done)); + .expect(404); + assertCorrectHeaders(res.headers); }); }); describe('Paged', function () { // Add enough posts to trigger pages - before(function (done) { - testUtils.teardownDb().then(function () { - // we initialize data, but not a user. No user should be required for navigating the frontend - return testUtils.initData(); - }).then(function () { - return testUtils.fixtures.insertPostsAndTags(); - }).then(function () { - return testUtils.fixtures.insertExtraPosts(9); - }).then(function () { - return testUtils.fixtures.overrideOwnerUser('ghost-owner'); - }).then(function () { - done(); - }).catch(done); + before(async function () { + await testUtils.teardownDb(); + // we initialize data, but not a user. No user should be required for navigating the frontend + await testUtils.initData(); + await testUtils.fixtures.insertPostsAndTags(); + await testUtils.fixtures.insertExtraPosts(9); + await testUtils.fixtures.overrideOwnerUser('ghost-owner'); }); after(testUtils.teardownDb); - it('should redirect without slash', function (done) { - request.get('/author/ghost-owner/page/2') + it('should redirect without slash', async function () { + const res = await request.get('/author/ghost-owner/page/2') .expect('Location', '/author/ghost-owner/page/2/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should respond with html', function (done) { - request.get('/author/ghost-owner/page/2/') + it('should respond with html', async function () { + const res = await request.get('/author/ghost-owner/page/2/') .expect('Content-Type', /html/) .expect('Cache-Control', testUtils.cacheRules.public) - .expect(200) - .end(doEnd(done)); + .expect(200); + assertCorrectHeaders(res.headers); }); - it('should redirect page 1', function (done) { - request.get('/author/ghost-owner/page/1/') + it('should redirect page 1', async function () { + const res = await request.get('/author/ghost-owner/page/1/') .expect('Location', '/author/ghost-owner/') .expect('Cache-Control', testUtils.cacheRules.year) - .expect(301) - .end(doEnd(done)); + .expect(301); + assertCorrectHeaders(res.headers); }); - it('should 404 if page too high', function (done) { - request.get('/author/ghost-owner/page/6/') + it('should 404 if page too high', async function () { + const res = await request.get('/author/ghost-owner/page/6/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('should 404 if page too low', function (done) { - request.get('/author/ghost-owner/page/0/') + it('should 404 if page too low', async function () { + const res = await request.get('/author/ghost-owner/page/0/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); describe('RSS', function () { - it('should 404 if index attempted with 0', function (done) { - request.get('/author/ghost-owner/rss/0/') + it('should 404 if index attempted with 0', async function () { + const res = await request.get('/author/ghost-owner/rss/0/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('should 404 if index attempted with 1', function (done) { - request.get('/author/ghost-owner/rss/1/') + it('should 404 if index attempted with 1', async function () { + const res = await request.get('/author/ghost-owner/rss/1/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); - it('should 404 for other pages', function (done) { - request.get('/author/ghost-owner/rss/2/') + it('should 404 for other pages', async function () { + const res = await request.get('/author/ghost-owner/rss/2/') .expect('Cache-Control', testUtils.cacheRules.noCache) .expect(404) - .expect(/Page not found/) - .end(doEnd(done)); + .expect(/Page not found/); + assertCorrectHeaders(res.headers); }); }); }); diff --git a/ghost/core/test/unit/frontend/helpers/comments.test.js b/ghost/core/test/unit/frontend/helpers/comments.test.js index ece96dfa6e9..bdcb04b9a9b 100644 --- a/ghost/core/test/unit/frontend/helpers/comments.test.js +++ b/ghost/core/test/unit/frontend/helpers/comments.test.js @@ -32,14 +32,12 @@ describe('{{comments}} helper', function () { await configUtils.restore(); }); - it('returns undefined if not used withing post context', function (done) { + it('returns undefined if not used withing post context', async function () { settingsCacheGetStub.withArgs('members_enabled').returns(true); settingsCacheGetStub.withArgs('comments_enabled').returns('all'); - comments({}).then(function (rendered) { - assert.equal(rendered, undefined); - done(); - }).catch(done); + const rendered = await comments({}); + assert.equal(rendered, undefined); }); it('returns a script tag', async function () { diff --git a/ghost/core/test/unit/frontend/helpers/get.test.js b/ghost/core/test/unit/frontend/helpers/get.test.js index 49d6d2e5fc1..69116fd80d4 100644 --- a/ghost/core/test/unit/frontend/helpers/get.test.js +++ b/ghost/core/test/unit/frontend/helpers/get.test.js @@ -91,21 +91,18 @@ describe('{{#get}} helper', function () { }); }); - it('converts html strings to SafeString', function (done) { - get.call( + it('converts html strings to SafeString', async function () { + await get.call( {}, 'posts', {hash: {}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - sinon.assert.called(fn); - const args = fn.firstCall.args[0]; - assert(args && typeof args === 'object'); - assert('posts' in args); - - assert(fn.firstCall.args[0].posts[0].feature_image_caption instanceof SafeString); + ); + sinon.assert.called(fn); + const args = fn.firstCall.args[0]; + assert(args && typeof args === 'object'); + assert('posts' in args); - done(); - }).catch(done); + assert(fn.firstCall.args[0].posts[0].feature_image_caption instanceof SafeString); }); }); @@ -122,21 +119,18 @@ describe('{{#get}} helper', function () { }); }); - it('browse authors', function (done) { - get.call( + it('browse authors', async function () { + await get.call( {}, 'authors', {hash: {}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - sinon.assert.called(fn); - const args = fn.firstCall.args[0]; - assert(args && typeof args === 'object'); - assert('authors' in args); - assert.deepEqual(fn.firstCall.args[0].authors, []); - sinon.assert.notCalled(inverse); - - done(); - }).catch(done); + ); + sinon.assert.called(fn); + const args = fn.firstCall.args[0]; + assert(args && typeof args === 'object'); + assert('authors' in args); + assert.deepEqual(fn.firstCall.args[0].authors, []); + sinon.assert.notCalled(inverse); }); }); @@ -153,76 +147,64 @@ describe('{{#get}} helper', function () { }); }); - it('browse newsletters', function (done) { - get.call( + it('browse newsletters', async function () { + await get.call( {}, 'newsletters', {hash: {}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - sinon.assert.called(fn); - const args = fn.firstCall.args[0]; - assert(args && typeof args === 'object'); - assert('newsletters' in args); - assert.deepEqual(fn.firstCall.args[0].newsletters, []); - sinon.assert.notCalled(inverse); - - done(); - }).catch(done); + ); + sinon.assert.called(fn); + const args = fn.firstCall.args[0]; + assert(args && typeof args === 'object'); + assert('newsletters' in args); + assert.deepEqual(fn.firstCall.args[0].newsletters, []); + sinon.assert.notCalled(inverse); }); }); describe('general error handling', function () { - it('should return an error for an unknown resource', function (done) { - get.call( + it('should return an error for an unknown resource', async function () { + await get.call( {}, 'magic', {hash: {}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - sinon.assert.notCalled(fn); - sinon.assert.calledOnce(inverse); - const args = inverse.firstCall.args[1]; - assert(args && typeof args === 'object'); - assert('data' in args); - const data = args.data; - assert(data && typeof data === 'object'); - assert('error' in data); - assert.equal(data.error, 'Invalid "magic" resource given to get helper'); - - done(); - }).catch(done); + ); + sinon.assert.notCalled(fn); + sinon.assert.calledOnce(inverse); + const args = inverse.firstCall.args[1]; + assert(args && typeof args === 'object'); + assert('data' in args); + const data = args.data; + assert(data && typeof data === 'object'); + assert('error' in data); + assert.equal(data.error, 'Invalid "magic" resource given to get helper'); }); - it('should handle error from the API', function (done) { - get.call( + it('should handle error from the API', async function () { + await get.call( {}, 'posts', {hash: {slug: 'thing!'}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - sinon.assert.notCalled(fn); - sinon.assert.calledOnce(inverse); - const args = inverse.firstCall.args[1]; - assert(args && typeof args === 'object'); - assert('data' in args); - const data = args.data; - assert(data && typeof data === 'object'); - assert('error' in data); - assert.match(data.error, /^Validation/); - - done(); - }).catch(done); + ); + sinon.assert.notCalled(fn); + sinon.assert.calledOnce(inverse); + const args = inverse.firstCall.args[1]; + assert(args && typeof args === 'object'); + assert('data' in args); + const data = args.data; + assert(data && typeof data === 'object'); + assert('error' in data); + assert.match(data.error, /^Validation/); }); - it('should show warning for call without any options', function (done) { - get.call( + it('should show warning for call without any options', async function () { + await get.call( {}, 'posts', {data: locals} - ).then(function () { - sinon.assert.notCalled(fn); - sinon.assert.notCalled(inverse); - - done(); - }).catch(done); + ); + sinon.assert.notCalled(fn); + sinon.assert.notCalled(inverse); }); }); @@ -246,123 +228,102 @@ describe('{{#get}} helper', function () { }); }); - it('should resolve post.tags alias', function (done) { - get.call( + it('should resolve post.tags alias', async function () { + await get.call( resource, 'posts', {hash: {filter: 'tags:[{{post.tags}}]'}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - assert(Array.isArray(browseStub.firstCall.args)); - assert.equal(browseStub.firstCall.args.length, 1); - const options = browseStub.firstCall.args[0]; - assert(options && typeof options === 'object'); - assert('filter' in options); - assert.equal(options.filter, 'tags:[test,magic]'); - - done(); - }).catch(done); + ); + assert(Array.isArray(browseStub.firstCall.args)); + assert.equal(browseStub.firstCall.args.length, 1); + const options = browseStub.firstCall.args[0]; + assert(options && typeof options === 'object'); + assert('filter' in options); + assert.equal(options.filter, 'tags:[test,magic]'); }); - it('should resolve post.author alias', function (done) { - get.call( + it('should resolve post.author alias', async function () { + await get.call( resource, 'posts', {hash: {filter: 'author:{{post.author}}'}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - assert(Array.isArray(browseStub.firstCall.args)); - assert.equal(browseStub.firstCall.args.length, 1); - const options = browseStub.firstCall.args[0]; - assert(options && typeof options === 'object'); - assert('filter' in options); - assert.equal(options.filter, 'author:cameron'); - - done(); - }).catch(done); + ); + assert(Array.isArray(browseStub.firstCall.args)); + assert.equal(browseStub.firstCall.args.length, 1); + const options = browseStub.firstCall.args[0]; + assert(options && typeof options === 'object'); + assert('filter' in options); + assert.equal(options.filter, 'author:cameron'); }); - it('should resolve basic path', function (done) { - get.call( + it('should resolve basic path', async function () { + await get.call( resource, 'posts', {hash: {filter: 'id:-{{post.id}}'}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - assert(Array.isArray(browseStub.firstCall.args)); - assert.equal(browseStub.firstCall.args.length, 1); - const options = browseStub.firstCall.args[0]; - assert(options && typeof options === 'object'); - assert('filter' in options); - assert.equal(options.filter, 'id:-3'); - - done(); - }).catch(done); + ); + assert(Array.isArray(browseStub.firstCall.args)); + assert.equal(browseStub.firstCall.args.length, 1); + const options = browseStub.firstCall.args[0]; + assert(options && typeof options === 'object'); + assert('filter' in options); + assert.equal(options.filter, 'id:-3'); }); - it('should handle arrays the same as handlebars', function (done) { - get.call( + it('should handle arrays the same as handlebars', async function () { + await get.call( resource, 'posts', {hash: {filter: 'tags:{{post.tags.[0].slug}}'}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - assert(Array.isArray(browseStub.firstCall.args)); - assert.equal(browseStub.firstCall.args.length, 1); - const options = browseStub.firstCall.args[0]; - assert(options && typeof options === 'object'); - assert('filter' in options); - assert.equal(options.filter, 'tags:test'); - - done(); - }).catch(done); + ); + assert(Array.isArray(browseStub.firstCall.args)); + assert.equal(browseStub.firstCall.args.length, 1); + const options = browseStub.firstCall.args[0]; + assert(options && typeof options === 'object'); + assert('filter' in options); + assert.equal(options.filter, 'tags:test'); }); - it('should handle dates', function (done) { - get.call( + it('should handle dates', async function () { + await get.call( resource, 'posts', {hash: {filter: 'published_at:<=\'{{post.published_at}}\''}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - assert(Array.isArray(browseStub.firstCall.args)); - assert.equal(browseStub.firstCall.args.length, 1); - const options = browseStub.firstCall.args[0]; - assert(options && typeof options === 'object'); - assert('filter' in options); - assert.equal(options.filter, `published_at:<='${pubDate.toISOString()}'`); - - done(); - }).catch(done); + ); + assert(Array.isArray(browseStub.firstCall.args)); + assert.equal(browseStub.firstCall.args.length, 1); + const options = browseStub.firstCall.args[0]; + assert(options && typeof options === 'object'); + assert('filter' in options); + assert.equal(options.filter, `published_at:<='${pubDate.toISOString()}'`); }); - it('should output nothing if path does not resolve', function (done) { - get.call( + it('should output nothing if path does not resolve', async function () { + await get.call( resource, 'posts', {hash: {filter: 'id:{{post.thing}}'}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - assert(Array.isArray(browseStub.firstCall.args)); - assert.equal(browseStub.firstCall.args.length, 1); - const options = browseStub.firstCall.args[0]; - assert(options && typeof options === 'object'); - assert('filter' in options); - assert.equal(options.filter, 'id:'); - - done(); - }).catch(done); + ); + assert(Array.isArray(browseStub.firstCall.args)); + assert.equal(browseStub.firstCall.args.length, 1); + const options = browseStub.firstCall.args[0]; + assert(options && typeof options === 'object'); + assert('filter' in options); + assert.equal(options.filter, 'id:'); }); - it('should resolve global props', function (done) { - get.call( + it('should resolve global props', async function () { + await get.call( resource, 'posts', {hash: {filter: 'slug:{{@globalProp.foo}}'}, data: locals, fn: fn, inverse: inverse} - ).then(function () { - assert(Array.isArray(browseStub.firstCall.args)); - assert.equal(browseStub.firstCall.args.length, 1); - const options = browseStub.firstCall.args[0]; - assert(options && typeof options === 'object'); - assert('filter' in options); - assert.equal(options.filter, 'slug:bar'); - - done(); - }).catch(done); + ); + assert(Array.isArray(browseStub.firstCall.args)); + assert.equal(browseStub.firstCall.args.length, 1); + const options = browseStub.firstCall.args[0]; + assert(options && typeof options === 'object'); + assert('filter' in options); + assert.equal(options.filter, 'slug:bar'); }); }); diff --git a/ghost/core/test/unit/frontend/meta/schema.test.js b/ghost/core/test/unit/frontend/meta/schema.test.js index 589394cb653..c4f2eb6aade 100644 --- a/ghost/core/test/unit/frontend/meta/schema.test.js +++ b/ghost/core/test/unit/frontend/meta/schema.test.js @@ -40,7 +40,7 @@ const BASE_METADATA = { }; describe('getSchema', function () { - it('should return post schema if context starts with post', function (done) { + it('should return post schema if context starts with post', function () { const metadata = { site: { title: 'Site Title', @@ -138,10 +138,9 @@ describe('getSchema', function () { }, url: 'http://mysite.com/post/my-post-slug/' }); - done(); }); - it('should return page schema if context starts with page', function (done) { + it('should return page schema if context starts with page', function () { const metadata = { site: { title: 'Site Title', @@ -239,10 +238,9 @@ describe('getSchema', function () { }, url: 'http://mysite.com/post/my-page-slug/' }); - done(); }); - it('should return post schema removing null or undefined values', function (done) { + it('should return post schema removing null or undefined values', function () { const metadata = { site: { title: 'Site Title' @@ -298,10 +296,9 @@ describe('getSchema', function () { }, url: 'http://mysite.com/post/my-post-slug/' }); - done(); }); - it('should return image url instead of ImageObjects if no dimensions supplied', function (done) { + it('should return image url instead of ImageObjects if no dimensions supplied', function () { const metadata = { site: { title: 'Site Title', @@ -383,7 +380,6 @@ describe('getSchema', function () { }, url: 'http://mysite.com/post/my-post-slug/' }); - done(); }); it('should return home schema if context starts with home', function () { diff --git a/ghost/core/test/unit/frontend/meta/structured-data.test.js b/ghost/core/test/unit/frontend/meta/structured-data.test.js index 4af5b523495..4cd1b254239 100644 --- a/ghost/core/test/unit/frontend/meta/structured-data.test.js +++ b/ghost/core/test/unit/frontend/meta/structured-data.test.js @@ -2,7 +2,7 @@ const assert = require('node:assert/strict'); const getStructuredData = require('../../../../core/frontend/meta/structured-data'); describe('getStructuredData', function () { - it('should return structured data from metadata per post', function (done) { + it('should return structured data from metadata per post', function () { const metadata = { site: { title: 'Site Title', @@ -64,10 +64,9 @@ describe('getStructuredData', function () { 'twitter:site': '@testuser', 'twitter:creator': '@twitterpage' }); - done(); }); - it('should return structured data from metadata with provided og and twitter images only per post', function (done) { + it('should return structured data from metadata with provided og and twitter images only per post', function () { const metadata = { site: { title: 'Site Title', @@ -126,10 +125,9 @@ describe('getStructuredData', function () { 'twitter:site': '@testuser', 'twitter:creator': '@twitterpage' }); - done(); }); - it('should return structured data from metadata with no nulls', function (done) { + it('should return structured data from metadata with no nulls', function () { const metadata = { site: { title: 'Site Title', @@ -172,6 +170,5 @@ describe('getStructuredData', function () { 'twitter:title': 'Post Title', 'twitter:url': 'http://mysite.com/post/my-post-slug/' }); - done(); }); }); diff --git a/ghost/core/test/unit/frontend/services/data/fetch-data.test.js b/ghost/core/test/unit/frontend/services/data/fetch-data.test.js index 3e2b59fe242..e07dd0dad9d 100644 --- a/ghost/core/test/unit/frontend/services/data/fetch-data.test.js +++ b/ghost/core/test/unit/frontend/services/data/fetch-data.test.js @@ -57,44 +57,38 @@ describe('Unit - frontend/data/fetch-data', function () { sinon.restore(); }); - it('should handle no options', function (done) { - data.fetchData(null, null, locals).then(function (result) { - assertExists(result); - assert(result && typeof result === 'object'); - assert('posts' in result); - assert('meta' in result); - assert(!('data' in result)); - - sinon.assert.calledOnce(browsePostsStub); - assert(_.isPlainObject(browsePostsStub.firstCall.args[0])); - assert('include' in browsePostsStub.firstCall.args[0]); - assert(!('filter' in browsePostsStub.firstCall.args[0])); - - done(); - }).catch(done); + it('should handle no options', async function () { + const result = await data.fetchData(null, null, locals); + assertExists(result); + assert(result && typeof result === 'object'); + assert('posts' in result); + assert('meta' in result); + assert(!('data' in result)); + + sinon.assert.calledOnce(browsePostsStub); + assert(_.isPlainObject(browsePostsStub.firstCall.args[0])); + assert('include' in browsePostsStub.firstCall.args[0]); + assert(!('filter' in browsePostsStub.firstCall.args[0])); }); - it('should handle path options with page/limit', function (done) { - data.fetchData({page: 2, limit: 10}, null, locals).then(function (result) { - assertExists(result); - assert(result && typeof result === 'object'); - assert('posts' in result); - assert('meta' in result); - assert(!('data' in result)); - - assert.equal(result.posts.length, posts.length); - - sinon.assert.calledOnce(browsePostsStub); - assert(_.isPlainObject(browsePostsStub.firstCall.args[0])); - assert('include' in browsePostsStub.firstCall.args[0]); - assert.equal(browsePostsStub.firstCall.args[0].limit, 10); - assert.equal(browsePostsStub.firstCall.args[0].page, 2); - - done(); - }).catch(done); + it('should handle page and limit options', async function () { + const result = await data.fetchData({page: 2, limit: 10}, null, locals); + assertExists(result); + assert(result && typeof result === 'object'); + assert('posts' in result); + assert('meta' in result); + assert(!('data' in result)); + + assert.equal(result.posts.length, posts.length); + + sinon.assert.calledOnce(browsePostsStub); + assert(_.isPlainObject(browsePostsStub.firstCall.args[0])); + assert('include' in browsePostsStub.firstCall.args[0]); + assert.equal(browsePostsStub.firstCall.args[0].limit, 10); + assert.equal(browsePostsStub.firstCall.args[0].page, 2); }); - it('should handle multiple queries', function (done) { + it('should handle multiple queries', async function () { const pathOptions = {}; const routerOptions = { @@ -110,27 +104,25 @@ describe('Unit - frontend/data/fetch-data', function () { } }; - data.fetchData(pathOptions, routerOptions, locals).then(function (result) { - assertExists(result); - assert(result && typeof result === 'object'); - assert('posts' in result); - assert('meta' in result); - assert('data' in result); - assert(result.data && typeof result.data === 'object'); - assert('featured' in result.data); - - assert.equal(result.posts.length, posts.length); - assert.equal(result.data.featured.length, posts.length); - - sinon.assert.calledTwice(browsePostsStub); - assert.equal(browsePostsStub.firstCall.args[0].include, 'authors,tags,tiers'); - assert.equal(browsePostsStub.secondCall.args[0].filter, 'featured:true'); - assert.equal(browsePostsStub.secondCall.args[0].limit, 3); - done(); - }).catch(done); + const result = await data.fetchData(pathOptions, routerOptions, locals); + assertExists(result); + assert(result && typeof result === 'object'); + assert('posts' in result); + assert('meta' in result); + assert('data' in result); + assert(result.data && typeof result.data === 'object'); + assert('featured' in result.data); + + assert.equal(result.posts.length, posts.length); + assert.equal(result.data.featured.length, posts.length); + + sinon.assert.calledTwice(browsePostsStub); + assert.equal(browsePostsStub.firstCall.args[0].include, 'authors,tags,tiers'); + assert.equal(browsePostsStub.secondCall.args[0].filter, 'featured:true'); + assert.equal(browsePostsStub.secondCall.args[0].limit, 3); }); - it('should handle multiple queries with page param', function (done) { + it('should handle multiple queries with page param', async function () { const pathOptions = { page: 2 }; @@ -145,29 +137,27 @@ describe('Unit - frontend/data/fetch-data', function () { } }; - data.fetchData(pathOptions, routerOptions, locals).then(function (result) { - assertExists(result); - - assert(result && typeof result === 'object'); - assert('posts' in result); - assert('meta' in result); - assert('data' in result); - assert(result.data && typeof result.data === 'object'); - assert('featured' in result.data); - - assert.equal(result.posts.length, posts.length); - assert.equal(result.data.featured.length, posts.length); - - sinon.assert.calledTwice(browsePostsStub); - assert.equal(browsePostsStub.firstCall.args[0].include, 'authors,tags,tiers'); - assert.equal(browsePostsStub.firstCall.args[0].page, 2); - assert.equal(browsePostsStub.secondCall.args[0].filter, 'featured:true'); - assert.equal(browsePostsStub.secondCall.args[0].limit, 3); - done(); - }).catch(done); + const result = await data.fetchData(pathOptions, routerOptions, locals); + assertExists(result); + + assert(result && typeof result === 'object'); + assert('posts' in result); + assert('meta' in result); + assert('data' in result); + assert(result.data && typeof result.data === 'object'); + assert('featured' in result.data); + + assert.equal(result.posts.length, posts.length); + assert.equal(result.data.featured.length, posts.length); + + sinon.assert.calledTwice(browsePostsStub); + assert.equal(browsePostsStub.firstCall.args[0].include, 'authors,tags,tiers'); + assert.equal(browsePostsStub.firstCall.args[0].page, 2); + assert.equal(browsePostsStub.secondCall.args[0].filter, 'featured:true'); + assert.equal(browsePostsStub.secondCall.args[0].limit, 3); }); - it('should handle queries with slug replacements', function (done) { + it('should handle queries with slug replacements', async function () { const pathOptions = { slug: 'testing' }; @@ -184,24 +174,22 @@ describe('Unit - frontend/data/fetch-data', function () { } }; - data.fetchData(pathOptions, routerOptions, locals).then(function (result) { - assertExists(result); - assert(result && typeof result === 'object'); - assert('posts' in result); - assert('meta' in result); - assert('data' in result); - assert(result.data && typeof result.data === 'object'); - assert('tag' in result.data); - - assert.equal(result.posts.length, posts.length); - assert.equal(result.data.tag.length, tags.length); - - sinon.assert.calledOnce(browsePostsStub); - assert('include' in browsePostsStub.firstCall.args[0]); - assert.equal(browsePostsStub.firstCall.args[0].filter, 'tags:testing'); - assert(!('slug' in browsePostsStub.firstCall.args[0])); - assert.equal(readTagsStub.firstCall.args[0].slug, 'testing'); - done(); - }).catch(done); + const result = await data.fetchData(pathOptions, routerOptions, locals); + assertExists(result); + assert(result && typeof result === 'object'); + assert('posts' in result); + assert('meta' in result); + assert('data' in result); + assert(result.data && typeof result.data === 'object'); + assert('tag' in result.data); + + assert.equal(result.posts.length, posts.length); + assert.equal(result.data.tag.length, tags.length); + + sinon.assert.calledOnce(browsePostsStub); + assert('include' in browsePostsStub.firstCall.args[0]); + assert.equal(browsePostsStub.firstCall.args[0].filter, 'tags:testing'); + assert(!('slug' in browsePostsStub.firstCall.args[0])); + assert.equal(readTagsStub.firstCall.args[0].slug, 'testing'); }); }); diff --git a/ghost/core/test/unit/frontend/services/routing/controllers/channel.test.js b/ghost/core/test/unit/frontend/services/routing/controllers/channel.test.js index f1f56dd4bb9..1dc8ce440de 100644 --- a/ghost/core/test/unit/frontend/services/routing/controllers/channel.test.js +++ b/ghost/core/test/unit/frontend/services/routing/controllers/channel.test.js @@ -1,5 +1,4 @@ const assert = require('node:assert/strict'); -const {assertExists} = require('../../../../../utils/assertions'); const errors = require('@tryghost/errors'); const sinon = require('sinon'); const testUtils = require('../../../../../utils'); @@ -9,13 +8,6 @@ const controllers = require('../../../../../../core/frontend/services/routing/co const renderer = require('../../../../../../core/frontend/services/rendering'); const dataService = require('../../../../../../core/frontend/services/data'); -function failTest(done) { - return function (err) { - assertExists(err); - done(err); - }; -} - describe('Unit - services/routing/controllers/channel', function () { let req; let res; @@ -67,7 +59,9 @@ describe('Unit - services/routing/controllers/channel', function () { sinon.restore(); }); - it('no params', function (done) { + it('no params', async function () { + let next = sinon.stub(); + fetchDataStub.withArgs({page: 1, slug: undefined, limit: postsPerPage}, res.routerOptions) .resolves({ posts: posts, @@ -78,15 +72,15 @@ describe('Unit - services/routing/controllers/channel', function () { } }); - controllers.channel(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - done(); - }).catch(done); + await controllers.channel(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.notCalled(next); }); - it('pass page param', function (done) { + it('pass page param', async function () { + let next = sinon.stub(); req.params.page = 2; fetchDataStub.withArgs({page: 2, slug: undefined, limit: postsPerPage}, res.routerOptions) @@ -99,15 +93,15 @@ describe('Unit - services/routing/controllers/channel', function () { } }); - controllers.channel(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - done(); - }).catch(done); + await controllers.channel(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.notCalled(next); }); - it('update hbs engine: router defines limit', function (done) { + it('update hbs engine: router defines limit', async function () { + let next = sinon.stub(); res.routerOptions.limit = 3; req.params.page = 2; @@ -121,16 +115,16 @@ describe('Unit - services/routing/controllers/channel', function () { } }); - controllers.channel(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.calledOnce(themeEngine.getActive().updateTemplateOptions.withArgs({data: {config: {posts_per_page: 3}}})); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - done(); - }).catch(done); + await controllers.channel(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.calledOnce(themeEngine.getActive().updateTemplateOptions.withArgs({data: {config: {posts_per_page: 3}}})); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.notCalled(next); }); - it('page param too big', function (done) { + it('page param too big', async function () { + let next = sinon.stub(); req.params.page = 6; fetchDataStub.withArgs({page: 6, slug: undefined, limit: postsPerPage}, res.routerOptions) @@ -143,18 +137,15 @@ describe('Unit - services/routing/controllers/channel', function () { } }); - controllers.channel(req, res, function (err) { - assert.equal((err instanceof errors.NotFoundError), true); - - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.notCalled(renderStub); - done(); - }); + await controllers.channel(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.calledWith(next, sinon.match.instanceOf(errors.NotFoundError)); }); - it('slug param', function (done) { + it('slug param', async function () { + let next = sinon.stub(); req.params.slug = 'unsafe'; fetchDataStub.withArgs({page: 1, slug: 'safe', limit: postsPerPage}, res.routerOptions) @@ -167,15 +158,15 @@ describe('Unit - services/routing/controllers/channel', function () { } }); - controllers.channel(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.calledOnce(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - done(); - }).catch(done); + await controllers.channel(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.calledOnce(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.notCalled(next); }); - it('invalid posts per page', function (done) { + it('invalid posts per page', async function () { + let next = sinon.stub(); postsPerPage = -1; fetchDataStub.withArgs({page: 1, slug: undefined}, res.routerOptions) @@ -188,11 +179,10 @@ describe('Unit - services/routing/controllers/channel', function () { } }); - controllers.channel(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - done(); - }).catch(done); + await controllers.channel(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.notCalled(next); }); }); diff --git a/ghost/core/test/unit/frontend/services/routing/controllers/collection.test.js b/ghost/core/test/unit/frontend/services/routing/controllers/collection.test.js index b77218edddd..772068f759b 100644 --- a/ghost/core/test/unit/frontend/services/routing/controllers/collection.test.js +++ b/ghost/core/test/unit/frontend/services/routing/controllers/collection.test.js @@ -1,5 +1,4 @@ const assert = require('node:assert/strict'); -const {assertExists} = require('../../../../../utils/assertions'); const errors = require('@tryghost/errors'); const sinon = require('sinon'); const testUtils = require('../../../../../utils'); @@ -10,13 +9,6 @@ const controllers = require('../../../../../../core/frontend/services/routing/co const renderer = require('../../../../../../core/frontend/services/rendering'); const dataService = require('../../../../../../core/frontend/services/data'); -function failTest(done) { - return function (err) { - assertExists(err); - done(err); - }; -} - describe('Unit - services/routing/controllers/collection', function () { let req; let res; @@ -25,6 +17,7 @@ describe('Unit - services/routing/controllers/collection', function () { let posts; let postsPerPage; let ownsStub; + let next; beforeEach(function () { postsPerPage = 5; @@ -35,6 +28,7 @@ describe('Unit - services/routing/controllers/collection', function () { fetchDataStub = sinon.stub(); renderStub = sinon.stub(); + next = sinon.stub(); sinon.stub(dataService, 'fetchData').get(function () { return fetchDataStub; @@ -74,7 +68,7 @@ describe('Unit - services/routing/controllers/collection', function () { sinon.restore(); }); - it('no params', function (done) { + it('no params', async function () { fetchDataStub.withArgs({page: 1, slug: undefined, limit: postsPerPage}, res.routerOptions) .resolves({ posts: posts, @@ -85,16 +79,15 @@ describe('Unit - services/routing/controllers/collection', function () { } }); - controllers.collection(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.calledOnce(ownsStub); - done(); - }).catch(done); + await controllers.collection(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.calledOnce(ownsStub); + sinon.assert.notCalled(next); }); - it('pass page param', function (done) { + it('pass page param', async function () { req.params.page = 2; fetchDataStub.withArgs({page: 2, slug: undefined, limit: postsPerPage}, res.routerOptions) @@ -107,16 +100,15 @@ describe('Unit - services/routing/controllers/collection', function () { } }); - controllers.collection(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.calledOnce(ownsStub); - done(); - }).catch(done); + await controllers.collection(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.calledOnce(ownsStub); + sinon.assert.notCalled(next); }); - it('update hbs engine: router defines limit', function (done) { + it('update hbs engine: router defines limit', async function () { res.routerOptions.limit = 3; req.params.page = 2; @@ -130,17 +122,16 @@ describe('Unit - services/routing/controllers/collection', function () { } }); - controllers.collection(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.calledOnce(themeEngine.getActive().updateTemplateOptions.withArgs({data: {config: {posts_per_page: 3}}})); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.calledOnce(ownsStub); - done(); - }).catch(done); + await controllers.collection(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.calledOnce(themeEngine.getActive().updateTemplateOptions.withArgs({data: {config: {posts_per_page: 3}}})); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.calledOnce(ownsStub); + sinon.assert.notCalled(next); }); - it('page param too big', function (done) { + it('page param too big', async function () { req.params.page = 6; fetchDataStub.withArgs({page: 6, slug: undefined, limit: postsPerPage}, res.routerOptions) @@ -153,19 +144,16 @@ describe('Unit - services/routing/controllers/collection', function () { } }); - controllers.collection(req, res, function (err) { - assert.equal((err instanceof errors.NotFoundError), true); - - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.notCalled(renderStub); - sinon.assert.notCalled(ownsStub); - done(); - }); + await controllers.collection(req, res, next); + sinon.assert.calledWith(next, sinon.match.instanceOf(errors.NotFoundError)); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.notCalled(renderStub); + sinon.assert.notCalled(ownsStub); }); - it('slug param', function (done) { + it('slug param', async function () { req.params.slug = 'unsafe'; fetchDataStub.withArgs({page: 1, slug: 'safe', limit: postsPerPage}, res.routerOptions) @@ -178,16 +166,15 @@ describe('Unit - services/routing/controllers/collection', function () { } }); - controllers.collection(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.calledOnce(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.calledOnce(ownsStub); - done(); - }).catch(done); + await controllers.collection(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.calledOnce(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.calledOnce(ownsStub); + sinon.assert.notCalled(next); }); - it('invalid posts per page', function (done) { + it('invalid posts per page', async function () { postsPerPage = -1; fetchDataStub.withArgs({page: 1, slug: undefined}, res.routerOptions) @@ -200,16 +187,15 @@ describe('Unit - services/routing/controllers/collection', function () { } }); - controllers.collection(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.calledOnce(ownsStub); - done(); - }).catch(done); + await controllers.collection(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.calledOnce(ownsStub); + sinon.assert.notCalled(next); }); - it('should verify if post belongs to collection', function (done) { + it('should verify if post belongs to collection', async function () { posts = [ testUtils.DataGenerator.forKnex.createPost({url: '/a/'}), testUtils.DataGenerator.forKnex.createPost({url: '/b/'}), @@ -238,12 +224,11 @@ describe('Unit - services/routing/controllers/collection', function () { } }); - controllers.collection(req, res, failTest(done)).then(function () { - sinon.assert.calledOnce(themeEngine.getActive); - sinon.assert.notCalled(security.string.safe); - sinon.assert.calledOnce(fetchDataStub); - sinon.assert.callCount(ownsStub, 4); - done(); - }).catch(done); + await controllers.collection(req, res, next); + sinon.assert.calledOnce(themeEngine.getActive); + sinon.assert.notCalled(security.string.safe); + sinon.assert.calledOnce(fetchDataStub); + sinon.assert.callCount(ownsStub, 4); + sinon.assert.notCalled(next); }); }); diff --git a/ghost/core/test/unit/frontend/services/routing/controllers/previews.test.js b/ghost/core/test/unit/frontend/services/routing/controllers/previews.test.js index 517a242bfaa..de8a9242229 100644 --- a/ghost/core/test/unit/frontend/services/routing/controllers/previews.test.js +++ b/ghost/core/test/unit/frontend/services/routing/controllers/previews.test.js @@ -1,4 +1,3 @@ -const {assertExists} = require('../../../../../utils/assertions'); const sinon = require('sinon'); const testUtils = require('../../../../../utils'); const configUtils = require('../../../../../utils/config-utils'); @@ -15,13 +14,6 @@ describe('Unit - services/routing/controllers/previews', function () { let post; let apiResponse; - function failTest(done) { - return function (err) { - assertExists(err); - done(err); - }; - } - afterEach(async function () { sinon.restore(); await configUtils.restore(); @@ -80,10 +72,10 @@ describe('Unit - services/routing/controllers/previews', function () { }); }); - it('should render post', function (done) { - controllers.previews(req, res, failTest(done)).then(function () { - sinon.assert.called(renderStub); - done(); - }).catch(done); + it('should render post', async function () { + const next = sinon.stub(); + await controllers.previews(req, res, next); + sinon.assert.called(renderStub); + sinon.assert.notCalled(next); }); }); diff --git a/ghost/core/test/unit/frontend/services/rss/cache.test.js b/ghost/core/test/unit/frontend/services/rss/cache.test.js index 93b977a2a79..f5793409f0b 100644 --- a/ghost/core/test/unit/frontend/services/rss/cache.test.js +++ b/ghost/core/test/unit/frontend/services/rss/cache.test.js @@ -21,7 +21,7 @@ describe('RSS: Cache', function () { generateFeedReset = rssCache.__set__('generateFeed', generateSpy); }); - it('should not rebuild xml for same data and url', function (done) { + it('should not rebuild xml for same data and url', async function () { const data = { title: 'Test Title', description: 'Testing Desc', @@ -30,27 +30,22 @@ describe('RSS: Cache', function () { }; let xmlData1; - rssCache.getXML('/rss/', data) - .then(function (_xmlData) { - xmlData1 = _xmlData; + const _xmlData = await rssCache.getXML('/rss/', data); - // We should have called generateFeed - sinon.assert.calledOnce(generateSpy); + xmlData1 = _xmlData; - // Call RSS again to check that we didn't rebuild - return rssCache.getXML('/rss/', data); - }) - .then(function (xmlData2) { - // Assertions + // We should have called generateFeed + sinon.assert.calledOnce(generateSpy); - // We should not have called generateFeed again - sinon.assert.calledOnce(generateSpy); + // Call RSS again to check that we didn't rebuild + const xmlData2 = await rssCache.getXML('/rss/', data); - // The data should be identical, no changing lastBuildDate - assert.equal(xmlData1, xmlData2); + // Assertions - done(); - }) - .catch(done); + // We should not have called generateFeed again + sinon.assert.calledOnce(generateSpy); + + // The data should be identical, no changing lastBuildDate + assert.equal(xmlData1, xmlData2); }); }); diff --git a/ghost/core/test/unit/frontend/services/rss/generate-feed.test.js b/ghost/core/test/unit/frontend/services/rss/generate-feed.test.js index 007e5843ec9..7fb8cd000fb 100644 --- a/ghost/core/test/unit/frontend/services/rss/generate-feed.test.js +++ b/ghost/core/test/unit/frontend/services/rss/generate-feed.test.js @@ -60,72 +60,66 @@ describe('RSS: Generate Feed', function () { configUtils.set({url: 'http://my-ghost-blog.com'}); }); - it('should get the RSS tags correct', function (done) { + it('should get the RSS tags correct', async function () { data.posts = []; - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); - - // xml & rss tags - assert.match(xmlData, /^<\?xml version="1.0" encoding="UTF-8"\?>/); - assert.match(xmlData, /<!\[CDATA\[Test Title\]\]><\/title>/); - assert.match(xmlData, /<description><!\[CDATA\[Testing Desc\]\]><\/description>/); - assert.match(xmlData, /<link>http:\/\/my-ghost-blog.com\/<\/link>/); - assert.match(xmlData, /<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/); - assert.match(xmlData, /<generator>Ghost 0.6<\/generator>/); - assert.match(xmlData, /<lastBuildDate>.*?<\/lastBuildDate>/); - assert.match(xmlData, /<atom:link href="http:\/\/my-ghost-blog.com\/rss\/" rel="self"/); - assert.match(xmlData, /type="application\/rss\+xml"\/><ttl>60<\/ttl>/); - assert.match(xmlData, /<\/channel><\/rss>$/); - - done(); - }).catch(done); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); + + // xml & rss tags + assert.match(xmlData, /^<\?xml version="1.0" encoding="UTF-8"\?>/); + assert.match(xmlData, /<rss/); + assert.match(xmlData, /xmlns:dc="http:\/\/purl.org\/dc\/elements\/1.1\/"/); + assert.match(xmlData, /xmlns:content="http:\/\/purl.org\/rss\/1.0\/modules\/content\/"/); + assert.match(xmlData, /xmlns:atom="http:\/\/www.w3.org\/2005\/Atom"/); + assert.match(xmlData, /version="2.0"/); + assert.match(xmlData, /xmlns:media="http:\/\/search.yahoo.com\/mrss\/"/); + + // channel tags + assert.match(xmlData, /<channel><title><!\[CDATA\[Test Title\]\]><\/title>/); + assert.match(xmlData, /<description><!\[CDATA\[Testing Desc\]\]><\/description>/); + assert.match(xmlData, /<link>http:\/\/my-ghost-blog.com\/<\/link>/); + assert.match(xmlData, /<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/); + assert.match(xmlData, /<generator>Ghost 0.6<\/generator>/); + assert.match(xmlData, /<lastBuildDate>.*?<\/lastBuildDate>/); + assert.match(xmlData, /<atom:link href="http:\/\/my-ghost-blog.com\/rss\/" rel="self"/); + assert.match(xmlData, /type="application\/rss\+xml"\/><ttl>60<\/ttl>/); + assert.match(xmlData, /<\/channel><\/rss>$/); }); - it('should get the item tags correct', function (done) { + it('should get the item tags correct', async function () { data.posts = posts; _.each(data.posts, function (post) { routerManagerGetUrlByResourceIdStub.withArgs(post.id, {absolute: true}).returns('http://my-ghost-blog.com/' + post.slug + '/'); }); - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); - - // item tags - assert.match(xmlData, /<item><title><!\[CDATA\[HTML Ipsum\]\]><\/title>/); - assert.match(xmlData, /<description><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1>/); - assert.match(xmlData, /<link>http:\/\/my-ghost-blog.com\/html-ipsum\/<\/link>/); - assert.match(xmlData, /<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/); - assert.match(xmlData, /<guid isPermaLink="false">/); - assert.match(xmlData, /<\/guid><dc:creator><!\[CDATA\[Joe Bloggs\]\]><\/dc:creator>/); - assert.match(xmlData, /<pubDate>Thu, 01 Jan 2015/); - assert.match(xmlData, /<content:encoded><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1>/); - assert.match(xmlData, /<\/code><\/pre>\]\]><\/content:encoded><\/item>/); - assert.doesNotMatch(xmlData, /<author>/); - - // basic structure check - const postEnd = '<\/code><\/pre>\]\]><\/content:encoded>'; - const firstIndex = xmlData.indexOf(postEnd); - - // The first title should be before the first content - assert(xmlData.indexOf('HTML Ipsum') < firstIndex); - // The second title should be after the first content - assert(xmlData.indexOf('Ghostly Kitchen Sink') > firstIndex); - - done(); - }).catch(done); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); + + // item tags + assert.match(xmlData, /<item><title><!\[CDATA\[HTML Ipsum\]\]><\/title>/); + assert.match(xmlData, /<description><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1>/); + assert.match(xmlData, /<link>http:\/\/my-ghost-blog.com\/html-ipsum\/<\/link>/); + assert.match(xmlData, /<image><url>http:\/\/my-ghost-blog.com\/favicon.png<\/url><title>Test Title<\/title><link>http:\/\/my-ghost-blog.com\/<\/link><\/image>/); + assert.match(xmlData, /<guid isPermaLink="false">/); + assert.match(xmlData, /<\/guid><dc:creator><!\[CDATA\[Joe Bloggs\]\]><\/dc:creator>/); + assert.match(xmlData, /<pubDate>Thu, 01 Jan 2015/); + assert.match(xmlData, /<content:encoded><!\[CDATA\[<h1>HTML Ipsum Presents<\/h1>/); + assert.match(xmlData, /<\/code><\/pre>\]\]><\/content:encoded><\/item>/); + assert.doesNotMatch(xmlData, /<author>/); + + // basic structure check + const postEnd = '<\/code><\/pre>\]\]><\/content:encoded>'; + const firstIndex = xmlData.indexOf(postEnd); + + // The first title should be before the first content + assert(xmlData.indexOf('HTML Ipsum') < firstIndex); + // The second title should be after the first content + assert(xmlData.indexOf('Ghostly Kitchen Sink') > firstIndex); }); - it('should only return visible tags', function (done) { + it('should only return visible tags', async function () { const postWithTags = posts[2]; postWithTags.tags = [ {name: 'public', visibility: 'public'}, @@ -135,111 +129,94 @@ describe('RSS: Generate Feed', function () { data.posts = [postWithTags]; - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); - // item tags - assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); - assert.match(xmlData, /<description><!\[CDATA\[test stuff/); - assert.match(xmlData, /<content:encoded><!\[CDATA\[<!--kg-card-begin: markdown--><h2 id="testing">testing<\/h2>\n/); - assert.match(xmlData, /<img src="http:\/\/placekitten.com\/500\/200"/); - assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); - assert.match(xmlData, /<category><!\[CDATA\[public\]\]/); - assert.match(xmlData, /<category><!\[CDATA\[visibility\]\]/); - assert.doesNotMatch(xmlData, /<category><!\[CDATA\[internal\]\]/); - done(); - }).catch(done); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); + // item tags + assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); + assert.match(xmlData, /<description><!\[CDATA\[test stuff/); + assert.match(xmlData, /<content:encoded><!\[CDATA\[<!--kg-card-begin: markdown--><h2 id="testing">testing<\/h2>\n/); + assert.match(xmlData, /<img src="http:\/\/placekitten.com\/500\/200"/); + assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); + assert.match(xmlData, /<category><!\[CDATA\[public\]\]/); + assert.match(xmlData, /<category><!\[CDATA\[visibility\]\]/); + assert.doesNotMatch(xmlData, /<category><!\[CDATA\[internal\]\]/); }); - it('should not error if author is somehow not present', function (done) { + it('should not error if author is somehow not present', async function () { data.posts = [_.omit(posts[2], 'primary_author')]; - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); - // special/optional tags - assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); - assert.match(xmlData, /<description><!\[CDATA\[test stuff/); - assert.match(xmlData, /<content:encoded><!\[CDATA\[<!--kg-card-begin: markdown--><h2 id="testing">testing<\/h2>\n/); - assert.match(xmlData, /<img src="http:\/\/placekitten.com\/500\/200"/); - assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); - assert.doesNotMatch(xmlData, /<dc:creator>/); - - done(); - }).catch(done); + // special/optional tags + assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); + assert.match(xmlData, /<description><!\[CDATA\[test stuff/); + assert.match(xmlData, /<content:encoded><!\[CDATA\[<!--kg-card-begin: markdown--><h2 id="testing">testing<\/h2>\n/); + assert.match(xmlData, /<img src="http:\/\/placekitten.com\/500\/200"/); + assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); + assert.doesNotMatch(xmlData, /<dc:creator>/); }); - it('should not error if post content is null', function (done) { + it('should not error if post content is null', async function () { data.posts = [Object.assign({}, posts[2], {html: null})]; - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); - - // special/optional tags - assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); - assert.match(xmlData, /<description><!\[CDATA\[test stuff/); - assert.match(xmlData, /<content:encoded\/>/); - assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); - assert.match(xmlData, /<dc:creator>/); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); - done(); - }).catch(done); + // special/optional tags + assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); + assert.match(xmlData, /<description><!\[CDATA\[test stuff/); + assert.match(xmlData, /<content:encoded\/>/); + assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); + assert.match(xmlData, /<dc:creator>/); }); - it('should use meta_description and image where available', function (done) { + it('should use meta_description and image where available', async function () { data.posts = [posts[2]]; - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); - - // special/optional tags - assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); - assert.match(xmlData, /<description><!\[CDATA\[test stuff/); - assert.match(xmlData, /<content:encoded><!\[CDATA\[<!--kg-card-begin: markdown--><h2 id="testing">testing<\/h2>\n/); - assert.match(xmlData, /<img src="http:\/\/placekitten.com\/500\/200"/); - assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); - done(); - }).catch(done); + // special/optional tags + assert.match(xmlData, /<title><!\[CDATA\[Short and Sweet\]\]>/); + assert.match(xmlData, /<description><!\[CDATA\[test stuff/); + assert.match(xmlData, /<content:encoded><!\[CDATA\[<!--kg-card-begin: markdown--><h2 id="testing">testing<\/h2>\n/); + assert.match(xmlData, /<img src="http:\/\/placekitten.com\/500\/200"/); + assert.match(xmlData, /<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); }); - it('should use excerpt when no meta_description is set', function (done) { + it('should use excerpt when no meta_description is set', async function () { data.posts = [posts[0]]; _.each(data.posts, function (post) { routerManagerGetUrlByResourceIdStub.withArgs(post.id, {absolute: true}).returns('http://my-ghost-blog.com/' + post.slug + '/'); }); - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); - // special/optional tags - assert.match(xmlData, /<title><!\[CDATA\[HTML Ipsum\]\]>/); - assert.match(xmlData, /<description><!\[CDATA\[This is my custom excerpt!/); - - done(); - }).catch(done); + // special/optional tags + assert.match(xmlData, /<title><!\[CDATA\[HTML Ipsum\]\]>/); + assert.match(xmlData, /<description><!\[CDATA\[This is my custom excerpt!/); }); - it('should process urls correctly', function (done) { + it('should process urls correctly', async function () { data.posts = [posts[3]]; - generateFeed(baseUrl, data).then(function (xmlData) { - assertExists(xmlData); - - // anchor URL - <a href="#nowhere" title="Anchor URL"> - assert.match(xmlData, /<a href="#nowhere" title="Anchor URL">/); + const xmlData = await generateFeed(baseUrl, data); + assertExists(xmlData); - // relative URL - <a href="/about#nowhere" title="Relative URL"> - assert.match(xmlData, /<a href="http:\/\/my-ghost-blog.com\/about#nowhere" title="Relative URL">/); + // anchor URL - <a href="#nowhere" title="Anchor URL"> + assert.match(xmlData, /<a href="#nowhere" title="Anchor URL">/); - // protocol relative URL - <a href="//somewhere.com/link#nowhere" title="Protocol Relative URL"> - assert.match(xmlData, /<a href="\/\/somewhere.com\/link#nowhere" title="Protocol Relative URL">/); + // relative URL - <a href="/about#nowhere" title="Relative URL"> + assert.match(xmlData, /<a href="http:\/\/my-ghost-blog.com\/about#nowhere" title="Relative URL">/); - // absolute URL - <a href="http://somewhere.com/link#nowhere" title="Absolute URL"> - assert.match(xmlData, /<a href="http:\/\/somewhere.com\/link#nowhere" title="Absolute URL">/); + // protocol relative URL - <a href="//somewhere.com\/link#nowhere" title="Protocol Relative URL"> + assert.match(xmlData, /<a href="\/\/somewhere.com\/link#nowhere" title="Protocol Relative URL">/); - done(); - }).catch(done); + // absolute URL - <a href="http:\/\/somewhere.com\/link#nowhere" title="Absolute URL"> + assert.match(xmlData, /<a href="http:\/\/somewhere.com\/link#nowhere" title="Absolute URL">/); }); }); }); diff --git a/ghost/core/test/unit/frontend/services/rss/renderer.test.js b/ghost/core/test/unit/frontend/services/rss/renderer.test.js index 774ba8d97bd..bccc64e53b0 100644 --- a/ghost/core/test/unit/frontend/services/rss/renderer.test.js +++ b/ghost/core/test/unit/frontend/services/rss/renderer.test.js @@ -24,77 +24,64 @@ describe('RSS: Renderer', function () { sinon.restore(); }); - it('calls the cache and attempts to render, even without data', function (done) { + it('calls the cache and attempts to render, even without data', async function () { rssCacheStub.returns(Promise.resolve('dummyxml')); - renderer.render(res, baseUrl).then(function () { - sinon.assert.calledOnce(rssCacheStub); - assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {}]); + await renderer.render(res, baseUrl); + sinon.assert.calledOnce(rssCacheStub); + assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {}]); - sinon.assert.calledOnce(res.set); - sinon.assert.calledWith(res.set, 'Content-Type', 'application/rss+xml; charset=UTF-8'); + sinon.assert.calledOnce(res.set); + sinon.assert.calledWith(res.set, 'Content-Type', 'application/rss+xml; charset=UTF-8'); - sinon.assert.calledOnce(res.send); - sinon.assert.calledWith(res.send, 'dummyxml'); - - done(); - }).catch(done); + sinon.assert.calledOnce(res.send); + sinon.assert.calledWith(res.send, 'dummyxml'); }); - it('correctly merges locals into empty data before rendering', function (done) { + it('correctly merges locals into empty data before rendering', async function () { rssCacheStub.returns(Promise.resolve('dummyxml')); res.locals = {foo: 'bar'}; - renderer.render(res, baseUrl).then(function () { - sinon.assert.calledOnce(rssCacheStub); - assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {foo: 'bar'}]); - - sinon.assert.calledOnce(res.set); - sinon.assert.calledWith(res.set, 'Content-Type', 'application/rss+xml; charset=UTF-8'); + await renderer.render(res, baseUrl); + sinon.assert.calledOnce(rssCacheStub); + assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {foo: 'bar'}]); - sinon.assert.calledOnce(res.send); - sinon.assert.calledWith(res.send, 'dummyxml'); + sinon.assert.calledOnce(res.set); + sinon.assert.calledWith(res.set, 'Content-Type', 'application/rss+xml; charset=UTF-8'); - done(); - }).catch(done); + sinon.assert.calledOnce(res.send); + sinon.assert.calledWith(res.send, 'dummyxml'); }); - it('correctly merges locals into non-empty data before rendering', function (done) { + it('correctly merges locals into non-empty data before rendering', async function () { rssCacheStub.returns(Promise.resolve('dummyxml')); res.locals = {foo: 'bar'}; const data = {foo: 'baz', fizz: 'buzz'}; - renderer.render(res, baseUrl, data).then(function () { - sinon.assert.calledOnce(rssCacheStub); - assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {foo: 'baz', fizz: 'buzz'}]); - - sinon.assert.calledOnce(res.set); - sinon.assert.calledWith(res.set, 'Content-Type', 'application/rss+xml; charset=UTF-8'); + await renderer.render(res, baseUrl, data); + sinon.assert.calledOnce(rssCacheStub); + assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {foo: 'baz', fizz: 'buzz'}]); - sinon.assert.calledOnce(res.send); - sinon.assert.calledWith(res.send, 'dummyxml'); + sinon.assert.calledOnce(res.set); + sinon.assert.calledWith(res.set, 'Content-Type', 'application/rss+xml; charset=UTF-8'); - done(); - }).catch(done); + sinon.assert.calledOnce(res.send); + sinon.assert.calledWith(res.send, 'dummyxml'); }); - it('does nothing if it gets an error', function (done) { + it('does nothing if it gets an error', async function () { rssCacheStub.returns(Promise.reject(new Error('Fake Error'))); - renderer.render(res, baseUrl).then(function () { - done('This should have errored'); - }).catch(function (err) { - assert.equal(err.message, 'Fake Error'); - - sinon.assert.calledOnce(rssCacheStub); - assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {}]); + await assert.rejects(() => renderer.render(res, baseUrl), { + message: 'Fake Error' + }); - sinon.assert.notCalled(res.set); - sinon.assert.notCalled(res.send); + sinon.assert.calledOnce(rssCacheStub); + assert.deepEqual(rssCacheStub.firstCall.args, ['/rss/', {}]); - done(); - }); + sinon.assert.notCalled(res.set); + sinon.assert.notCalled(res.send); }); }); diff --git a/ghost/core/test/unit/frontend/web/routers/serve-favicon.test.js b/ghost/core/test/unit/frontend/web/routers/serve-favicon.test.js index d28bcc2d218..d2a84e485ce 100644 --- a/ghost/core/test/unit/frontend/web/routers/serve-favicon.test.js +++ b/ghost/core/test/unit/frontend/web/routers/serve-favicon.test.js @@ -33,61 +33,56 @@ describe('Serve Favicon', function () { describe('serveFavicon', function () { describe('serves', function () { - it('default favicon.ico', function (done) { + it('default favicon.ico', async function () { localSettingsCache.icon = ''; - request(blogApp) + await request(blogApp) .get('/favicon.ico') .expect(200) .expect('Content-Type', /image\/x-icon/) - .expect('Content-Length', '15406') - .end(done); + .expect('Content-Length', '15406'); }); }); describe('redirects', function () { - it('custom uploaded favicon.png', function (done) { + it('custom uploaded favicon.png', async function () { storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/'); localSettingsCache.icon = '/content/images/favicon.png'; - request(blogApp) + await request(blogApp) .get('/favicon.png') .expect(302) - .expect('Location', '/content/images/size/w256h256/favicon.png') - .end(done); + .expect('Location', '/content/images/size/w256h256/favicon.png'); }); - it('custom uploaded favicon.webp', function (done) { + it('custom uploaded favicon.webp', async function () { storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/'); localSettingsCache.icon = '/content/images/favicon.webp'; - request(blogApp) + await request(blogApp) .get('/favicon.png') .expect(302) - .expect('Location', '/content/images/size/w256h256/format/png/favicon.webp') - .end(done); + .expect('Location', '/content/images/size/w256h256/format/png/favicon.webp'); }); - it('custom uploaded favicon.ico', function (done) { + it('custom uploaded favicon.ico', async function () { storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/'); localSettingsCache.icon = '/content/images/favicon.ico'; - request(blogApp) + await request(blogApp) .get('/favicon.ico') .expect(302) - .expect('Location', '/content/images/favicon.ico') - .end(done); + .expect('Location', '/content/images/favicon.ico'); }); - it('to favicon.ico when favicon.png is requested', function (done) { + it('to favicon.ico when favicon.png is requested', async function () { configUtils.set('paths:publicFilePath', path.join(__dirname, '../../../../test/utils/fixtures/')); localSettingsCache.icon = null; - request(blogApp) + await request(blogApp) .get('/favicon.png') .expect(302) - .expect('Location', '/favicon.ico') - .end(done); + .expect('Location', '/favicon.ico'); }); }); }); diff --git a/ghost/core/test/unit/server/data/db/backup.test.js b/ghost/core/test/unit/server/data/db/backup.test.js index c40ffe2d435..90157ccf083 100644 --- a/ghost/core/test/unit/server/data/db/backup.test.js +++ b/ghost/core/test/unit/server/data/db/backup.test.js @@ -24,25 +24,21 @@ describe('Backup', function () { fsStub = sinon.stub(fs, 'writeFile').resolves(); }); - it('should create a backup JSON file', function (done) { - dbBackup.backup().then(function () { - sinon.assert.calledOnce(exportStub); - sinon.assert.calledOnce(filenameStub); - sinon.assert.calledOnce(fsStub); - - done(); - }).catch(done); + it('should create a backup JSON file', async function () { + await dbBackup.backup(); + + sinon.assert.calledOnce(exportStub); + sinon.assert.calledOnce(filenameStub); + sinon.assert.calledOnce(fsStub); }); - it('should not create a backup JSON file if disabled', function (done) { + it('should not create a backup JSON file if disabled', async function () { configUtils.set('disableJSBackups', true); - dbBackup.backup().then(function () { - sinon.assert.notCalled(exportStub); - sinon.assert.notCalled(filenameStub); - sinon.assert.notCalled(fsStub); + await dbBackup.backup(); - done(); - }).catch(done); + sinon.assert.notCalled(exportStub); + sinon.assert.notCalled(filenameStub); + sinon.assert.notCalled(fsStub); }); }); diff --git a/ghost/core/test/unit/server/data/exporter/index.test.js b/ghost/core/test/unit/server/data/exporter/index.test.js index 17d5fabf39c..34909f0f224 100644 --- a/ghost/core/test/unit/server/data/exporter/index.test.js +++ b/ghost/core/test/unit/server/data/exporter/index.test.js @@ -38,116 +38,105 @@ describe('Exporter', function () { }); }); - it('should try to export all the correct tables (without excluded)', function (done) { - exporter.doExport().then(function (exportData) { - // NOTE: 15 default tables - const expectedCallCount = exporter.TABLES_ALLOWLIST.length; - - assertExists(exportData); - - assert.match(exportData.meta.version, /\d+.\d+.\d+/gi); - - sinon.assert.calledOnce(tablesStub); - sinon.assert.called(db.knex); - - sinon.assert.callCount(knexMock, expectedCallCount); - sinon.assert.callCount(queryMock.select, expectedCallCount); - - const expectedTables = new Set([ - 'posts', - 'posts_authors', - 'posts_meta', - 'posts_tags', - 'roles', - 'roles_users', - 'settings', - 'custom_theme_settings', - 'tags', - 'users', - 'products', - 'stripe_products', - 'stripe_prices', - 'posts_products', - 'newsletters', - 'benefits', - 'products_benefits', - 'offers', - 'offer_redemptions', - 'snippets' - ]); - const actualTables = new Set(knexMock.getCalls().map(call => call.args[0])); - assert.deepEqual(actualTables, expectedTables); - - done(); - }).catch(done); + it('should try to export all the correct tables (without excluded)', async function () { + const exportData = await exporter.doExport(); + // NOTE: 15 default tables + const expectedCallCount = exporter.TABLES_ALLOWLIST.length; + + assertExists(exportData); + + assert.match(exportData.meta.version, /\d+.\d+.\d+/gi); + + sinon.assert.calledOnce(tablesStub); + sinon.assert.called(db.knex); + + sinon.assert.callCount(knexMock, expectedCallCount); + sinon.assert.callCount(queryMock.select, expectedCallCount); + + const expectedTables = new Set([ + 'posts', + 'posts_authors', + 'posts_meta', + 'posts_tags', + 'roles', + 'roles_users', + 'settings', + 'custom_theme_settings', + 'tags', + 'users', + 'products', + 'stripe_products', + 'stripe_prices', + 'posts_products', + 'newsletters', + 'benefits', + 'products_benefits', + 'offers', + 'offer_redemptions', + 'snippets' + ]); + const actualTables = new Set(knexMock.getCalls().map(call => call.args[0])); + assert.deepEqual(actualTables, expectedTables); }); - it('should try to export all the correct tables with extra tables', function (done) { + it('should try to export all the correct tables with extra tables', async function () { const include = ['mobiledoc_revisions', 'email_recipients']; - exporter.doExport({include}).then(function (exportData) { - // NOTE: 15 default tables + 2 includes - const expectedCallCount = exporter.TABLES_ALLOWLIST.length + 2; - - assertExists(exportData); - - assert.match(exportData.meta.version, /\d+.\d+.\d+/gi); - - sinon.assert.calledOnce(tablesStub); - sinon.assert.called(db.knex); - sinon.assert.called(queryMock.select); - - sinon.assert.callCount(knexMock, expectedCallCount); - sinon.assert.callCount(queryMock.select, expectedCallCount); - - const expectedTables = new Set([ - 'posts', - 'posts_authors', - 'posts_meta', - 'posts_tags', - 'roles', - 'roles_users', - 'settings', - 'custom_theme_settings', - 'tags', - 'users', - 'products', - 'stripe_products', - 'stripe_prices', - 'posts_products', - 'newsletters', - 'benefits', - 'products_benefits', - 'offers', - 'offer_redemptions', - 'snippets', - ...include - ]); - const actualTables = new Set(knexMock.getCalls().map(call => call.args[0])); - assert.deepEqual(actualTables, expectedTables); - - done(); - }).catch(done); + const exportData = await exporter.doExport({include}); + // NOTE: 15 default tables + 2 includes + const expectedCallCount = exporter.TABLES_ALLOWLIST.length + 2; + + assertExists(exportData); + + assert.match(exportData.meta.version, /\d+.\d+.\d+/gi); + + sinon.assert.calledOnce(tablesStub); + sinon.assert.called(db.knex); + sinon.assert.called(queryMock.select); + + sinon.assert.callCount(knexMock, expectedCallCount); + sinon.assert.callCount(queryMock.select, expectedCallCount); + + const expectedTables = new Set([ + 'posts', + 'posts_authors', + 'posts_meta', + 'posts_tags', + 'roles', + 'roles_users', + 'settings', + 'custom_theme_settings', + 'tags', + 'users', + 'products', + 'stripe_products', + 'stripe_prices', + 'posts_products', + 'newsletters', + 'benefits', + 'products_benefits', + 'offers', + 'offer_redemptions', + 'snippets', + ...include + ]); + const actualTables = new Set(knexMock.getCalls().map(call => call.args[0])); + assert.deepEqual(actualTables, expectedTables); }); - it('should catch and log any errors', function (done) { + it('should catch and log any errors', async function () { // Setup for failure queryMock.select.returns(Promise.reject({})); // Execute - exporter.doExport() - .then(function () { - done(new Error('expected error for export')); - }) - .catch(function (err) { - assert.equal((err instanceof errors.DataExportError), true); - done(); - }); + await assert.rejects(async () => { + await exporter.doExport(); + }, errors.DataExportError); }); }); describe('exportFileName', function () { - it('should return a correctly structured filename', function (done) { + it('should return a correctly structured filename', async function () { const settingsStub = sinon.stub(models.Settings, 'findOne').returns( Promise.resolve({ get: function () { @@ -156,43 +145,34 @@ describe('Exporter', function () { }) ); - exporter.fileName().then(function (result) { - assertExists(result); - sinon.assert.calledOnce(settingsStub); - assert.match(result, /^testblog\.ghost\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}\.json$/); - - done(); - }).catch(done); + const result = await exporter.fileName(); + assertExists(result); + sinon.assert.calledOnce(settingsStub); + assert.match(result, /^testblog\.ghost\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}\.json$/); }); - it('should return a correctly structured filename if settings is empty', function (done) { + it('should return a correctly structured filename if settings is empty', async function () { const settingsStub = sinon.stub(models.Settings, 'findOne').returns( Promise.resolve() ); - exporter.fileName().then(function (result) { - assertExists(result); - sinon.assert.calledOnce(settingsStub); - assert.match(result, /^ghost\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}\.json$/); - - done(); - }).catch(done); + const result = await exporter.fileName(); + assertExists(result); + sinon.assert.calledOnce(settingsStub); + assert.match(result, /^ghost\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}\.json$/); }); - it('should return a correctly structured filename if settings errors', function (done) { + it('should return a correctly structured filename if settings errors', async function () { const settingsStub = sinon.stub(models.Settings, 'findOne').returns( Promise.reject() ); const loggingStub = sinon.stub(logging, 'error'); - exporter.fileName().then(function (result) { - assertExists(result); - sinon.assert.calledOnce(settingsStub); - sinon.assert.calledOnce(loggingStub); - assert.match(result, /^ghost\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}\.json$/); - - done(); - }).catch(done); + const result = await exporter.fileName(); + assertExists(result); + sinon.assert.calledOnce(settingsStub); + sinon.assert.calledOnce(loggingStub); + assert.match(result, /^ghost\.[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}\.json$/); }); }); diff --git a/ghost/core/test/unit/server/data/importer/handlers/image.test.js b/ghost/core/test/unit/server/data/importer/handlers/image.test.js index 892a2dc73d5..be1cb298fb3 100644 --- a/ghost/core/test/unit/server/data/importer/handlers/image.test.js +++ b/ghost/core/test/unit/server/data/importer/handlers/image.test.js @@ -38,7 +38,7 @@ describe('ImageHandler', function () { assert.equal(typeof ImageHandler.loadFile, 'function'); }); - it('can load a single file', function (done) { + it('can load a single file', async function () { const filename = 'test-image.jpeg'; const file = [{ @@ -49,18 +49,15 @@ describe('ImageHandler', function () { const storeSpy = sinon.spy(store, 'getUniqueFileName'); const storageSpy = sinon.spy(storage, 'getStorage'); - ImageHandler.loadFile(_.clone(file)).then(function () { - sinon.assert.calledOnce(storageSpy); - sinon.assert.calledOnce(storeSpy); - assert.equal(storeSpy.firstCall.args[0].originalPath, 'test-image.jpeg'); - assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); - assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/test-image.jpeg'); - - done(); - }).catch(done); + await ImageHandler.loadFile(_.clone(file)); + sinon.assert.calledOnce(storageSpy); + sinon.assert.calledOnce(storeSpy); + assert.equal(storeSpy.firstCall.args[0].originalPath, 'test-image.jpeg'); + assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); + assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/test-image.jpeg'); }); - it('can load a single file, maintaining structure', function (done) { + it('can load a single file, maintaining structure', async function () { const filename = 'photos/my-cat.jpeg'; const file = [{ @@ -71,18 +68,15 @@ describe('ImageHandler', function () { const storeSpy = sinon.spy(store, 'getUniqueFileName'); const storageSpy = sinon.spy(storage, 'getStorage'); - ImageHandler.loadFile(_.clone(file)).then(function () { - sinon.assert.calledOnce(storageSpy); - sinon.assert.calledOnce(storeSpy); - assert.equal(storeSpy.firstCall.args[0].originalPath, 'photos/my-cat.jpeg'); - assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images(\/|\\)photos$/); - assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/photos/my-cat.jpeg'); - - done(); - }).catch(done); + await ImageHandler.loadFile(_.clone(file)); + sinon.assert.calledOnce(storageSpy); + sinon.assert.calledOnce(storeSpy); + assert.equal(storeSpy.firstCall.args[0].originalPath, 'photos/my-cat.jpeg'); + assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images(\/|\\)photos$/); + assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/photos/my-cat.jpeg'); }); - it('can load a single file, removing ghost dirs', function (done) { + it('can load a single file, removing ghost dirs', async function () { const filename = 'content/images/my-cat.jpeg'; const file = [{ @@ -93,18 +87,15 @@ describe('ImageHandler', function () { const storeSpy = sinon.spy(store, 'getUniqueFileName'); const storageSpy = sinon.spy(storage, 'getStorage'); - ImageHandler.loadFile(_.clone(file)).then(function () { - sinon.assert.calledOnce(storageSpy); - sinon.assert.calledOnce(storeSpy); - assert.equal(storeSpy.firstCall.args[0].originalPath, 'content/images/my-cat.jpeg'); - assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); - assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/my-cat.jpeg'); - - done(); - }).catch(done); + await ImageHandler.loadFile(_.clone(file)); + sinon.assert.calledOnce(storageSpy); + sinon.assert.calledOnce(storeSpy); + assert.equal(storeSpy.firstCall.args[0].originalPath, 'content/images/my-cat.jpeg'); + assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); + assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/my-cat.jpeg'); }); - it('can load a file (subdirectory)', function (done) { + it('can load a file (subdirectory)', async function () { configUtils.set({url: 'http://localhost:65535/subdir'}); const filename = 'test-image.jpeg'; @@ -117,18 +108,15 @@ describe('ImageHandler', function () { const storeSpy = sinon.spy(store, 'getUniqueFileName'); const storageSpy = sinon.spy(storage, 'getStorage'); - ImageHandler.loadFile(_.clone(file)).then(function () { - sinon.assert.calledOnce(storageSpy); - sinon.assert.calledOnce(storeSpy); - assert.equal(storeSpy.firstCall.args[0].originalPath, 'test-image.jpeg'); - assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); - assert.equal(storeSpy.firstCall.args[0].newPath, '/subdir/content/images/test-image.jpeg'); - - done(); - }).catch(done); + await ImageHandler.loadFile(_.clone(file)); + sinon.assert.calledOnce(storageSpy); + sinon.assert.calledOnce(storeSpy); + assert.equal(storeSpy.firstCall.args[0].originalPath, 'test-image.jpeg'); + assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); + assert.equal(storeSpy.firstCall.args[0].newPath, '/subdir/content/images/test-image.jpeg'); }); - it('can load multiple files', function (done) { + it('can load multiple files', async function () { const files = [{ path: '/my/test/testing.png', name: 'testing.png' @@ -149,23 +137,20 @@ describe('ImageHandler', function () { const storeSpy = sinon.spy(store, 'getUniqueFileName'); const storageSpy = sinon.spy(storage, 'getStorage'); - ImageHandler.loadFile(_.clone(files)).then(function () { - sinon.assert.calledOnce(storageSpy); - sinon.assert.callCount(storeSpy, 4); - assert.equal(storeSpy.firstCall.args[0].originalPath, 'testing.png'); - assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); - assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/testing.png'); - assert.equal(storeSpy.secondCall.args[0].originalPath, 'photo/kitten.jpg'); - assert.match(storeSpy.secondCall.args[0].targetDir, /(\/|\\)content(\/|\\)images(\/|\\)photo$/); - assert.equal(storeSpy.secondCall.args[0].newPath, '/content/images/photo/kitten.jpg'); - assert.equal(storeSpy.thirdCall.args[0].originalPath, 'content/images/animated/bunny.gif'); - assert.match(storeSpy.thirdCall.args[0].targetDir, /(\/|\\)content(\/|\\)images(\/|\\)animated$/); - assert.equal(storeSpy.thirdCall.args[0].newPath, '/content/images/animated/bunny.gif'); - assert.equal(storeSpy.lastCall.args[0].originalPath, 'images/puppy.jpg'); - assert.match(storeSpy.lastCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); - assert.equal(storeSpy.lastCall.args[0].newPath, '/content/images/puppy.jpg'); - - done(); - }).catch(done); + await ImageHandler.loadFile(_.clone(files)); + sinon.assert.calledOnce(storageSpy); + sinon.assert.callCount(storeSpy, 4); + assert.equal(storeSpy.firstCall.args[0].originalPath, 'testing.png'); + assert.match(storeSpy.firstCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); + assert.equal(storeSpy.firstCall.args[0].newPath, '/content/images/testing.png'); + assert.equal(storeSpy.secondCall.args[0].originalPath, 'photo/kitten.jpg'); + assert.match(storeSpy.secondCall.args[0].targetDir, /(\/|\\)content(\/|\\)images(\/|\\)photo$/); + assert.equal(storeSpy.secondCall.args[0].newPath, '/content/images/photo/kitten.jpg'); + assert.equal(storeSpy.thirdCall.args[0].originalPath, 'content/images/animated/bunny.gif'); + assert.match(storeSpy.thirdCall.args[0].targetDir, /(\/|\\)content(\/|\\)images(\/|\\)animated$/); + assert.equal(storeSpy.thirdCall.args[0].newPath, '/content/images/animated/bunny.gif'); + assert.equal(storeSpy.lastCall.args[0].originalPath, 'images/puppy.jpg'); + assert.match(storeSpy.lastCall.args[0].targetDir, /(\/|\\)content(\/|\\)images$/); + assert.equal(storeSpy.lastCall.args[0].newPath, '/content/images/puppy.jpg'); }); }); diff --git a/ghost/core/test/unit/server/data/importer/index.test.js b/ghost/core/test/unit/server/data/importer/index.test.js index a59e72e878b..76c444946e9 100644 --- a/ghost/core/test/unit/server/data/importer/index.test.js +++ b/ghost/core/test/unit/server/data/importer/index.test.js @@ -155,32 +155,28 @@ describe('Importer', function () { // Step 1 of importing is loadFile describe('loadFile', function () { - it('knows when to process a file', function (done) { + it('knows when to process a file', async function () { const testFile = {name: 'myFile.json', path: '/my/path/myFile.json'}; const zipSpy = sinon.stub(ImportManager, 'processZip').returns(Promise.resolve({})); const fileSpy = sinon.stub(ImportManager, 'processFile').returns(Promise.resolve({})); - ImportManager.loadFile(testFile).then(function () { - sinon.assert.notCalled(zipSpy); - sinon.assert.calledOnce(fileSpy); - done(); - }).catch(done); + await ImportManager.loadFile(testFile); + sinon.assert.notCalled(zipSpy); + sinon.assert.calledOnce(fileSpy); }); // We need to make sure we don't actually extract a zip and leave temporary files everywhere! - it('knows when to process a zip', function (done) { + it('knows when to process a zip', async function () { const testZip = {name: 'myFile.zip', path: '/my/path/myFile.zip'}; const zipSpy = sinon.stub(ImportManager, 'processZip').resolves({}); const fileSpy = sinon.stub(ImportManager, 'processFile').resolves({}); - ImportManager.loadFile(testZip).then(function () { - sinon.assert.calledOnce(zipSpy); - sinon.assert.notCalled(fileSpy); - done(); - }).catch(done); + await ImportManager.loadFile(testZip); + sinon.assert.calledOnce(zipSpy); + sinon.assert.notCalled(fileSpy); }); - it('has same result for zips and files', function (done) { + it('has same result for zips and files', async function () { const testFile = {name: 'myFile.json', path: '/my/path/myFile.json'}; const testZip = {name: 'myFile.zip', path: '/my/path/myFile.zip'}; @@ -199,26 +195,24 @@ describe('Importer', function () { getFileSpy.withArgs(JSONHandler, sinon.match.string).returns([{path: '/tmp/dir/myFile.json', name: 'myFile.json'}]); getFileSpy.withArgs(RevueHandler, sinon.match.string).returns([{path: '/tmp/dir/myFile.json', name: 'myFile.json'}]); - ImportManager.processZip(testZip).then(function (zipResult) { - sinon.assert.calledOnce(extractSpy); - sinon.assert.calledOnce(validSpy); - sinon.assert.calledOnce(baseDirSpy); - sinon.assert.callCount(getFileSpy, 6); - sinon.assert.calledOnce(jsonSpy); - sinon.assert.notCalled(imageSpy); - sinon.assert.notCalled(mdSpy); - sinon.assert.called(revueSpy); - - ImportManager.processFile(testFile, '.json').then(function (fileResult) { - sinon.assert.calledTwice(jsonSpy); - - // They should both have data keys, and they should be equivalent - assert('data' in zipResult); - assert('data' in fileResult); - assert.deepEqual(zipResult, fileResult); - done(); - }); - }).catch(done); + const zipResult = await ImportManager.processZip(testZip); + + sinon.assert.calledOnce(extractSpy); + sinon.assert.calledOnce(validSpy); + sinon.assert.calledOnce(baseDirSpy); + sinon.assert.callCount(getFileSpy, 6); + sinon.assert.calledOnce(jsonSpy); + sinon.assert.notCalled(imageSpy); + sinon.assert.notCalled(mdSpy); + sinon.assert.called(revueSpy); + + const fileResult = await ImportManager.processFile(testFile, '.json'); + sinon.assert.calledTwice(jsonSpy); + + // They should both have data keys, and they should be equivalent + assert('data' in zipResult); + assert('data' in fileResult); + assert.deepEqual(zipResult, fileResult); }); describe('Validate Zip', function () { @@ -383,17 +377,16 @@ describe('Importer', function () { }); describe('Zip behavior', function () { - it('can call extract and error correctly', function () { - return ImportManager - // Deliberately pass something that can't be extracted just to check this method signature is working - .extractZip('test/utils/fixtures/import/zips/zip-with-base-dir') - .then(() => { - throw new Error('should have failed'); - }) - .catch((err) => { + it('can call extract and error correctly', async function () { + // Deliberately pass something that can't be extracted just to check this method signature is working + await assert.rejects( + ImportManager.extractZip('test/utils/fixtures/import/zips/zip-with-base-dir'), + (err) => { assert.match(err.message, /EISDIR/); assert.match(err.code, /EISDIR/); - }); + return true; + } + ); }); }); }); @@ -401,7 +394,7 @@ describe('Importer', function () { // Step 2 of importing is preProcess describe('preProcess', function () { // preProcess can modify the data prior to importing - it('calls the DataImporter preProcess method', function (done) { + it('calls the DataImporter preProcess method', async function () { const input = { data: {}, images: [], @@ -416,22 +409,20 @@ describe('Importer', function () { const imageSpy = sinon.spy(ImportManager.importers[0], 'preProcess'); const revueSpy = sinon.spy(RevueImporter, 'preProcess'); - ImportManager.preProcess(inputCopy).then(function (output) { - sinon.assert.calledOnce(revueSpy); - sinon.assert.calledWith(revueSpy, inputCopy); - sinon.assert.calledOnce(dataSpy); - sinon.assert.calledWith(dataSpy, inputCopy); - sinon.assert.calledOnce(imageSpy); - sinon.assert.calledWith(imageSpy, inputCopy); - // eql checks for equality - // equal checks the references are for the same object - assert.notEqual(output, input); - assert.equal(output.preProcessedByData, true); - assert.equal(output.preProcessedByImage, true); - assert.equal(output.preProcessedByMedia, true); - assert.equal(output.preProcessedByFiles, true); - done(); - }).catch(done); + const output = await ImportManager.preProcess(inputCopy); + sinon.assert.calledOnce(revueSpy); + sinon.assert.calledWith(revueSpy, inputCopy); + sinon.assert.calledOnce(dataSpy); + sinon.assert.calledWith(dataSpy, inputCopy); + sinon.assert.calledOnce(imageSpy); + sinon.assert.calledWith(imageSpy, inputCopy); + // eql checks for equality + // equal checks the references are for the same object + assert.notEqual(output, input); + assert.equal(output.preProcessedByData, true); + assert.equal(output.preProcessedByImage, true); + assert.equal(output.preProcessedByMedia, true); + assert.equal(output.preProcessedByFiles, true); }); }); @@ -439,7 +430,7 @@ describe('Importer', function () { describe('doImport', function () { // doImport calls the real importers and has an effect on the DB. We don't want any of those calls to be made, // but to test that the right calls would be made - it('calls the DataImporter doImport method with the data object', function (done) { + it('calls the DataImporter doImport method with the data object', async function () { const input = {data: {posts: []}, images: []}; // pass a copy so that input doesn't get modified @@ -458,18 +449,16 @@ describe('Importer', function () { const expectedImages = input.images; - ImportManager.doImport(inputCopy).then(function (output) { - // eql checks for equality - // equal checks the references are for the same object - sinon.assert.calledOnce(dataSpy); - sinon.assert.calledOnce(imageSpy); - assert.deepEqual(dataSpy.getCall(0).args[0], expectedData); - assert.deepEqual(imageSpy.getCall(0).args[0], expectedImages); - - // we stubbed this as a noop but ImportManager calls with sequence, so we should get an array - assert.deepEqual(output, {images: expectedImages, data: expectedData}); - done(); - }).catch(done); + const output = await ImportManager.doImport(inputCopy); + // eql checks for equality + // equal checks the references are for the same object + sinon.assert.calledOnce(dataSpy); + sinon.assert.calledOnce(imageSpy); + assert.deepEqual(dataSpy.getCall(0).args[0], expectedData); + assert.deepEqual(imageSpy.getCall(0).args[0], expectedImages); + + // we stubbed this as a noop but ImportManager calls with sequence, so we should get an array + assert.deepEqual(output, {images: expectedImages, data: expectedData}); }); }); @@ -477,33 +466,28 @@ describe('Importer', function () { describe('generateReport', function () { // generateReport is intended to create a message to show to the user about what has been imported // it is currently a noop - it('is currently a noop', function (done) { + it('is currently a noop', async function () { const input = [{data: {}, images: []}]; - ImportManager.generateReport(input).then(function (output) { - assert.equal(output, input); - done(); - }).catch(done); + const output = await ImportManager.generateReport(input); + assert.equal(output, input); }); }); describe('importFromFile', function () { - it('does the import steps in order', function (done) { + it('does the import steps in order', async function () { const loadFileSpy = sinon.stub(ImportManager, 'loadFile').returns(Promise.resolve({})); const preProcessSpy = sinon.stub(ImportManager, 'preProcess').returns(Promise.resolve({})); const doImportSpy = sinon.stub(ImportManager, 'doImport').returns(Promise.resolve([])); const generateReportSpy = sinon.spy(ImportManager, 'generateReport'); const cleanupSpy = sinon.stub(ImportManager, 'cleanUp').returns(Promise.resolve()); - ImportManager.importFromFile({name: 'test.json', path: '/test.json'}).then(function () { - sinon.assert.calledOnce(loadFileSpy); - sinon.assert.calledOnce(preProcessSpy); - sinon.assert.calledOnce(doImportSpy); - sinon.assert.calledOnce(generateReportSpy); - sinon.assert.calledOnce(cleanupSpy); - sinon.assert.callOrder(loadFileSpy, preProcessSpy, doImportSpy, generateReportSpy, cleanupSpy); - - done(); - }).catch(done); + await ImportManager.importFromFile({name: 'test.json', path: '/test.json'}); + sinon.assert.calledOnce(loadFileSpy); + sinon.assert.calledOnce(preProcessSpy); + sinon.assert.calledOnce(doImportSpy); + sinon.assert.calledOnce(generateReportSpy); + sinon.assert.calledOnce(cleanupSpy); + sinon.assert.callOrder(loadFileSpy, preProcessSpy, doImportSpy, generateReportSpy, cleanupSpy); }); }); }); @@ -519,30 +503,29 @@ describe('Importer', function () { assert.equal(typeof JSONHandler.loadFile, 'function'); }); - it('correctly handles a valid db api wrapper', function (done) { + it('correctly handles a valid db api wrapper', async function () { const file = [{ path: testUtils.fixtures.getExportFixturePath('valid'), name: 'valid.json' }]; - JSONHandler.loadFile(file).then(function (result) { - assert(_.keys(result).includes('meta')); - assert(_.keys(result).includes('data')); - done(); - }).catch(done); + const result = await JSONHandler.loadFile(file); + assert(_.keys(result).includes('meta')); + assert(_.keys(result).includes('data')); }); - it('correctly errors when given a bad db api wrapper', function (done) { + it('correctly errors when given a bad db api wrapper', async function () { const file = [{ path: testUtils.fixtures.getExportFixturePath('broken'), name: 'broken.json' }]; - JSONHandler.loadFile(file).then(function () { - done(new Error('Didn\'t error for bad db api wrapper')); - }).catch(function (response) { - assert.equal(response.errorType, 'BadRequestError'); - done(); - }).catch(done); + await assert.rejects( + JSONHandler.loadFile(file), + (response) => { + assert.equal(response.errorType, 'BadRequestError'); + return true; + } + ); }); }); @@ -560,7 +543,7 @@ describe('Importer', function () { assert.equal(typeof MarkdownHandler.loadFile, 'function'); }); - it('does convert a markdown file into a post object', function (done) { + it('does convert a markdown file into a post object', async function () { const filename = 'draft-2014-12-19-test-1.md'; const file = [{ @@ -568,20 +551,17 @@ describe('Importer', function () { name: filename }]; - MarkdownHandler.loadFile(file).then(function (result) { - assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); - assert.equal(result.data.posts[0].status, 'draft'); - assert.equal(result.data.posts[0].slug, 'test-1'); - assert.equal(result.data.posts[0].title, 'test-1'); - assert.equal(result.data.posts[0].created_at, 1418990400000); - assert.equal(moment.utc(result.data.posts[0].created_at).format('DD MM YY HH:mm'), '19 12 14 12:00'); - assert(!('image' in result.data.posts[0])); - - done(); - }).catch(done); + const result = await MarkdownHandler.loadFile(file); + assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); + assert.equal(result.data.posts[0].status, 'draft'); + assert.equal(result.data.posts[0].slug, 'test-1'); + assert.equal(result.data.posts[0].title, 'test-1'); + assert.equal(result.data.posts[0].created_at, 1418990400000); + assert.equal(moment.utc(result.data.posts[0].created_at).format('DD MM YY HH:mm'), '19 12 14 12:00'); + assert(!('image' in result.data.posts[0])); }); - it('can parse a title from a markdown file', function (done) { + it('can parse a title from a markdown file', async function () { const filename = 'draft-2014-12-19-test-2.md'; const file = [{ @@ -589,19 +569,16 @@ describe('Importer', function () { name: filename }]; - MarkdownHandler.loadFile(file).then(function (result) { - assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); - assert.equal(result.data.posts[0].status, 'draft'); - assert.equal(result.data.posts[0].slug, 'test-2'); - assert.equal(result.data.posts[0].title, 'Welcome to Ghost'); - assert.equal(result.data.posts[0].created_at, 1418990400000); - assert(!('image' in result.data.posts[0])); - - done(); - }).catch(done); + const result = await MarkdownHandler.loadFile(file); + assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); + assert.equal(result.data.posts[0].status, 'draft'); + assert.equal(result.data.posts[0].slug, 'test-2'); + assert.equal(result.data.posts[0].title, 'Welcome to Ghost'); + assert.equal(result.data.posts[0].created_at, 1418990400000); + assert(!('image' in result.data.posts[0])); }); - it('can parse a featured image from a markdown file if there is a title', function (done) { + it('can parse a featured image from a markdown file if there is a title', async function () { const filename = 'draft-2014-12-19-test-3.md'; const file = [{ @@ -609,19 +586,16 @@ describe('Importer', function () { name: filename }]; - MarkdownHandler.loadFile(file).then(function (result) { - assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); - assert.equal(result.data.posts[0].status, 'draft'); - assert.equal(result.data.posts[0].slug, 'test-3'); - assert.equal(result.data.posts[0].title, 'Welcome to Ghost'); - assert.equal(result.data.posts[0].created_at, 1418990400000); - assert.equal(result.data.posts[0].image, '/images/kitten.jpg'); - - done(); - }).catch(done); + const result = await MarkdownHandler.loadFile(file); + assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); + assert.equal(result.data.posts[0].status, 'draft'); + assert.equal(result.data.posts[0].slug, 'test-3'); + assert.equal(result.data.posts[0].title, 'Welcome to Ghost'); + assert.equal(result.data.posts[0].created_at, 1418990400000); + assert.equal(result.data.posts[0].image, '/images/kitten.jpg'); }); - it('can import a published post', function (done) { + it('can import a published post', async function () { const filename = 'published-2014-12-19-test-1.md'; const file = [{ @@ -629,20 +603,17 @@ describe('Importer', function () { name: filename }]; - MarkdownHandler.loadFile(file).then(function (result) { - assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); - assert.equal(result.data.posts[0].status, 'published'); - assert.equal(result.data.posts[0].slug, 'test-1'); - assert.equal(result.data.posts[0].title, 'Welcome to Ghost'); - assert.equal(result.data.posts[0].published_at, 1418990400000); - assert.equal(moment.utc(result.data.posts[0].published_at).format('DD MM YY HH:mm'), '19 12 14 12:00'); - assert(!('image' in result.data.posts[0])); - - done(); - }).catch(done); + const result = await MarkdownHandler.loadFile(file); + assert.equal(result.data.posts[0].markdown, 'You\'re live! Nice.'); + assert.equal(result.data.posts[0].status, 'published'); + assert.equal(result.data.posts[0].slug, 'test-1'); + assert.equal(result.data.posts[0].title, 'Welcome to Ghost'); + assert.equal(result.data.posts[0].published_at, 1418990400000); + assert.equal(moment.utc(result.data.posts[0].published_at).format('DD MM YY HH:mm'), '19 12 14 12:00'); + assert(!('image' in result.data.posts[0])); }); - it('does not import deleted posts', function (done) { + it('does not import deleted posts', async function () { const filename = 'deleted-2014-12-19-test-1.md'; const file = [{ @@ -650,14 +621,11 @@ describe('Importer', function () { name: filename }]; - MarkdownHandler.loadFile(file).then(function (result) { - assert.equal(result.data.posts.length, 0); - - done(); - }).catch(done); + const result = await MarkdownHandler.loadFile(file); + assert.equal(result.data.posts.length, 0); }); - it('can import multiple files', function (done) { + it('can import multiple files', async function () { const files = [{ path: testUtils.fixtures.getImportFixturePath('deleted-2014-12-19-test-1.md'), name: 'deleted-2014-12-19-test-1.md' @@ -669,34 +637,31 @@ describe('Importer', function () { name: 'draft-2014-12-19-test-3.md' }]; - MarkdownHandler.loadFile(files).then(function (result) { - // deleted-2014-12-19-test-1.md - // doesn't get imported ;) - - // loadFile doesn't guarantee order of results - const one = result.data.posts[0].status === 'published' ? 0 : 1; - - const two = one === 0 ? 1 : 0; - - // published-2014-12-19-test-1.md - assert.equal(result.data.posts[one].markdown, 'You\'re live! Nice.'); - assert.equal(result.data.posts[one].status, 'published'); - assert.equal(result.data.posts[one].slug, 'test-1'); - assert.equal(result.data.posts[one].title, 'Welcome to Ghost'); - assert.equal(result.data.posts[one].published_at, 1418990400000); - assert.equal(moment.utc(result.data.posts[one].published_at).format('DD MM YY HH:mm'), '19 12 14 12:00'); - assert(!('image' in result.data.posts[one])); - - // draft-2014-12-19-test-3.md - assert.equal(result.data.posts[two].markdown, 'You\'re live! Nice.'); - assert.equal(result.data.posts[two].status, 'draft'); - assert.equal(result.data.posts[two].slug, 'test-3'); - assert.equal(result.data.posts[two].title, 'Welcome to Ghost'); - assert.equal(result.data.posts[two].created_at, 1418990400000); - assert.equal(result.data.posts[two].image, '/images/kitten.jpg'); - - done(); - }).catch(done); + const result = await MarkdownHandler.loadFile(files); + // deleted-2014-12-19-test-1.md + // doesn't get imported ;) + + // loadFile doesn't guarantee order of results + const one = result.data.posts[0].status === 'published' ? 0 : 1; + + const two = one === 0 ? 1 : 0; + + // published-2014-12-19-test-1.md + assert.equal(result.data.posts[one].markdown, 'You\'re live! Nice.'); + assert.equal(result.data.posts[one].status, 'published'); + assert.equal(result.data.posts[one].slug, 'test-1'); + assert.equal(result.data.posts[one].title, 'Welcome to Ghost'); + assert.equal(result.data.posts[one].published_at, 1418990400000); + assert.equal(moment.utc(result.data.posts[one].published_at).format('DD MM YY HH:mm'), '19 12 14 12:00'); + assert(!('image' in result.data.posts[one])); + + // draft-2014-12-19-test-3.md + assert.equal(result.data.posts[two].markdown, 'You\'re live! Nice.'); + assert.equal(result.data.posts[two].status, 'draft'); + assert.equal(result.data.posts[two].slug, 'test-3'); + assert.equal(result.data.posts[two].title, 'Welcome to Ghost'); + assert.equal(result.data.posts[two].created_at, 1418990400000); + assert.equal(result.data.posts[two].image, '/images/kitten.jpg'); }); }); diff --git a/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js b/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js index 8cfac95b848..c6183758932 100644 --- a/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js +++ b/ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js @@ -328,7 +328,7 @@ describe('Migration Fixture Utils', function () { }); describe('Add Fixtures For Model', function () { - it('should call add for main post fixture', function (done) { + it('should call add for main post fixture', async function () { const postOneStub = sinon.stub(models.Post, 'findOne').returns(Promise.resolve()); const postAddStub = sinon.stub(models.Post, 'add').returns(Promise.resolve({})); @@ -336,20 +336,17 @@ describe('Migration Fixture Utils', function () { return modelFixture.name === 'Post'; }); - fixtureManager.addFixturesForModel(postFixtures).then(function (result) { - assertExists(result); - assert(_.isPlainObject(result)); - assert.equal(result.expected, 11); - assert.equal(result.done, 11); - - sinon.assert.callCount(postOneStub, 11); - sinon.assert.callCount(postAddStub, 11); + const result = await fixtureManager.addFixturesForModel(postFixtures); + assertExists(result); + assert(_.isPlainObject(result)); + assert.equal(result.expected, 11); + assert.equal(result.done, 11); - done(); - }).catch(done); + sinon.assert.callCount(postOneStub, 11); + sinon.assert.callCount(postAddStub, 11); }); - it('should call add for main newsletter fixture', function (done) { + it('should call add for main newsletter fixture', async function () { const newsletterOneStub = sinon.stub(models.Newsletter, 'findOne').returns(Promise.resolve()); const newsletterAddStub = sinon.stub(models.Newsletter, 'add').returns(Promise.resolve({})); @@ -357,20 +354,17 @@ describe('Migration Fixture Utils', function () { return modelFixture.name === 'Newsletter'; }); - fixtureManager.addFixturesForModel(newsletterFixtures).then(function (result) { - assertExists(result); - assert(_.isPlainObject(result)); - assert.equal(result.expected, 1); - assert.equal(result.done, 1); - - sinon.assert.calledOnce(newsletterOneStub); - sinon.assert.calledOnce(newsletterAddStub); + const result = await fixtureManager.addFixturesForModel(newsletterFixtures); + assertExists(result); + assert(_.isPlainObject(result)); + assert.equal(result.expected, 1); + assert.equal(result.done, 1); - done(); - }).catch(done); + sinon.assert.calledOnce(newsletterOneStub); + sinon.assert.calledOnce(newsletterAddStub); }); - it('should not call add for main post fixture if it is already found', function (done) { + it('should not call add for main post fixture if it is already found', async function () { const postOneStub = sinon.stub(models.Post, 'findOne').returns(Promise.resolve({})); const postAddStub = sinon.stub(models.Post, 'add').returns(Promise.resolve({})); @@ -378,22 +372,19 @@ describe('Migration Fixture Utils', function () { return modelFixture.name === 'Post'; }); - fixtureManager.addFixturesForModel(postFixtures).then(function (result) { - assertExists(result); - assert(_.isPlainObject(result)); - assert.equal(result.expected, 11); - assert.equal(result.done, 0); + const result = await fixtureManager.addFixturesForModel(postFixtures); + assertExists(result); + assert(_.isPlainObject(result)); + assert.equal(result.expected, 11); + assert.equal(result.done, 0); - sinon.assert.callCount(postOneStub, 11); - sinon.assert.notCalled(postAddStub); - - done(); - }).catch(done); + sinon.assert.callCount(postOneStub, 11); + sinon.assert.notCalled(postAddStub); }); }); describe('Add Fixtures For Relation', function () { - it('should call attach for permissions-roles', function (done) { + it('should call attach for permissions-roles', async function () { const fromItem = { related: sinon.stub().returnsThis(), find: sinon.stub().returns() @@ -410,28 +401,25 @@ describe('Migration Fixture Utils', function () { const permsAllStub = sinon.stub(models.Permission, 'findAll').returns(Promise.resolve(dataMethodStub)); const rolesAllStub = sinon.stub(models.Role, 'findAll').returns(Promise.resolve(dataMethodStub)); - fixtureManager.addFixturesForRelation(fixtures.relations[0]).then(function (result) { - const FIXTURE_COUNT = 140; - assertExists(result); - assert(_.isPlainObject(result)); - assert.equal(result.expected, FIXTURE_COUNT); - assert.equal(result.done, FIXTURE_COUNT); - - // Permissions & Roles - sinon.assert.calledOnce(permsAllStub); - sinon.assert.calledOnce(rolesAllStub); - sinon.assert.callCount(dataMethodStub.filter, FIXTURE_COUNT); - sinon.assert.callCount(dataMethodStub.find, 10); - sinon.assert.callCount(baseUtilAttachStub, FIXTURE_COUNT); - - sinon.assert.callCount(fromItem.related, FIXTURE_COUNT); - sinon.assert.callCount(fromItem.find, FIXTURE_COUNT); - - done(); - }).catch(done); + const result = await fixtureManager.addFixturesForRelation(fixtures.relations[0]); + const FIXTURE_COUNT = 140; + assertExists(result); + assert(_.isPlainObject(result)); + assert.equal(result.expected, FIXTURE_COUNT); + assert.equal(result.done, FIXTURE_COUNT); + + // Permissions & Roles + sinon.assert.calledOnce(permsAllStub); + sinon.assert.calledOnce(rolesAllStub); + sinon.assert.callCount(dataMethodStub.filter, FIXTURE_COUNT); + sinon.assert.callCount(dataMethodStub.find, 10); + sinon.assert.callCount(baseUtilAttachStub, FIXTURE_COUNT); + + sinon.assert.callCount(fromItem.related, FIXTURE_COUNT); + sinon.assert.callCount(fromItem.find, FIXTURE_COUNT); }); - it('should call attach for posts-tags', function (done) { + it('should call attach for posts-tags', async function () { const fromItem = { related: sinon.stub().returnsThis(), find: sinon.stub().returns() @@ -448,26 +436,23 @@ describe('Migration Fixture Utils', function () { const postsAllStub = sinon.stub(models.Post, 'findAll').returns(Promise.resolve(dataMethodStub)); const tagsAllStub = sinon.stub(models.Tag, 'findAll').returns(Promise.resolve(dataMethodStub)); - fixtureManager.addFixturesForRelation(fixtures.relations[1]).then(function (result) { - assertExists(result); - assert(_.isPlainObject(result)); - assert.equal(result.expected, 7); - assert.equal(result.done, 7); - - // Posts & Tags - sinon.assert.calledOnce(postsAllStub); - sinon.assert.calledOnce(tagsAllStub); - sinon.assert.callCount(dataMethodStub.filter, 7); - sinon.assert.callCount(dataMethodStub.find, 7); - sinon.assert.callCount(fromItem.related, 7); - sinon.assert.callCount(fromItem.find, 7); - sinon.assert.callCount(baseUtilAttachStub, 7); - - done(); - }).catch(done); + const result = await fixtureManager.addFixturesForRelation(fixtures.relations[1]); + assertExists(result); + assert(_.isPlainObject(result)); + assert.equal(result.expected, 7); + assert.equal(result.done, 7); + + // Posts & Tags + sinon.assert.calledOnce(postsAllStub); + sinon.assert.calledOnce(tagsAllStub); + sinon.assert.callCount(dataMethodStub.filter, 7); + sinon.assert.callCount(dataMethodStub.find, 7); + sinon.assert.callCount(fromItem.related, 7); + sinon.assert.callCount(fromItem.find, 7); + sinon.assert.callCount(baseUtilAttachStub, 7); }); - it('will not call attach for posts-tags if already present', function (done) { + it('will not call attach for posts-tags if already present', async function () { const fromItem = { related: sinon.stub().returnsThis(), find: sinon.stub().returns({}), @@ -485,26 +470,23 @@ describe('Migration Fixture Utils', function () { const postsAllStub = sinon.stub(models.Post, 'findAll').returns(Promise.resolve(dataMethodStub)); const tagsAllStub = sinon.stub(models.Tag, 'findAll').returns(Promise.resolve(dataMethodStub)); - fixtureManager.addFixturesForRelation(fixtures.relations[1]).then(function (result) { - assertExists(result); - assert(_.isPlainObject(result)); - assert.equal(result.expected, 7); - assert.equal(result.done, 0); - - // Posts & Tags - sinon.assert.calledOnce(postsAllStub); - sinon.assert.calledOnce(tagsAllStub); - sinon.assert.callCount(dataMethodStub.filter, 7); - sinon.assert.callCount(dataMethodStub.find, 7); + const result = await fixtureManager.addFixturesForRelation(fixtures.relations[1]); + assertExists(result); + assert(_.isPlainObject(result)); + assert.equal(result.expected, 7); + assert.equal(result.done, 0); - sinon.assert.callCount(fromItem.related, 7); - sinon.assert.callCount(fromItem.find, 7); + // Posts & Tags + sinon.assert.calledOnce(postsAllStub); + sinon.assert.calledOnce(tagsAllStub); + sinon.assert.callCount(dataMethodStub.filter, 7); + sinon.assert.callCount(dataMethodStub.find, 7); - sinon.assert.notCalled(fromItem.tags); - sinon.assert.notCalled(fromItem.attach); + sinon.assert.callCount(fromItem.related, 7); + sinon.assert.callCount(fromItem.find, 7); - done(); - }).catch(done); + sinon.assert.notCalled(fromItem.tags); + sinon.assert.notCalled(fromItem.attach); }); }); diff --git a/ghost/core/test/unit/server/lib/image/image-size.test.js b/ghost/core/test/unit/server/lib/image/image-size.test.js index 7790dc04cb3..80e6287995e 100644 --- a/ghost/core/test/unit/server/lib/image/image-size.test.js +++ b/ghost/core/test/unit/server/lib/image/image-size.test.js @@ -86,7 +86,7 @@ describe('lib/image: image size', function () { }); describe('getImageSizeFromUrl', function () { - it('[success] should return image dimensions from probe request for probe-supported extension', function (done) { + it('[success] should return image dimensions from probe request for probe-supported extension', async function () { const url = 'http://img.stockfresh.com/files/f/feedough/x/11/1540353_20925115.jpg'; const expectedImageObject = { height: 1, @@ -100,17 +100,15 @@ describe('lib/image: image size', function () { const imageSize = createImageSize(); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMock.isDone(), true); - assertExists(res); - assert.equal(res.width, expectedImageObject.width); - assert.equal(res.height, expectedImageObject.height); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMock.isDone(), true); + assertExists(res); + assert.equal(res.width, expectedImageObject.width); + assert.equal(res.height, expectedImageObject.height); + assert.equal(res.url, expectedImageObject.url); }); - it('[success] should return image dimensions from fetch request for non-probe-supported extension', function (done) { + it('[success] should return image dimensions from fetch request for non-probe-supported extension', async function () { const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.cur'; const expectedImageObject = { height: 1, @@ -129,17 +127,15 @@ describe('lib/image: image size', function () { } }); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMock.isDone(), false); - assertExists(res); - assert.equal(res.width, expectedImageObject.width); - assert.equal(res.height, expectedImageObject.height); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMock.isDone(), false); + assertExists(res); + assert.equal(res.width, expectedImageObject.width); + assert.equal(res.height, expectedImageObject.height); + assert.equal(res.url, expectedImageObject.url); }); - it('[success] should return image dimensions when no image extension given', function (done) { + it('[success] should return image dimensions when no image extension given', async function () { const url = 'https://www.zomato.com/logo/18163505/minilogo'; const expectedImageObject = { height: 1, @@ -153,17 +149,15 @@ describe('lib/image: image size', function () { const imageSize = createImageSize(); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMock.isDone(), true); - assertExists(res); - assert.equal(res.width, expectedImageObject.width); - assert.equal(res.height, expectedImageObject.height); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMock.isDone(), true); + assertExists(res); + assert.equal(res.width, expectedImageObject.width); + assert.equal(res.height, expectedImageObject.height); + assert.equal(res.url, expectedImageObject.url); }); - it('[success] should returns largest image value for .ico files', function (done) { + it('[success] should returns largest image value for .ico files', async function () { const url = 'https://super-website.com/media/icon.ico'; const expectedImageObject = { height: 64, @@ -178,18 +172,16 @@ describe('lib/image: image size', function () { const imageSize = createImageSize(); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMockNotFound.isDone(), false); - assert.equal(requestMock.isDone(), true); - assertExists(res); - assert.equal(res.width, expectedImageObject.width); - assert.equal(res.height, expectedImageObject.height); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMockNotFound.isDone(), false); + assert.equal(requestMock.isDone(), true); + assertExists(res); + assert.equal(res.width, expectedImageObject.width); + assert.equal(res.height, expectedImageObject.height); + assert.equal(res.url, expectedImageObject.url); }); - it('[success] should return image dimensions asset path images', function (done) { + it('[success] should return image dimensions asset path images', async function () { const url = '/assets/img/logo.png?v=d30c3d1e41'; const expectedImageObject = { height: 1, @@ -223,17 +215,15 @@ describe('lib/image: image size', function () { } }); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMock.isDone(), true); - assertExists(res); - assert.equal(res.width, expectedImageObject.width); - assert.equal(res.height, expectedImageObject.height); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMock.isDone(), true); + assertExists(res); + assert.equal(res.width, expectedImageObject.width); + assert.equal(res.height, expectedImageObject.height); + assert.equal(res.url, expectedImageObject.url); }); - it('[success] should return image dimensions for gravatar images request', function (done) { + it('[success] should return image dimensions for gravatar images request', async function () { const url = '//www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&d=mm&r=x'; const expectedImageObject = { height: 1, @@ -247,17 +237,15 @@ describe('lib/image: image size', function () { const imageSize = createImageSize(); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMock.isDone(), true); - assertExists(res); - assert.equal(res.width, expectedImageObject.width); - assert.equal(res.height, expectedImageObject.height); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMock.isDone(), true); + assertExists(res); + assert.equal(res.width, expectedImageObject.width); + assert.equal(res.height, expectedImageObject.height); + assert.equal(res.url, expectedImageObject.url); }); - it('[success] can handle redirect (probe-image-size)', function (done) { + it('[success] can handle redirect (probe-image-size)', async function () { const url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg'; const expectedImageObject = { height: 1, @@ -277,18 +265,16 @@ describe('lib/image: image size', function () { const imageSize = createImageSize(); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMock.isDone(), true); - assert.equal(secondRequestMock.isDone(), true); - assertExists(res); - assert.equal(res.width, expectedImageObject.width); - assert.equal(res.height, expectedImageObject.height); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMock.isDone(), true); + assert.equal(secondRequestMock.isDone(), true); + assertExists(res); + assert.equal(res.width, expectedImageObject.width); + assert.equal(res.height, expectedImageObject.height); + assert.equal(res.url, expectedImageObject.url); }); - it('[success] should switch to local file storage if available', function (done) { + it('[success] should switch to local file storage if available', async function () { const url = '/content/images/favicon.png'; const expectedImageObject = { height: 100, @@ -325,17 +311,15 @@ describe('lib/image: image size', function () { } }); - imageSize.getImageSizeFromUrl(url).then(function (res) { - assert.equal(requestMock.isDone(), false); - assertExists(res); - assertExists(res.width); - assert.equal(res.width, expectedImageObject.width); - assertExists(res.height); - assert.equal(res.height, expectedImageObject.height); - assertExists(res.url); - assert.equal(res.url, expectedImageObject.url); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromUrl(url); + assert.equal(requestMock.isDone(), false); + assertExists(res); + assertExists(res.width); + assert.equal(res.width, expectedImageObject.width); + assertExists(res.height); + assert.equal(res.height, expectedImageObject.height); + assertExists(res.url); + assert.equal(res.url, expectedImageObject.url); }); it('should use storage for local URL and HTTP for CDN URL', async function () { @@ -380,7 +364,7 @@ describe('lib/image: image size', function () { sinon.assert.calledOnce(storageReadSpy); }); - it('[failure] can handle an error with statuscode not 200 (probe-image-size)', function (done) { + it('[failure] can handle an error with statuscode not 200 (probe-image-size)', async function () { const url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg'; const requestMock = nock('http://noimagehere.com') @@ -389,17 +373,16 @@ describe('lib/image: image size', function () { const imageSize = createImageSize(); - imageSize.getImageSizeFromUrl(url) - .catch(function (err) { - assert.equal(requestMock.isDone(), true); - assertExists(err); - assert.equal(err.errorType, 'NotFoundError'); - assert.equal(err.message, 'Image not found.'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'NotFoundError', + message: 'Image not found.' + }); + assert.equal(requestMock.isDone(), true); }); - it('[failure] can handle an error with statuscode not 200 (image-size)', function (done) { + it('[failure] can handle an error with statuscode not 200 (image-size)', async function () { const url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.cur'; const requestMock = nock('http://noimagehere.com') @@ -423,33 +406,31 @@ describe('lib/image: image size', function () { } }); - imageSize.getImageSizeFromUrl(url) - .catch(function (err) { - assert.equal(requestMock.isDone(), false); - assertExists(err); - assert.equal(err.errorType, 'NotFoundError'); - assert.equal(err.message, 'Image not found.'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'NotFoundError', + message: 'Image not found.' + }); + assert.equal(requestMock.isDone(), false); }); - it('[failure] handles invalid URL', function (done) { + it('[failure] handles invalid URL', async function () { const url = 'Not-a-valid-url'; const imageSize = createImageSize({ validator: {isURL: () => false} }); - imageSize.getImageSizeFromUrl(url) - .catch(function (err) { - assertExists(err); - assert.equal(err.errorType, 'InternalServerError'); - assert.equal(err.message, 'URL empty or invalid.'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'InternalServerError', + message: 'URL empty or invalid.' + }); }); - it('[failure] will handle responses timing out', function (done) { + it('[failure] will handle responses timing out', async function () { const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256'; const requestMock = nock('https://static.wixstatic.com') @@ -471,17 +452,16 @@ describe('lib/image: image size', function () { } }); - imageSize.getImageSizeFromUrl(url) - .catch(function (err) { - assert.equal(requestMock.isDone(), true); - assertExists(err); - assert.equal(err.errorType, 'InternalServerError'); - assert.equal(err.message, 'Request timed out.'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'InternalServerError', + message: 'Request timed out.' + }); + assert.equal(requestMock.isDone(), true); }); - it('[failure] returns error if \`probe-image-size`\ module throws error', function (done) { + it('[failure] returns error if `probe-image-size` module throws error', async function () { const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg'; const requestMock = nock('https://static.wixstatic.com') @@ -490,19 +470,15 @@ describe('lib/image: image size', function () { const imageSize = createImageSize(); - imageSize.getImageSizeFromUrl(url) - .then(() => { - assert.equal(true, false, 'succeeded when expecting failure'); - }) - .catch(function (err) { - assert.equal(requestMock.isDone(), true); - assertExists(err); - assert.equal(err.errorType, 'InternalServerError'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'InternalServerError' + }); + assert.equal(requestMock.isDone(), true); }); - it('[failure] returns error if \`image-size`\ module throws error', function (done) { + it('[failure] returns error if `image-size` module throws error', async function () { const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.cur'; const requestMock = nock('https://static.wixstatic.com') @@ -520,35 +496,30 @@ describe('lib/image: image size', function () { } }); - imageSize.getImageSizeFromUrl(url) - .then(() => { - assert.equal(true, false, 'succeeded when expecting failure'); - }) - .catch(function (err) { - assert.equal(requestMock.isDone(), false); - assertExists(err); - assert.equal(err.errorType, 'InternalServerError'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'InternalServerError' + }); + assert.equal(requestMock.isDone(), false); }); - it('[failure] returns error if request errors', function (done) { + it('[failure] returns error if request errors', async function () { const url = 'https://notarealwebsite.com/images/notapicture.dds'; const imageSize = createImageSize({ request: () => Promise.reject({}) }); - imageSize.getImageSizeFromUrl(url) - .catch(function (err) { - assertExists(err); - assert.equal(err.errorType, 'InternalServerError'); - assert.equal(err.message, 'Unknown Request error.'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'InternalServerError', + message: 'Unknown Request error.' + }); }); - it('[failure] handles probe being unresponsive', function (done) { + it('[failure] handles probe being unresponsive', async function () { const url = 'http://img.stockfresh.com/files/f/feedough/x/11/1540353_20925115.jpg'; const requestMock = nock('http://img.stockfresh.com') .get('/files/f/feedough/x/11/1540353_20925115.jpg') @@ -568,19 +539,18 @@ describe('lib/image: image size', function () { } }); - imageSize.getImageSizeFromUrl(url) - .catch(function (err) { - assert.equal(requestMock.isDone(), true); - assertExists(err); - assert.equal(err.errorType, 'InternalServerError'); - assert.equal(err.message, 'Probe unresponsive.'); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromUrl(url); + }, { + errorType: 'InternalServerError', + message: 'Probe unresponsive.' + }); + assert.equal(requestMock.isDone(), true); }); }); describe('getImageSizeFromStoragePath', function () { - it('[success] should return image dimensions for locally stored images', function (done) { + it('[success] should return image dimensions for locally stored images', async function () { const url = '/content/images/ghost-logo.png'; const expectedImageObject = { height: 257, @@ -595,13 +565,11 @@ describe('lib/image: image size', function () { request: () => Promise.reject(new Error('request should not be used')) }); - imageSize.getImageSizeFromStoragePath(url).then(function (res) { - assertImageObject(res, expectedImageObject); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromStoragePath(url); + assertImageObject(res, expectedImageObject); }); - it('[success] should return image dimensions for locally stored images with subdirectory', function (done) { + it('[success] should return image dimensions for locally stored images with subdirectory', async function () { const url = '/content/images/favicon_too_large.png'; const expectedImageObject = { height: 1010, @@ -616,13 +584,11 @@ describe('lib/image: image size', function () { request: () => Promise.reject(new Error('request should not be used')) }); - imageSize.getImageSizeFromStoragePath(url).then(function (res) { - assertImageObject(res, expectedImageObject); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromStoragePath(url); + assertImageObject(res, expectedImageObject); }); - it('[success] should return largest image dimensions for locally stored .ico image', function (done) { + it('[success] should return largest image dimensions for locally stored .ico image', async function () { const url = 'http://myblog.com/content/images/favicon_multi_sizes.ico'; const expectedImageObject = { height: 64, @@ -637,13 +603,11 @@ describe('lib/image: image size', function () { request: () => Promise.reject(new Error('request should not be used')) }); - imageSize.getImageSizeFromStoragePath(url).then(function (res) { - assertImageObject(res, expectedImageObject); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromStoragePath(url); + assertImageObject(res, expectedImageObject); }); - it('[success] should return image dimensions for locally stored .webp image', function (done) { + it('[success] should return image dimensions for locally stored .webp image', async function () { const url = 'http://myblog.com/content/images/ghosticon.webp'; const expectedImageObject = { height: 249, @@ -658,13 +622,11 @@ describe('lib/image: image size', function () { request: () => Promise.reject(new Error('request should not be used')) }); - imageSize.getImageSizeFromStoragePath(url).then(function (res) { - assertImageObject(res, expectedImageObject); - done(); - }).catch(done); + const res = await imageSize.getImageSizeFromStoragePath(url); + assertImageObject(res, expectedImageObject); }); - it('[failure] returns error if storage adapter errors', function (done) { + it('[failure] returns error if storage adapter errors', async function () { const url = '/content/images/not-existing-image.png'; const imageSize = createImageSize({storage: { @@ -675,12 +637,9 @@ describe('lib/image: image size', function () { }) }, storageUtils: createFixtureStorageUtils(), urlUtils: createLocalUrlUtils('http://myblog.com/content/images/not-existing-image.png'), request: () => Promise.reject(new Error('request should not be used'))}); - imageSize.getImageSizeFromStoragePath(url) - .catch(function (err) { - assertExists(err); - assert.equal((err instanceof errors.NotFoundError), true); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromStoragePath(url); + }, errors.NotFoundError); }); it('[failure] rejects traversal outside the image storage root', async function () { @@ -709,7 +668,7 @@ describe('lib/image: image size', function () { ); }); - it('[failure] returns error if \`image-size`\ module throws error', function (done) { + it('[failure] returns error if `image-size` module throws error', async function () { const url = '/content/images/malformed.svg'; const imageSize = createImageSize({storage: { @@ -727,11 +686,9 @@ describe('lib/image: image size', function () { return Promise.reject({}); }}); - imageSize.getImageSizeFromStoragePath(url) - .catch(function (err) { - assertExists(err); - done(); - }).catch(done); + await assert.rejects(async () => { + await imageSize.getImageSizeFromStoragePath(url); + }); }); }); }); diff --git a/ghost/core/test/unit/server/lib/package-json/read.test.js b/ghost/core/test/unit/server/lib/package-json/read.test.js index 9abdf289d65..76773c972a5 100644 --- a/ghost/core/test/unit/server/lib/package-json/read.test.js +++ b/ghost/core/test/unit/server/lib/package-json/read.test.js @@ -7,7 +7,7 @@ const packageJSON = require('../../../../../core/server/lib/package-json'); describe('package-json read', function () { describe('readPackages', function () { - it('should read directory and ignore unneeded items', function (done) { + it('should read directory and ignore unneeded items', async function () { const packagePath = tmp.dirSync({unsafeCleanup: true}); // create example theme @@ -20,23 +20,21 @@ describe('package-json read', function () { fs.mkdirSync(join(packagePath.name, '.git')); fs.writeFileSync(join(packagePath.name, '.DS_Store'), ''); - packageJSON.readPackages(packagePath.name) - .then(function (pkgs) { - assert.deepEqual(pkgs, { - casper: { - name: 'casper', - path: join(packagePath.name, 'casper'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + try { + const pkgs = await packageJSON.readPackages(packagePath.name); + assert.deepEqual(pkgs, { + casper: { + name: 'casper', + path: join(packagePath.name, 'casper'), + 'package.json': null + } + }); + } finally { + await packagePath.removeCallback(); + } }); - it('should read directory and parse package.json files', function (done) { + it('should read directory and parse package.json files', async function () { let packagePath; let pkgJson; @@ -51,26 +49,24 @@ describe('package-json read', function () { fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs'), ''); - packageJSON.readPackages(packagePath.name) - .then(function (pkgs) { - assert.deepEqual(pkgs, { - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': { - name: 'test', - version: '0.0.0' - } + try { + const pkgs = await packageJSON.readPackages(packagePath.name); + assert.deepEqual(pkgs, { + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': { + name: 'test', + version: '0.0.0' } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + } + }); + } finally { + await packagePath.removeCallback(); + } }); - it('should read directory and ignore invalid package.json files', function (done) { + it('should read directory and ignore invalid package.json files', async function () { let packagePath; let pkgJson; @@ -84,23 +80,21 @@ describe('package-json read', function () { fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs'), ''); - packageJSON.readPackages(packagePath.name) - .then(function (pkgs) { - assert.deepEqual(pkgs, { - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + try { + const pkgs = await packageJSON.readPackages(packagePath.name); + assert.deepEqual(pkgs, { + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': null + } + }); + } finally { + await packagePath.removeCallback(); + } }); - it('should read directory and include symlinked directories', function (done) { + it('should read directory and include symlinked directories', async function () { let packagePath; let pkgJson; @@ -117,25 +111,23 @@ describe('package-json read', function () { // Symlink one theme to the other so we should have 2 themes fs.symlinkSync(join(packagePath.name, 'testtheme'), join(packagePath.name, 'testtheme2')); - packageJSON.readPackages(packagePath.name) - .then(function (pkgs) { - assert.deepEqual(pkgs, { - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': null - }, - testtheme2: { - name: 'testtheme2', - path: join(packagePath.name, 'testtheme2'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + try { + const pkgs = await packageJSON.readPackages(packagePath.name); + assert.deepEqual(pkgs, { + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': null + }, + testtheme2: { + name: 'testtheme2', + path: join(packagePath.name, 'testtheme2'), + 'package.json': null + } + }); + } finally { + await packagePath.removeCallback(); + } }); it('should read directory and ignore invalid symlinks', async function () { @@ -168,7 +160,7 @@ describe('package-json read', function () { }); describe('readPackage', function () { - it('should read directory and ignore unneeded items', function (done) { + it('should read directory and ignore unneeded items', async function () { const packagePath = tmp.dirSync({unsafeCleanup: true}); // create example theme @@ -181,23 +173,21 @@ describe('package-json read', function () { fs.mkdirSync(join(packagePath.name, '.git')); fs.writeFileSync(join(packagePath.name, '.DS_Store'), ''); - packageJSON.readPackage(packagePath.name, 'casper') - .then(function (pkgs) { - assert.deepEqual(pkgs, { - casper: { - name: 'casper', - path: join(packagePath.name, 'casper'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + try { + const pkgs = await packageJSON.readPackage(packagePath.name, 'casper'); + assert.deepEqual(pkgs, { + casper: { + name: 'casper', + path: join(packagePath.name, 'casper'), + 'package.json': null + } + }); + } finally { + await packagePath.removeCallback(); + } }); - it('should read directory and parse package.json files', function (done) { + it('should read directory and parse package.json files', async function () { let packagePath; let pkgJson; @@ -212,26 +202,24 @@ describe('package-json read', function () { fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs'), ''); - packageJSON.readPackage(packagePath.name, 'testtheme') - .then(function (pkgs) { - assert.deepEqual(pkgs, { - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': { - name: 'test', - version: '0.0.0' - } + try { + const pkgs = await packageJSON.readPackage(packagePath.name, 'testtheme'); + assert.deepEqual(pkgs, { + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': { + name: 'test', + version: '0.0.0' } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + } + }); + } finally { + await packagePath.removeCallback(); + } }); - it('should read directory and ignore invalid package.json files', function (done) { + it('should read directory and ignore invalid package.json files', async function () { let packagePath; let pkgJson; @@ -245,23 +233,21 @@ describe('package-json read', function () { fs.writeFileSync(join(packagePath.name, 'testtheme', 'package.json'), pkgJson); fs.writeFileSync(join(packagePath.name, 'testtheme', 'index.hbs'), ''); - packageJSON.readPackage(packagePath.name, 'testtheme') - .then(function (pkgs) { - assert.deepEqual(pkgs, { - testtheme: { - name: 'testtheme', - path: join(packagePath.name, 'testtheme'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + try { + const pkgs = await packageJSON.readPackage(packagePath.name, 'testtheme'); + assert.deepEqual(pkgs, { + testtheme: { + name: 'testtheme', + path: join(packagePath.name, 'testtheme'), + 'package.json': null + } + }); + } finally { + await packagePath.removeCallback(); + } }); - it('should read directory and include only single requested package', function (done) { + it('should read directory and include only single requested package', async function () { const packagePath = tmp.dirSync({unsafeCleanup: true}); // create trash @@ -276,55 +262,46 @@ describe('package-json read', function () { fs.mkdirSync(join(packagePath.name, 'not-casper')); fs.writeFileSync(join(packagePath.name, 'not-casper', 'index.hbs'), ''); - packageJSON.readPackage(packagePath.name, 'casper') - .then(function (pkgs) { - assert.deepEqual(pkgs, { - casper: { - name: 'casper', - path: join(packagePath.name, 'casper'), - 'package.json': null - } - }); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + try { + const pkgs = await packageJSON.readPackage(packagePath.name, 'casper'); + assert.deepEqual(pkgs, { + casper: { + name: 'casper', + path: join(packagePath.name, 'casper'), + 'package.json': null + } + }); + } finally { + await packagePath.removeCallback(); + } }); - it('should return an error if package cannot be found', function (done) { + it('should return an error if package cannot be found', async function () { const packagePath = tmp.dirSync({unsafeCleanup: true}); // create trash fs.writeFileSync(join(packagePath.name, 'casper.zip'), ''); fs.writeFileSync(join(packagePath.name, '.DS_Store'), ''); - packageJSON.readPackage(packagePath.name, 'casper') - .then(function () { - done('Should have thrown an error'); - }) - .catch(function (err) { - assert.equal(err.message, 'Package not found'); - done(); - }) - .finally(packagePath.removeCallback); + await assert.rejects(async () => { + await packageJSON.readPackage(packagePath.name, 'casper'); + }, /Package not found/); + await packagePath.removeCallback(); }); - it('should return empty object if package is not a directory', function (done) { + it('should return empty object if package is not a directory', async function () { const packagePath = tmp.dirSync({unsafeCleanup: true}); // create trash fs.writeFileSync(join(packagePath.name, 'casper.zip'), ''); fs.writeFileSync(join(packagePath.name, '.DS_Store'), ''); - packageJSON.readPackage(packagePath.name, 'casper.zip') - .then(function (pkg) { - assert.deepEqual(pkg, {}); - - done(); - }) - .catch(done) - .finally(packagePath.removeCallback); + try { + const pkgs = await packageJSON.readPackage(packagePath.name, 'casper.zip'); + assert.deepEqual(pkgs, {}); + } finally { + await packagePath.removeCallback(); + } }); }); }); diff --git a/ghost/core/test/unit/server/models/post.test.js b/ghost/core/test/unit/server/models/post.test.js index 03398c7d409..04281fd7452 100644 --- a/ghost/core/test/unit/server/models/post.test.js +++ b/ghost/core/test/unit/server/models/post.test.js @@ -431,7 +431,7 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { describe('Permissible', function () { describe('As Contributor', function () { describe('Editing', function () { - it('rejects if changing status', function (done) { + it('rejects if changing status', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -442,24 +442,24 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.get.withArgs('status').returns('draft'); mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - false - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - done(); - }).catch(done); + await assert.rejects( + async () => { + await models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + false + ); + }, + errors.NoPermissionError + ); }); - it('rejects if changing visibility', function (done) { + it('rejects if changing visibility', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -470,24 +470,22 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.get.withArgs('visibility').returns('paid'); mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - false - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - done(); - }).catch(done); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + false + ), + errors.NoPermissionError + ); }); - it('rejects if changing authors.0', function (done) { + it('rejects if changing authors.0', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -497,26 +495,24 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - false - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - assert.equal(mockPostObj.related.calledTwice, false); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + false + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); + assert.equal(mockPostObj.related.calledTwice, false); }); - it('ignores if changes authors.1', function (done) { + it('ignores if changes authors.1', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -527,7 +523,7 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); mockPostObj.get.withArgs('status').returns('draft'); - models.Post.permissible( + const result = await models.Post.permissible( mockPostObj, 'edit', context, @@ -536,16 +532,14 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { true, true, false - ).then((result) => { - assertExists(result); - assert.deepEqual(result.excludedAttrs, ['authors', 'tags']); - sinon.assert.calledTwice(mockPostObj.get); - sinon.assert.calledTwice(mockPostObj.related); - done(); - }).catch(done); + ); + assertExists(result); + assert.deepEqual(result.excludedAttrs, ['authors', 'tags']); + sinon.assert.calledTwice(mockPostObj.get); + sinon.assert.calledTwice(mockPostObj.related); }); - it('rejects if post is not draft', function (done) { + it('rejects if post is not draft', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -556,26 +550,24 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.get.withArgs('status').returns('published'); mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - false - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.calledTwice(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + false + ), + errors.NoPermissionError + ); + sinon.assert.calledTwice(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); - it('rejects if contributor is not author of post', function (done) { + it('rejects if contributor is not author of post', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -585,22 +577,20 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - false - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + false + ), + errors.NoPermissionError + ); + sinon.assert.calledOnce(mockPostObj.related); }); it('resolves if none of the above cases are true', function () { @@ -633,89 +623,83 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { }); describe('Adding', function () { - it('rejects if "published" status', function (done) { + it('rejects if "published" status', async function () { const mockPostObj = { get: sinon.stub() }; const context = {user: 1}; const unsafeAttrs = {status: 'published', authors: [{id: 1}]}; - models.Post.permissible( - mockPostObj, - 'add', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'add', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); }); - it('rejects if different author id', function (done) { + it('rejects if different author id', async function () { const mockPostObj = { get: sinon.stub() }; const context = {user: 1}; const unsafeAttrs = {status: 'draft', authors: [{id: 2}]}; - models.Post.permissible( - mockPostObj, - 'add', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'add', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); }); - it('rejects if different logged in user and `authors.0`', function (done) { + it('rejects if different logged in user and `authors.0`', async function () { const mockPostObj = { get: sinon.stub() }; const context = {user: 1}; const unsafeAttrs = {status: 'draft', authors: [{id: 2}]}; - models.Post.permissible( - mockPostObj, - 'add', - context, - unsafeAttrs, - testUtils.permissions.contributor, - true, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'add', + context, + unsafeAttrs, + testUtils.permissions.contributor, + true, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); }); - it('resolves if same logged in user and `authors.0`', function (done) { + it('resolves if same logged in user and `authors.0`', async function () { const mockPostObj = { get: sinon.stub() }; const context = {user: 1}; const unsafeAttrs = {status: 'draft', authors: [{id: 1}]}; - models.Post.permissible( + const result = await models.Post.permissible( mockPostObj, 'add', context, @@ -724,17 +708,15 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { true, true, true - ).then((result) => { - assertExists(result); - assert.deepEqual(result.excludedAttrs, ['authors', 'tags']); - sinon.assert.notCalled(mockPostObj.get); - done(); - }).catch(done); + ); + assertExists(result); + assert.deepEqual(result.excludedAttrs, ['authors', 'tags']); + sinon.assert.notCalled(mockPostObj.get); }); }); describe('Destroying', function () { - it('rejects if destroying another author\'s post', function (done) { + it('rejects if destroying another author\'s post', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -743,26 +725,24 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'destroy', - context, - {}, - testUtils.permissions.contributor, - true, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.calledOnce(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'destroy', + context, + {}, + testUtils.permissions.contributor, + true, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.calledOnce(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); - it('rejects if destroying a published post', function (done) { + it('rejects if destroying a published post', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -772,23 +752,21 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); mockPostObj.get.withArgs('status').returns('published'); - models.Post.permissible( - mockPostObj, - 'destroy', - context, - {}, - testUtils.permissions.contributor, - true, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.calledOnce(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'destroy', + context, + {}, + testUtils.permissions.contributor, + true, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.calledOnce(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); it('resolves if none of the above cases are true', function () { @@ -822,7 +800,7 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { describe('As Author', function () { describe('Editing', function () { - it('rejects if editing another\'s post', function (done) { + it('rejects if editing another\'s post', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -832,26 +810,24 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 2}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.author, - false, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.author, + false, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); - it('rejects if changing visibility', function (done) { + it('rejects if changing visibility', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -862,26 +838,24 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.get.withArgs('visibility').returns('paid'); mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.author, - false, - false, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.author, + false, + false, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); - it('rejects if editing another\'s post (using `authors`)', function (done) { + it('rejects if editing another\'s post (using `authors`)', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -891,26 +865,24 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.author, - false, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - sinon.assert.calledTwice(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.author, + false, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); + sinon.assert.calledTwice(mockPostObj.related); }); - it('rejects if changing authors', function (done) { + it('rejects if changing authors', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -920,23 +892,21 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.author, - false, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - sinon.assert.calledTwice(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.author, + false, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); + sinon.assert.calledTwice(mockPostObj.related); }); it('resolves if none of the above cases are true', function () { @@ -964,32 +934,30 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { }); describe('Adding', function () { - it('rejects if different author id', function (done) { + it('rejects if different author id', async function () { const mockPostObj = { get: sinon.stub() }; const context = {user: 1}; const unsafeAttrs = {authors: [{id: 2}]}; - models.Post.permissible( - mockPostObj, - 'add', - context, - unsafeAttrs, - testUtils.permissions.author, - false, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'add', + context, + unsafeAttrs, + testUtils.permissions.author, + false, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); }); - it('rejects if different authors', function (done) { + it('rejects if different authors', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -999,28 +967,26 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( - mockPostObj, - 'add', - context, - unsafeAttrs, - testUtils.permissions.author, - false, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'add', + context, + unsafeAttrs, + testUtils.permissions.author, + false, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); }); }); }); describe('Everyone Else', function () { - it('rejects if hasUserPermissions is false and not current owner', function (done) { + it('rejects if hasUserPermissions is false and not current owner', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -1030,23 +996,21 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.related.withArgs('authors').returns({models: [{id: 2}]}); - models.Post.permissible( - mockPostObj, - 'edit', - context, - unsafeAttrs, - testUtils.permissions.editor, - false, - true, - true - ).then(() => { - done(new Error('Permissible function should have rejected.')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.notCalled(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }); + await assert.rejects( + models.Post.permissible( + mockPostObj, + 'edit', + context, + unsafeAttrs, + testUtils.permissions.editor, + false, + true, + true + ), + errors.NoPermissionError + ); + sinon.assert.notCalled(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); it('resolves if hasUserPermission is true', function () { @@ -1070,7 +1034,7 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { }); }); - it('resolves if changing visibility as owner', function (done) { + it('resolves if changing visibility as owner', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -1081,7 +1045,7 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.get.withArgs('visibility').returns('paid'); mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( + await models.Post.permissible( mockPostObj, 'edit', context, @@ -1090,16 +1054,12 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { false, true, true - ).then(() => { - sinon.assert.notCalled(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }).catch(() => { - done(new Error('Permissible function should have passed for owner.')); - }); + ); + sinon.assert.notCalled(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); - it('resolves if changing visibility as administrator', function (done) { + it('resolves if changing visibility as administrator', async function () { const mockPostObj = { get: sinon.stub(), related: sinon.stub() @@ -1110,7 +1070,7 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { mockPostObj.get.withArgs('visibility').returns('paid'); mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]}); - models.Post.permissible( + await models.Post.permissible( mockPostObj, 'edit', context, @@ -1119,13 +1079,9 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () { false, true, true - ).then(() => { - sinon.assert.notCalled(mockPostObj.get); - sinon.assert.calledOnce(mockPostObj.related); - done(); - }).catch(() => { - done(new Error('Permissible function should have passed for administrator.')); - }); + ); + sinon.assert.notCalled(mockPostObj.get); + sinon.assert.calledOnce(mockPostObj.related); }); }); }); diff --git a/ghost/core/test/unit/server/models/session.test.js b/ghost/core/test/unit/server/models/session.test.js index ec4aabcd8ef..5eb2fe77da2 100644 --- a/ghost/core/test/unit/server/models/session.test.js +++ b/ghost/core/test/unit/server/models/session.test.js @@ -117,7 +117,7 @@ describe('Unit: models/session', function () { assert.equal(returnVal, baseDestroyReturnVal); }); - it('calls forge with the session_id, fetchs with the filtered options and then destroys with the options', function (done) { + it('calls forge with the session_id, fetchs with the filtered options and then destroys with the options', async function () { const model = models.Session.forge({}); const session_id = 23; const unfilteredOptions = {session_id}; @@ -132,22 +132,20 @@ describe('Unit: models/session', function () { const destroyStub = sinon.stub(model, 'destroy') .resolves(); - models.Session.destroy(unfilteredOptions).then(() => { - assert.equal(filterOptionsStub.args[0][0], unfilteredOptions); - assert.equal(filterOptionsStub.args[0][1], 'destroy'); + await models.Session.destroy(unfilteredOptions); - assert.deepEqual(forgeStub.args[0][0], {session_id}); + assert.equal(filterOptionsStub.args[0][0], unfilteredOptions); + assert.equal(filterOptionsStub.args[0][1], 'destroy'); - assert.equal(fetchStub.args[0][0], filteredOptions); - assert.equal(destroyStub.args[0][0], filteredOptions); + assert.deepEqual(forgeStub.args[0][0], {session_id}); - done(); - }); + assert.equal(fetchStub.args[0][0], filteredOptions); + assert.equal(destroyStub.args[0][0], filteredOptions); }); }); describe('upsert', function () { - it('calls findOne and then add if findOne results in nothing', function (done) { + it('calls findOne and then add if findOne results in nothing', async function () { const session_id = 314; const unfilteredOptions = {session_id}; const filteredOptions = {session_id}; @@ -165,27 +163,26 @@ describe('Unit: models/session', function () { const addStub = sinon.stub(models.Session, 'add'); - models.Session.upsert(data, unfilteredOptions).then(() => { - assert.equal(filterOptionsStub.args[0][0], unfilteredOptions); - assert.equal(filterOptionsStub.args[0][1], 'upsert'); + await models.Session.upsert(data, unfilteredOptions); - assert.deepEqual(findOneStub.args[0][0], { - session_id - }); - assert.equal(findOneStub.args[0][1], filteredOptions); + assert.equal(filterOptionsStub.args[0][0], unfilteredOptions); + assert.equal(filterOptionsStub.args[0][1], 'upsert'); - assert.deepEqual(addStub.args[0][0], { - session_id: filteredOptions.session_id, - session_data: data.session_data, - user_id: data.session_data.user_id - }); + assert.deepEqual(findOneStub.args[0][0], { + session_id + }); + assert.equal(findOneStub.args[0][1], filteredOptions); - assert.equal(addStub.args[0][1], filteredOptions); - done(); + assert.deepEqual(addStub.args[0][0], { + session_id: filteredOptions.session_id, + session_data: data.session_data, + user_id: data.session_data.user_id }); + + assert.equal(addStub.args[0][1], filteredOptions); }); - it('calls findOne and then edit if findOne results in nothing', function (done) { + it('calls findOne and then edit if findOne results in nothing', async function () { const model = models.Session.forge({id: 2}); const session_id = 314; const unfilteredOptions = {session_id}; @@ -204,27 +201,26 @@ describe('Unit: models/session', function () { const editStub = sinon.stub(models.Session, 'edit'); - models.Session.upsert(data, unfilteredOptions).then(() => { - assert.equal(filterOptionsStub.args[0][0], unfilteredOptions); - assert.equal(filterOptionsStub.args[0][1], 'upsert'); + await models.Session.upsert(data, unfilteredOptions); - assert.deepEqual(findOneStub.args[0][0], { - session_id - }); - assert.equal(findOneStub.args[0][1], filteredOptions); + assert.equal(filterOptionsStub.args[0][0], unfilteredOptions); + assert.equal(filterOptionsStub.args[0][1], 'upsert'); - assert.deepEqual(editStub.args[0][0], { - session_data: data.session_data - }); + assert.deepEqual(findOneStub.args[0][0], { + session_id + }); + assert.equal(findOneStub.args[0][1], filteredOptions); - assert.deepEqual(editStub.args[0][1], { - session_id, - id: model.id - }); + assert.deepEqual(editStub.args[0][0], { + session_data: data.session_data + }); - assert.equal(editStub.args[0][1], filteredOptions); - done(); + assert.deepEqual(editStub.args[0][1], { + session_id, + id: model.id }); + + assert.equal(editStub.args[0][1], filteredOptions); }); }); }); diff --git a/ghost/core/test/unit/server/models/user.test.js b/ghost/core/test/unit/server/models/user.test.js index b5153d1f797..ec23aa105a2 100644 --- a/ghost/core/test/unit/server/models/user.test.js +++ b/ghost/core/test/unit/server/models/user.test.js @@ -161,17 +161,14 @@ describe('Unit: models/user', function () { }; } - it('cannot delete owner', function (done) { + it('cannot delete owner', async function () { const mockUser = getUserModel(1, 'Owner'); const context = {user: 1}; - models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.owner, true, true, true).then(() => { - done(new Error('Permissible function should have errored')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.calledOnce(mockUser.hasRole); - done(); - }); + await assert.rejects(async () => { + await models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.owner, true, true, true); + }, errors.NoPermissionError); + sinon.assert.calledOnce(mockUser.hasRole); }); it('can always edit self', function () { @@ -312,46 +309,37 @@ describe('Unit: models/user', function () { }); describe('as editor', function () { - it('can\'t edit another editor', function (done) { + it('can\'t edit another editor', async function () { const mockUser = getUserModel(3, 'Editor'); const context = {user: 2}; - models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true, true).then(() => { - done(new Error('Permissible function should have errored')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.called(mockUser.hasRole); - sinon.assert.calledOnce(mockUser.get); - done(); - }); + await assert.rejects(async () => { + await models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true, true); + }, errors.NoPermissionError); + sinon.assert.called(mockUser.hasRole); + sinon.assert.calledOnce(mockUser.get); }); - it('can\'t edit owner', function (done) { + it('can\'t edit owner', async function () { const mockUser = getUserModel(3, 'Owner'); const context = {user: 2}; - models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true, true).then(() => { - done(new Error('Permissible function should have errored')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.called(mockUser.hasRole); - sinon.assert.calledOnce(mockUser.get); - done(); - }); + await assert.rejects(async () => { + await models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true, true); + }, errors.NoPermissionError); + sinon.assert.called(mockUser.hasRole); + sinon.assert.calledOnce(mockUser.get); }); - it('can\'t edit an admin', function (done) { + it('can\'t edit an admin', async function () { const mockUser = getUserModel(3, 'Administrator'); const context = {user: 2}; - models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true, true).then(() => { - done(new Error('Permissible function should have errored')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.called(mockUser.hasRole); - sinon.assert.calledOnce(mockUser.get); - done(); - }); + await assert.rejects(async () => { + await models.User.permissible(mockUser, 'edit', context, {}, testUtils.permissions.editor, true, true, true); + }, errors.NoPermissionError); + sinon.assert.called(mockUser.hasRole); + sinon.assert.calledOnce(mockUser.get); }); it('can edit author', function () { @@ -384,32 +372,26 @@ describe('Unit: models/user', function () { }); }); - it('can\'t destroy another editor', function (done) { + it('can\'t destroy another editor', async function () { const mockUser = getUserModel(3, 'Editor'); const context = {user: 2}; - models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true, true).then(() => { - done(new Error('Permissible function should have errored')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.called(mockUser.hasRole); - sinon.assert.calledOnce(mockUser.get); - done(); - }); + await assert.rejects(async () => { + await models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true, true); + }, errors.NoPermissionError); + sinon.assert.called(mockUser.hasRole); + sinon.assert.calledOnce(mockUser.get); }); - it('can\'t destroy an admin', function (done) { + it('can\'t destroy an admin', async function () { const mockUser = getUserModel(3, 'Administrator'); const context = {user: 2}; - models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true, true).then(() => { - done(new Error('Permissible function should have errored')); - }).catch((error) => { - assert(error instanceof errors.NoPermissionError); - sinon.assert.called(mockUser.hasRole); - sinon.assert.calledOnce(mockUser.get); - done(); - }); + await assert.rejects(async () => { + await models.User.permissible(mockUser, 'destroy', context, {}, testUtils.permissions.editor, true, true, true); + }, errors.NoPermissionError); + sinon.assert.called(mockUser.hasRole); + sinon.assert.calledOnce(mockUser.get); }); it('can destroy an author', function () { diff --git a/ghost/core/test/unit/server/services/permissions/can-this.test.js b/ghost/core/test/unit/server/services/permissions/can-this.test.js index f53f588a2f9..2b92e1470db 100644 --- a/ghost/core/test/unit/server/services/permissions/can-this.test.js +++ b/ghost/core/test/unit/server/services/permissions/can-this.test.js @@ -104,136 +104,114 @@ describe('Permissions', function () { // Permissions need to be NOT fundamentally baked into Ghost, but a separate module, at some point // It can depend on bookshelf, but should NOT use hard coded model knowledge. describe('with permissible calls (post model)', function () { - it('No context: does not allow edit post (no model)', function (done) { - permissions + it('No context: does not allow edit post (no model)', async function () { + await assert.rejects(permissions .canThis() // no context .edit - .post() // post id - .then(function () { - done(new Error('was able to edit post without permission')); - }) - .catch(function (err) { - assert.equal(err.errorType, 'NoPermissionError'); + .post(), // post id + function (err) { + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); - sinon.assert.notCalled(findPostSpy); - done(); - }); + sinon.assert.notCalled(findPostSpy); }); - it('No context: does not allow edit post (model syntax)', function (done) { - permissions + it('No context: does not allow edit post (model syntax)', async function () { + await assert.rejects(permissions .canThis() // no context .edit - .post({id: 1}) // post id in model syntax - .then(function () { - done(new Error('was able to edit post without permission')); - }) - .catch(function (err) { - assert.equal(err.errorType, 'NoPermissionError'); + .post({id: 1}), // post id in model syntax + function (err) { + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); - sinon.assert.calledOnce(findPostSpy); - assert.deepEqual(findPostSpy.firstCall.args[0], {id: 1, status: 'all'}); - done(); - }); + sinon.assert.calledOnce(findPostSpy); + assert.deepEqual(findPostSpy.firstCall.args[0], {id: 1, status: 'all'}); }); - it('No context: does not allow edit post (model ID syntax)', function (done) { - permissions + it('No context: does not allow edit post (model ID syntax)', async function () { + await assert.rejects(permissions .canThis({}) // no context .edit - .post(1) // post id using number syntax - .then(function () { - done(new Error('was able to edit post without permission')); - }) - .catch(function (err) { - assert.equal(err.errorType, 'NoPermissionError'); + .post(1), // post id using number syntax + function (err) { + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); - sinon.assert.calledOnce(findPostSpy); - assert.deepEqual(findPostSpy.firstCall.args[0], {id: 1, status: 'all'}); - done(); - }); + sinon.assert.calledOnce(findPostSpy); + assert.deepEqual(findPostSpy.firstCall.args[0], {id: 1, status: 'all'}); }); - it('Internal context: instantly grants permissions', function (done) { - permissions + it('Internal context: instantly grants permissions', async function () { + await permissions .canThis({internal: true}) // internal context .edit - .post({id: 1}) // post id - .then(function () { - // We don't get this far, permissions are instantly granted for internal - sinon.assert.notCalled(findPostSpy); - done(); - }) - .catch(function () { - done(new Error('Should allow editing post with { internal: true }')); - }); + .post({id: 1}); // post id + + // We don't get this far, permissions are instantly granted for internal + sinon.assert.notCalled(findPostSpy); }); - it('External context: does not grant permissions', function (done) { - permissions + it('External context: does not grant permissions', async function () { + await assert.rejects(permissions .canThis({external: true}) // internal context .edit - .post({id: 1}) // post id - .then(function () { - done(new Error('was able to edit post without permission')); - }) - .catch(function (err) { - assert.equal(err.errorType, 'NoPermissionError'); + .post({id: 1}), // post id + function (err) { + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); - sinon.assert.calledOnce(findPostSpy); - assert.deepEqual(findPostSpy.firstCall.args[0], {id: 1, status: 'all'}); - done(); - }); + sinon.assert.calledOnce(findPostSpy); + assert.deepEqual(findPostSpy.firstCall.args[0], {id: 1, status: 'all'}); }); }); describe('without permissible (tag model)', function () { - it('No context: does not allow edit tag (model syntax)', function (done) { - permissions + it('No context: does not allow edit tag (model syntax)', async function () { + await assert.rejects(permissions .canThis() // no context .edit - .tag({id: 1}) // tag id in model syntax - .then(function () { - done(new Error('was able to edit tag without permission')); - }) - .catch(function (err) { - assert.equal(err.errorType, 'NoPermissionError'); + .tag({id: 1}), // tag id in model syntax + function (err) { + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); - // We don't look up tags - sinon.assert.notCalled(findTagSpy); - done(); - }); + // We don't look up tags + sinon.assert.notCalled(findTagSpy); }); - it('Internal context: instantly grants permissions', function (done) { - permissions + it('Internal context: instantly grants permissions', async function () { + await permissions .canThis({internal: true}) // internal context .edit - .tag({id: 1}) // tag id - .then(function () { - // We don't look up tags - sinon.assert.notCalled(findTagSpy); - done(); - }) - .catch(function () { - done(new Error('Should allow editing post with { internal: true }')); - }); + .tag({id: 1}); // tag id + + // We don't look up tags + sinon.assert.notCalled(findTagSpy); }); - it('External context: does not grant permissions', function (done) { - permissions + it('External context: does not grant permissions', async function () { + await assert.rejects(permissions .canThis({external: true}) // external context .edit - .tag({id: 1}) // tag id - .then(function () { - done(new Error('was able to edit tag without permission')); - }) - .catch(function (err) { - assert.equal(err.errorType, 'NoPermissionError'); + .tag({id: 1}), // tag id + function (err) { + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); - sinon.assert.notCalled(findTagSpy); - done(); - }); + sinon.assert.notCalled(findTagSpy); }); }); }); @@ -243,7 +221,7 @@ describe('Permissions', function () { // Permissions need to be NOT fundamentally baked into Ghost, but a separate module, at some point // It can depend on bookshelf, but should NOT use hard coded model knowledge. // We use the tag model here because it doesn't have permissible, once that changes, these tests must also change - it('No permissions: cannot edit tag (no permissible function on model)', function (done) { + it('No permissions: cannot edit tag (no permissible function on model)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { // Fake the response from providers.user, which contains permissions and roles return Promise.resolve({ @@ -252,21 +230,19 @@ describe('Permissions', function () { }); }); - permissions + await assert.rejects(permissions .canThis({user: {}}) // user context .edit - .tag({id: 1}) // tag id in model syntax - .then(function () { - done(new Error('was able to edit tag without permission')); - }) - .catch(function (err) { - sinon.assert.calledOnce(userProviderStub); - assert.equal(err.errorType, 'NoPermissionError'); - done(); - }); + .tag({id: 1}), // tag id in model syntax + function (err) { + sinon.assert.calledOnce(userProviderStub); + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); }); - it('With permissions: can edit specific tag (no permissible function on model)', function (done) { + it('With permissions: can edit specific tag (no permissible function on model)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { // Fake the response from providers.user, which contains permissions and roles return Promise.resolve({ @@ -275,19 +251,16 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({user: {}}) // user context .edit - .tag({id: 1}) // tag id in model syntax - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(done); + .tag({id: 1}); // tag id in model syntax + + sinon.assert.calledOnce(userProviderStub); + assert.equal(res, undefined); }); - it('With permissions: can edit non-specific tag (no permissible function on model)', function (done) { + it('With permissions: can edit non-specific tag (no permissible function on model)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { // Fake the response from providers.user, which contains permissions and roles return Promise.resolve({ @@ -296,19 +269,16 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({user: {}}) // user context .edit - .tag() // tag id in model syntax - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(done); + .tag(); // tag id in model syntax + + sinon.assert.calledOnce(userProviderStub); + assert.equal(res, undefined); }); - it('With owner role: can edit tag (no permissible function on model)', function (done) { + it('With owner role: can edit tag (no permissible function on model)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { // Fake the response from providers.user, which contains permissions and roles return Promise.resolve({ @@ -318,16 +288,13 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({user: {}}) // user context .edit - .tag({id: 1}) // tag id in model syntax - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(done); + .tag({id: 1}); // tag id in model syntax + + sinon.assert.calledOnce(userProviderStub); + assert.equal(res, undefined); }); }); @@ -336,7 +303,7 @@ describe('Permissions', function () { // Permissions need to be NOT fundamentally baked into Ghost, but a separate module, at some point // It can depend on bookshelf, but should NOT use hard coded model knowledge. // We use the tag model here because it doesn't have permissible, once that changes, these tests must also change - it('With permissions: can edit non-specific tag (no permissible function on model)', function (done) { + it('With permissions: can edit non-specific tag (no permissible function on model)', async function () { const apiKeyProviderStub = sinon.stub(providers, 'apiKey').callsFake(() => { // Fake the response from providers.user, which contains permissions and roles return Promise.resolve({ @@ -345,17 +312,14 @@ describe('Permissions', function () { roles: [testUtils.DataGenerator.Content.roles[5]] }); }); - permissions.canThis({api_key: { + const res = await permissions.canThis({api_key: { id: 123 }}) // api key context .edit - .tag({id: 1}) // tag id in model syntax - .then(function (res) { - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(done); + .tag({id: 1}); // tag id in model syntax + + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(res, undefined); }); }); @@ -363,7 +327,7 @@ describe('Permissions', function () { // Tests for when both user and API key are present in context // This is the scenario introduced by staff API keys where a user can have an associated API key - it('Current behavior: User with permission + API key with permission (should pass with current logic)', function (done) { + it('Current behavior: User with permission + API key with permission (should pass with current logic)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: models.Permissions.forge(testUtils.DataGenerator.Content.permissions).models, @@ -378,23 +342,20 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(done); + .tag({id: 1}); + + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(res, undefined); }); - it('Fixed behavior: User with permission + API key without permission (now uses USER permission and passes)', function (done) { + it('Fixed behavior: User with permission + API key without permission (now uses USER permission and passes)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: models.Permissions.forge(testUtils.DataGenerator.Content.permissions).models, @@ -409,26 +370,21 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(res, undefined); - // Fixed: Now uses USER permission instead of API key logic - done(); - }) - .catch(function (err) { - done(new Error(`Should have passed using USER permissions, but failed with: ${err.message}`)); - }); + .tag({id: 1}); + + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(res, undefined); + // Fixed: Now uses USER permission instead of API key logic }); - it('Fixed behavior: User without permission + API key with permission (now uses USER permission and fails)', function (done) { + it('Fixed behavior: User without permission + API key with permission (now uses USER permission and fails)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: [], // User has no permissions @@ -443,26 +399,24 @@ describe('Permissions', function () { }); }); - permissions + await assert.rejects(permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function () { - done(new Error('Should have failed using USER permissions (ignoring API key permissions)')); - }) - .catch(function (err) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(err.errorType, 'NoPermissionError'); - // Fixed: Now uses USER permission instead of API key logic - done(); - }); + .tag({id: 1}), + function (err) { + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(err.errorType, 'NoPermissionError'); + // Fixed: Now uses USER permission instead of API key logic + return true; + } + ); }); - it('Current behavior: User without permission + API key without permission (should fail)', function (done) { + it('Current behavior: User without permission + API key without permission (should fail)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: [], @@ -477,25 +431,23 @@ describe('Permissions', function () { }); }); - permissions + await assert.rejects(permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function () { - done(new Error('Should have failed due to no permissions')); - }) - .catch(function (err) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(err.errorType, 'NoPermissionError'); - done(); - }); + .tag({id: 1}), + function (err) { + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); }); - it('Current behavior: Owner user + API key without permission (owner should override)', function (done) { + it('Current behavior: Owner user + API key without permission (owner should override)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: [], @@ -510,25 +462,22 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(done); + .tag({id: 1}); + + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(res, undefined); }); // Tests for NEW expected behavior after fix describe('Expected behavior after fix: User permissions should take precedence', function () { - it('Expected: User with permission + API key without permission (should use USER permission and pass)', function (done) { + it('Expected: User with permission + API key without permission (should use USER permission and pass)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: models.Permissions.forge(testUtils.DataGenerator.Content.permissions).models, @@ -543,25 +492,20 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(function (err) { - done(new Error(`Should have passed using USER permissions, but failed with: ${err.message}`)); - }); + .tag({id: 1}); + + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(res, undefined); }); - it('Expected: User without permission + API key with permission (should use USER permission and fail)', function (done) { + it('Expected: User without permission + API key with permission (should use USER permission and fail)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: [], // User has no permissions @@ -576,25 +520,23 @@ describe('Permissions', function () { }); }); - permissions + await assert.rejects(permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function () { - done(new Error('Should have failed using USER permissions (ignoring API key permissions)')); - }) - .catch(function (err) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(err.errorType, 'NoPermissionError'); - done(); - }); + .tag({id: 1}), + function (err) { + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(err.errorType, 'NoPermissionError'); + return true; + } + ); }); - it('Expected: Owner user + API key without permission (should use USER permission and pass)', function (done) { + it('Expected: Owner user + API key without permission (should use USER permission and pass)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { return Promise.resolve({ permissions: [], @@ -609,29 +551,24 @@ describe('Permissions', function () { }); }); - permissions + const res = await permissions .canThis({ user: {id: 1}, api_key: {id: 123, type: 'admin'} }) .edit - .tag({id: 1}) - .then(function (res) { - sinon.assert.calledOnce(userProviderStub); - sinon.assert.calledOnce(apiKeyProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(function (err) { - done(new Error(`Owner user should have permission regardless of API key, but failed with: ${err.message}`)); - }); + .tag({id: 1}); + + sinon.assert.calledOnce(userProviderStub); + sinon.assert.calledOnce(apiKeyProviderStub); + assert.equal(res, undefined); }); }); }); }); describe('permissible (overridden)', function () { - it('can use permissible function on model to forbid something (post model)', function (done) { + it('can use permissible function on model to forbid something (post model)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { // Fake the response from providers.user, which contains permissions and roles return Promise.resolve({ @@ -644,26 +581,24 @@ describe('Permissions', function () { return Promise.reject({message: 'Hello World!'}); }); - permissions + await assert.rejects(permissions .canThis({user: {}}) // user context .edit - .post({id: 1}) // tag id in model syntax - .then(function () { - done(new Error('was able to edit post without permission')); - }) - .catch(function (err) { - sinon.assert.calledOnce(permissibleStub); - sinon.assert.calledWith(permissibleStub, - 1, 'edit', sinon.match.object, sinon.match.object, sinon.match.object, true, true - ); - - sinon.assert.calledOnce(userProviderStub); - assert.equal(err.message, 'Hello World!'); - done(); - }); + .post({id: 1}), // tag id in model syntax + function (err) { + sinon.assert.calledOnce(permissibleStub); + sinon.assert.calledWith(permissibleStub, + 1, 'edit', sinon.match.object, sinon.match.object, sinon.match.object, true, true + ); + + sinon.assert.calledOnce(userProviderStub); + assert.equal(err.message, 'Hello World!'); + return true; + } + ); }); - it('can use permissible function on model to allow something (post model)', function (done) { + it('can use permissible function on model to allow something (post model)', async function () { const userProviderStub = sinon.stub(providers, 'user').callsFake(function () { // Fake the response from providers.user, which contains permissions and roles return Promise.resolve({ @@ -676,21 +611,18 @@ describe('Permissions', function () { return Promise.resolve(); }); - permissions + const res = await permissions .canThis({user: {}}) // user context .edit - .post({id: 1}) // tag id in model syntax - .then(function (res) { - sinon.assert.calledOnce(permissibleStub); - sinon.assert.calledWith(permissibleStub, - 1, 'edit', sinon.match.object, sinon.match.object, sinon.match.object, true, true - ); + .post({id: 1}); // tag id in model syntax - sinon.assert.calledOnce(userProviderStub); - assert.equal(res, undefined); - done(); - }) - .catch(done); + sinon.assert.calledOnce(permissibleStub); + sinon.assert.calledWith(permissibleStub, + 1, 'edit', sinon.match.object, sinon.match.object, sinon.match.object, true, true + ); + + sinon.assert.calledOnce(userProviderStub); + assert.equal(res, undefined); }); }); }); diff --git a/ghost/core/test/unit/server/services/permissions/index.test.js b/ghost/core/test/unit/server/services/permissions/index.test.js index 6ef2923cb7a..9aa949b84e4 100644 --- a/ghost/core/test/unit/server/services/permissions/index.test.js +++ b/ghost/core/test/unit/server/services/permissions/index.test.js @@ -73,42 +73,38 @@ describe('Permissions', function () { }); describe('Init (build actions map)', function () { - it('can load an actions map from existing permissions', function (done) { + it('can load an actions map from existing permissions', async function () { fakePermissions = loadFakePermissions(); - permissions.init().then(function (actions) { - assertExists(actions); + const actions = await permissions.init(); - assert.doesNotThrow(permissions.canThis); + assertExists(actions); - assert.deepEqual(_.keys(actions), ['browse', 'edit', 'add', 'destroy']); + assert.doesNotThrow(permissions.canThis); - assert.deepEqual(actions.browse, ['post']); - assert.deepEqual(actions.edit, ['post', 'tag', 'user', 'page']); - assert.deepEqual(actions.add, ['post', 'user', 'page']); - assert.deepEqual(actions.destroy, ['post', 'user']); + assert.deepEqual(_.keys(actions), ['browse', 'edit', 'add', 'destroy']); - done(); - }).catch(done); + assert.deepEqual(actions.browse, ['post']); + assert.deepEqual(actions.edit, ['post', 'tag', 'user', 'page']); + assert.deepEqual(actions.add, ['post', 'user', 'page']); + assert.deepEqual(actions.destroy, ['post', 'user']); }); - it('can load an actions map from existing permissions, and deduplicate', function (done) { + it('can load an actions map from existing permissions, and deduplicate', async function () { fakePermissions = loadFakePermissions({extra: true}); - permissions.init().then(function (actions) { - assertExists(actions); + const actions = await permissions.init(); - assert.doesNotThrow(permissions.canThis); + assertExists(actions); - assert.deepEqual(_.keys(actions), ['browse', 'edit', 'add', 'destroy']); + assert.doesNotThrow(permissions.canThis); - assert.deepEqual(actions.browse, ['post']); - assert.deepEqual(actions.edit, ['post', 'tag', 'user', 'page']); - assert.deepEqual(actions.add, ['post', 'user', 'page']); - assert.deepEqual(actions.destroy, ['post', 'user']); + assert.deepEqual(_.keys(actions), ['browse', 'edit', 'add', 'destroy']); - done(); - }).catch(done); + assert.deepEqual(actions.browse, ['post']); + assert.deepEqual(actions.edit, ['post', 'tag', 'user', 'page']); + assert.deepEqual(actions.add, ['post', 'user', 'page']); + assert.deepEqual(actions.destroy, ['post', 'user']); }); }); }); diff --git a/ghost/core/test/unit/server/services/permissions/providers.test.js b/ghost/core/test/unit/server/services/permissions/providers.test.js index c39cfc4e71b..9c7d31c8d1f 100644 --- a/ghost/core/test/unit/server/services/permissions/providers.test.js +++ b/ghost/core/test/unit/server/services/permissions/providers.test.js @@ -14,23 +14,20 @@ describe('Permission Providers', function () { }); describe('User', function () { - it('errors if user cannot be found', function (done) { + it('errors if user cannot be found', async function () { const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () { return Promise.resolve(); }); - providers.user(1) - .then(function () { - done(new Error('Should have thrown a user not found error')); - }) - .catch(function (err) { - sinon.assert.calledOnce(findUserSpy); - assert.equal(err.errorType, 'NotFoundError'); - done(); - }); + await assert.rejects(async () => { + await providers.user(1); + }, { + errorType: 'NotFoundError' + }); + sinon.assert.calledOnce(findUserSpy); }); - it('can load user with role, and permissions', function (done) { + it('can load user with role, and permissions', async function () { // This test requires quite a lot of unique setup work const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () { // Create a fake model @@ -56,38 +53,33 @@ describe('Permission Providers', function () { }); // Get permissions for the user - providers.user(1) - .then(function (res) { - sinon.assert.calledOnce(findUserSpy); - - assert(res && typeof res === 'object'); - assert('permissions' in res); - assert('roles' in res); - - assert(Array.isArray(res.permissions)); - assert.equal(res.permissions.length, 10); - assert(Array.isArray(res.roles)); - assert.equal(res.roles.length, 1); - - // @TODO fix this! - // Permissions is an array of models - // Roles is a JSON array - assert(res.permissions[0] && typeof res.permissions[0] === 'object'); - assert('attributes' in res.permissions[0]); - assert('id' in res.permissions[0]); - assert(res.roles[0] && typeof res.roles[0] === 'object'); - assert('id' in res.roles[0]); - assert('name' in res.roles[0]); - assert('description' in res.roles[0]); - assert(res.permissions[0] instanceof models.Base.Model); - assert(!(res.roles[0] instanceof models.Base.Model)); - - done(); - }) - .catch(done); + const res = await providers.user(1); + sinon.assert.calledOnce(findUserSpy); + + assert(res && typeof res === 'object'); + assert('permissions' in res); + assert('roles' in res); + + assert(Array.isArray(res.permissions)); + assert.equal(res.permissions.length, 10); + assert(Array.isArray(res.roles)); + assert.equal(res.roles.length, 1); + + // @TODO fix this! + // Permissions is an array of models + // Roles is a JSON array + assert(res.permissions[0] && typeof res.permissions[0] === 'object'); + assert('attributes' in res.permissions[0]); + assert('id' in res.permissions[0]); + assert(res.roles[0] && typeof res.roles[0] === 'object'); + assert('id' in res.roles[0]); + assert('name' in res.roles[0]); + assert('description' in res.roles[0]); + assert(res.permissions[0] instanceof models.Base.Model); + assert(!(res.roles[0] instanceof models.Base.Model)); }); - it('can load user with role, and role.permissions', function (done) { + it('can load user with role, and role.permissions', async function () { // This test requires quite a lot of unique setup work const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () { // Create a fake model @@ -115,38 +107,33 @@ describe('Permission Providers', function () { }); // Get permissions for the user - providers.user(1) - .then(function (res) { - sinon.assert.calledOnce(findUserSpy); - - assert(res && typeof res === 'object'); - assert('permissions' in res); - assert('roles' in res); - - assert(Array.isArray(res.permissions)); - assert.equal(res.permissions.length, 10); - assert(Array.isArray(res.roles)); - assert.equal(res.roles.length, 1); - - // @TODO fix this! - // Permissions is an array of models - // Roles is a JSON array - assert(res.permissions[0] && typeof res.permissions[0] === 'object'); - assert('attributes' in res.permissions[0]); - assert('id' in res.permissions[0]); - assert(res.roles[0] && typeof res.roles[0] === 'object'); - assert('id' in res.roles[0]); - assert('name' in res.roles[0]); - assert('description' in res.roles[0]); - assert(res.permissions[0] instanceof models.Base.Model); - assert(!(res.roles[0] instanceof models.Base.Model)); - - done(); - }) - .catch(done); + const res = await providers.user(1); + sinon.assert.calledOnce(findUserSpy); + + assert(res && typeof res === 'object'); + assert('permissions' in res); + assert('roles' in res); + + assert(Array.isArray(res.permissions)); + assert.equal(res.permissions.length, 10); + assert(Array.isArray(res.roles)); + assert.equal(res.roles.length, 1); + + // @TODO fix this! + // Permissions is an array of models + // Roles is a JSON array + assert(res.permissions[0] && typeof res.permissions[0] === 'object'); + assert('attributes' in res.permissions[0]); + assert('id' in res.permissions[0]); + assert(res.roles[0] && typeof res.roles[0] === 'object'); + assert('id' in res.roles[0]); + assert('name' in res.roles[0]); + assert('description' in res.roles[0]); + assert(res.permissions[0] instanceof models.Base.Model); + assert(!(res.roles[0] instanceof models.Base.Model)); }); - it('can load user with role, permissions and role.permissions and deduplicate them', function (done) { + it('can load user with role, permissions and role.permissions and deduplicate them', async function () { // This test requires quite a lot of unique setup work const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () { // Create a fake model @@ -175,38 +162,33 @@ describe('Permission Providers', function () { }); // Get permissions for the user - providers.user(1) - .then(function (res) { - sinon.assert.calledOnce(findUserSpy); - - assert(res && typeof res === 'object'); - assert('permissions' in res); - assert('roles' in res); - - assert(Array.isArray(res.permissions)); - assert.equal(res.permissions.length, 10); - assert(Array.isArray(res.roles)); - assert.equal(res.roles.length, 1); - - // @TODO fix this! - // Permissions is an array of models - // Roles is a JSON array - assert(res.permissions[0] && typeof res.permissions[0] === 'object'); - assert('attributes' in res.permissions[0]); - assert('id' in res.permissions[0]); - assert(res.roles[0] && typeof res.roles[0] === 'object'); - assert('id' in res.roles[0]); - assert('name' in res.roles[0]); - assert('description' in res.roles[0]); - assert(res.permissions[0] instanceof models.Base.Model); - assert(!(res.roles[0] instanceof models.Base.Model)); - - done(); - }) - .catch(done); + const res = await providers.user(1); + sinon.assert.calledOnce(findUserSpy); + + assert(res && typeof res === 'object'); + assert('permissions' in res); + assert('roles' in res); + + assert(Array.isArray(res.permissions)); + assert.equal(res.permissions.length, 10); + assert(Array.isArray(res.roles)); + assert.equal(res.roles.length, 1); + + // @TODO fix this! + // Permissions is an array of models + // Roles is a JSON array + assert(res.permissions[0] && typeof res.permissions[0] === 'object'); + assert('attributes' in res.permissions[0]); + assert('id' in res.permissions[0]); + assert(res.roles[0] && typeof res.roles[0] === 'object'); + assert('id' in res.roles[0]); + assert('name' in res.roles[0]); + assert('description' in res.roles[0]); + assert(res.permissions[0] instanceof models.Base.Model); + assert(!(res.roles[0] instanceof models.Base.Model)); }); - it('throws when user with non-active status is loaded', function (done) { + it('throws when user with non-active status is loaded', async function () { // This test requires quite a lot of unique setup work const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () { // Create a fake model @@ -217,33 +199,28 @@ describe('Permission Providers', function () { }); // Get permissions for the user - providers.user(1) - .then(function () { - done(new Error('Locked user should should throw an error')); - }) - .catch((err) => { - assert.equal(err.errorType, 'UnauthorizedError'); - sinon.assert.calledOnce(findUserSpy); - done(); - }); + await assert.rejects(async () => { + await providers.user(1); + }, { + errorType: 'UnauthorizedError' + }); + sinon.assert.calledOnce(findUserSpy); }); }); describe('API Key', function () { - it('errors if api_key cannot be found', function (done) { + it('errors if api_key cannot be found', async function () { let findApiKeySpy = sinon.stub(models.ApiKey, 'findOne'); findApiKeySpy.returns(Promise.resolve()); - providers.apiKey(1) - .then(() => { - done(new Error('Should have thrown an api key not found error')); - }) - .catch((err) => { - sinon.assert.calledOnce(findApiKeySpy); - assert.equal(err.errorType, 'NotFoundError'); - done(); - }); + + await assert.rejects(async () => { + await providers.apiKey(1); + }, { + errorType: 'NotFoundError' + }); + sinon.assert.calledOnce(findApiKeySpy); }); - it('can load api_key with role, and role.permissions', function (done) { + it('can load api_key with role, and role.permissions', async function () { const findApiKeySpy = sinon.stub(models.ApiKey, 'findOne').callsFake(function () { const fakeApiKey = models.ApiKey.forge(testUtils.DataGenerator.Content.api_keys[0]); const fakeAdminRole = models.Role.forge(testUtils.DataGenerator.Content.roles[0]); @@ -257,24 +234,22 @@ describe('Permission Providers', function () { fakeApiKey.withRelated = ['role', 'role.permissions']; return Promise.resolve(fakeApiKey); }); - providers.apiKey(1).then((res) => { - sinon.assert.calledOnce(findApiKeySpy); - assert(res && typeof res === 'object'); - assert('permissions' in res); - assert('roles' in res); - assert(Array.isArray(res.roles)); - assert.equal(res.roles.length, 1); - assert(res.permissions[0] && typeof res.permissions[0] === 'object'); - assert('attributes' in res.permissions[0]); - assert('id' in res.permissions[0]); - assert(res.roles[0] && typeof res.roles[0] === 'object'); - assert('id' in res.roles[0]); - assert('name' in res.roles[0]); - assert('description' in res.roles[0]); - assert(res.permissions[0] instanceof models.Base.Model); - assert(!(res.roles[0] instanceof models.Base.Model)); - done(); - }).catch(done); + const res = await providers.apiKey(1); + sinon.assert.calledOnce(findApiKeySpy); + assert(res && typeof res === 'object'); + assert('permissions' in res); + assert('roles' in res); + assert(Array.isArray(res.roles)); + assert.equal(res.roles.length, 1); + assert(res.permissions[0] && typeof res.permissions[0] === 'object'); + assert('attributes' in res.permissions[0]); + assert('id' in res.permissions[0]); + assert(res.roles[0] && typeof res.roles[0] === 'object'); + assert('id' in res.roles[0]); + assert('name' in res.roles[0]); + assert('description' in res.roles[0]); + assert(res.permissions[0] instanceof models.Base.Model); + assert(!(res.roles[0] instanceof models.Base.Model)); }); }); }); diff --git a/ghost/core/test/unit/server/services/url/url-service.test.js b/ghost/core/test/unit/server/services/url/url-service.test.js index 963cc0028a8..12d33f06fa1 100644 --- a/ghost/core/test/unit/server/services/url/url-service.test.js +++ b/ghost/core/test/unit/server/services/url/url-service.test.js @@ -70,20 +70,17 @@ describe('Unit: services/url/UrlService', function () { assert.equal(urlService.urlGenerators.length, 1); }); - it('fn: getResourceById', function (done) { + it('fn: getResourceById', function () { urlService.urls.getByResourceId.withArgs('id123').returns({resource: true}); assert.equal(urlService.getResourceById('id123'), true); urlService.urls.getByResourceId.withArgs('id12345').returns(null); - try { - assert.equal(urlService.getResourceById('id12345'), true); - done(new Error('expected error')); - } catch (err) { - assertExists(err); - assert.equal(err.code, 'URLSERVICE_RESOURCE_NOT_FOUND'); - done(); - } + assert.throws(() => { + urlService.getResourceById('id12345'); + }, { + code: 'URLSERVICE_RESOURCE_NOT_FOUND' + }); }); describe('fn: getResource', function () { From cd186f5dd9c9953eb7dbadaee38664905ca5713e Mon Sep 17 00:00:00 2001 From: Evan Hahn <evan@ghost.org> Date: Mon, 27 Apr 2026 15:18:07 -0500 Subject: [PATCH 10/15] Initialized email address service synchronously (#27556) ref https://github.com/TryGhost/Ghost/pull/27421#discussion_r3094858588 `emailAddressService.init()` was put in a `Promise.all` unnecessarily. Now it's just a regular function call. --- ghost/core/core/boot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/core/core/boot.js b/ghost/core/core/boot.js index c5d55c6b12b..e1874cffecb 100644 --- a/ghost/core/core/boot.js +++ b/ghost/core/core/boot.js @@ -359,12 +359,12 @@ async function initServices() { const urlUtils = require('./shared/url-utils'); // Initialize things that other services depend on first. + emailAddressService.init(); const apiUrl = urlUtils.urlFor('api', {type: 'admin'}, true); const schedulerAdapter = createSchedulerAdapter(); const [schedulerIntegration] = await Promise.all([ getSchedulerIntegration(), - stripe.init(), - emailAddressService.init() + stripe.init() ]); await Promise.all([ From 4091df06d4f5dac2f4adcb07e4e1c6e7463005ff Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 15:24:43 -0500 Subject: [PATCH 11/15] Added undici override to clear 5 advisories (2 high, 3 mod) (#27581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Adds a single override to root `pnpm.overrides`: ```json "undici@<6.24.0": "^6.24.0" ``` The vulnerable `undici@5.29.0` was being pulled in via: ``` ghost/core > @tryghost/metrics > @tryghost/elasticsearch > @elastic/transport > undici@5.29.0 ``` After the override, the chain resolves to `undici@6.24.1` (caret-pinned within the 6.x major). Other undici versions in the tree (`6.24.1` from `@actions/http-client`, `7.x` from `jsdom`) are unaffected — those consumers were already on safe ranges. ## On the 5 → 6 major bump This forces `@elastic/transport@8.4.1` to use undici 6 instead of 5. `@elastic/transport` is a **runtime path** — it executes when `@tryghost/metrics` ships log/metric data to elasticsearch. - `@elastic/transport` loads cleanly under undici 6 in unit tests (no require-time / load-time errors surfaced). - The undici 5 → 6 changes that affect consumers are mostly the removal of deprecated APIs and a Node-version bump (10+ → 18+); `ghost/core` requires Node 22+ so the engine constraint is satisfied. - `@elastic/transport` uses standard request/response APIs that haven't changed across the bump. The residual unknown is **actual elasticsearch traffic in production**. Local tests don't exercise live ES requests; staging / CI integration coverage is the place to catch this. The override is removable when `@tryghost/metrics` or `@elastic/transport` ships a release that declares `undici >= 6.24.0` directly. ## Audit delta `pnpm audit`: 114 → 109 (`−2 high`, `−3 moderate`). --- package.json | 1 + pnpm-lock.yaml | 18 ++---------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 0f8dfd3d901..156a789a008 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "qs@>=6.7.0 <=6.14.1": "^6.14.2", "tar@<7.5.11": "^7.5.11", "tmp@<=0.2.3": "^0.2.4", + "undici@<6.24.0": "^6.24.0", "@xmldom/xmldom@<0.8.13": "^0.8.13" }, "onlyBuiltDependencies": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6b05499b2e..31e3f1e578d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,7 @@ overrides: qs@>=6.7.0 <=6.14.1: ^6.14.2 tar@<7.5.11: ^7.5.11 tmp@<=0.2.3: ^0.2.4 + undici@<6.24.0: ^6.24.0 '@xmldom/xmldom@<0.8.13': ^0.8.13 importers: @@ -4744,10 +4745,6 @@ packages: resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} - '@fastify/busboy@2.1.1': - resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} - engines: {node: '>=14'} - '@fastify/otel@0.18.0': resolution: {integrity: sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==} peerDependencies: @@ -19153,7 +19150,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.14.2: @@ -21358,10 +21354,6 @@ packages: undici-types@7.24.7: resolution: {integrity: sha512-XA+gOBkzYD3C74sZowtCLTpgtaCdqZhqCvR6y9LXvrKTt/IVU6bz49T4D+BPi475scshCCkb0IklJRw6T1ZlgQ==} - undici@5.29.0: - resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} - engines: {node: '>=14.0'} - undici@6.24.1: resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} engines: {node: '>=18.17'} @@ -24245,7 +24237,7 @@ snapshots: ms: 2.1.3 secure-json-parse: 2.7.0 tslib: 2.8.1 - undici: 5.29.0 + undici: 6.24.1 transitivePeerDependencies: - supports-color @@ -25135,8 +25127,6 @@ snapshots: '@faker-js/faker@9.9.0': {} - '@fastify/busboy@2.1.1': {} - '@fastify/otel@0.18.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -47105,10 +47095,6 @@ snapshots: undici-types@7.24.7: {} - undici@5.29.0: - dependencies: - '@fastify/busboy': 2.1.1 - undici@6.24.1: {} undici@7.24.6: {} From 93f625b299b0591becc76de9aff90af9eed7e818 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 15:49:29 -0500 Subject: [PATCH 12/15] Fixed hoisted devDependency (#27583) no ref Two apps (`apps/announcement-bar`, `apps/sodo-search`) reference `concurrently` in their own `dev` scripts but never declared it as a `devDependency`. --- apps/announcement-bar/package.json | 3 +- apps/sodo-search/package.json | 3 +- package.json | 1 - pnpm-lock.yaml | 47 ++++++++++++++++-------------- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/apps/announcement-bar/package.json b/apps/announcement-bar/package.json index 8b8c168ae21..85a88ba34af 100644 --- a/apps/announcement-bar/package.json +++ b/apps/announcement-bar/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/announcement-bar", - "version": "1.1.17", + "version": "1.1.18", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -82,6 +82,7 @@ "devDependencies": { "@vitejs/plugin-react": "4.7.0", "@vitest/coverage-v8": "~3.2.4", + "concurrently": "8.2.2", "cross-fetch": "4.1.0", "jsdom": "28.1.0", "vite": "5.4.21", diff --git a/apps/sodo-search/package.json b/apps/sodo-search/package.json index 2d2e8d2ac49..1afdc6cdc85 100644 --- a/apps/sodo-search/package.json +++ b/apps/sodo-search/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/sodo-search", - "version": "1.8.15", + "version": "1.8.16", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -99,6 +99,7 @@ "@testing-library/react": "12.1.5", "@vitejs/plugin-react": "4.7.0", "@vitest/coverage-v8": "~3.2.4", + "concurrently": "8.2.2", "cross-fetch": "4.1.0", "jsdom": "28.1.0", "nock": "13.5.6", diff --git a/package.json b/package.json index 156a789a008..8ede6dc74c6 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,6 @@ "@playwright/test": "1.59.1", "chalk": "4.1.2", "chokidar": "3.6.0", - "concurrently": "8.2.2", "eslint": "catalog:", "eslint-plugin-ghost": "3.5.0", "eslint-plugin-react": "7.37.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31e3f1e578d..431c66c824e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,9 +67,6 @@ importers: chokidar: specifier: 3.6.0 version: 3.6.0 - concurrently: - specifier: 8.2.2 - version: 8.2.2 eslint: specifier: 'catalog:' version: 8.57.1 @@ -718,6 +715,9 @@ importers: '@vitest/coverage-v8': specifier: ~3.2.4 version: 3.2.4(vitest@3.2.4) + concurrently: + specifier: 8.2.2 + version: 8.2.2 cross-fetch: specifier: 4.1.0 version: 4.1.0(encoding@0.1.13) @@ -1387,6 +1387,9 @@ importers: '@vitest/coverage-v8': specifier: ~3.2.4 version: 3.2.4(vitest@3.2.4) + concurrently: + specifier: 8.2.2 + version: 8.2.2 cross-fetch: specifier: 4.1.0 version: 4.1.0(encoding@0.1.13) @@ -24518,7 +24521,7 @@ snapshots: js-string-escape: 1.0.1 jsdom: 16.7.0 json-stable-stringify: 1.3.0 - lodash: 4.18.1 + lodash: 4.17.23 pkg-up: 2.0.0 resolve: 1.22.11 resolve-package-path: 1.2.7 @@ -24581,7 +24584,7 @@ snapshots: babel-import-util: 3.0.1 ember-cli-babel: 7.26.11 find-up: 5.0.0 - lodash: 4.18.1 + lodash: 4.17.23 resolve: 1.22.11 semver: 7.7.4 transitivePeerDependencies: @@ -24591,7 +24594,7 @@ snapshots: dependencies: ember-rfc176-data: 0.3.18 fs-extra: 7.0.1 - lodash: 4.18.1 + lodash: 4.17.23 pkg-up: 3.1.0 resolve-package-path: 1.2.7 semver: 7.7.4 @@ -24603,7 +24606,7 @@ snapshots: ember-rfc176-data: 0.3.18 fs-extra: 9.1.0 js-string-escape: 1.0.1 - lodash: 4.18.1 + lodash: 4.17.23 resolve-package-path: 4.0.3 semver: 7.7.4 typescript-memoize: 1.1.1 @@ -24650,7 +24653,7 @@ snapshots: fs-extra: 9.1.0 is-subdir: 1.2.0 js-string-escape: 1.0.1 - lodash: 4.18.1 + lodash: 4.17.23 minimatch: 3.1.5 pkg-entry-points: 1.1.1 resolve-package-path: 4.0.3 @@ -31760,7 +31763,7 @@ snapshots: async@2.6.4: dependencies: - lodash: 4.18.1 + lodash: 4.17.23 async@3.2.3: {} @@ -31845,7 +31848,7 @@ snapshots: convert-source-map: 1.9.0 debug: 2.6.9(supports-color@1.2.0) json5: 0.5.1 - lodash: 4.18.1 + lodash: 4.17.23 minimatch: 3.1.5 path-is-absolute: 1.0.1 private: 0.1.8 @@ -31861,7 +31864,7 @@ snapshots: babel-types: 6.26.0 detect-indent: 4.0.0 jsesc: 1.3.0 - lodash: 4.18.1 + lodash: 4.17.23 source-map: 0.5.7 trim-right: 1.0.1 @@ -31893,7 +31896,7 @@ snapshots: babel-helper-function-name: 6.24.1 babel-runtime: 6.26.0 babel-types: 6.26.0 - lodash: 4.18.1 + lodash: 4.17.23 transitivePeerDependencies: - supports-color @@ -31934,7 +31937,7 @@ snapshots: dependencies: babel-runtime: 6.26.0 babel-types: 6.26.0 - lodash: 4.18.1 + lodash: 4.17.23 babel-helper-remap-async-to-generator@6.24.1: dependencies: @@ -32187,7 +32190,7 @@ snapshots: babel-template: 6.26.0 babel-traverse: 6.26.0 babel-types: 6.26.0 - lodash: 4.18.1 + lodash: 4.17.23 transitivePeerDependencies: - supports-color @@ -32412,7 +32415,7 @@ snapshots: babel-runtime: 6.26.0 core-js: 2.6.12 home-or-tmp: 2.0.0 - lodash: 4.18.1 + lodash: 4.17.23 mkdirp: 0.5.6 source-map-support: 0.4.18 transitivePeerDependencies: @@ -32443,7 +32446,7 @@ snapshots: debug: 2.6.9(supports-color@1.2.0) globals: 9.18.0 invariant: 2.2.4 - lodash: 4.18.1 + lodash: 4.17.23 transitivePeerDependencies: - supports-color @@ -32451,7 +32454,7 @@ snapshots: dependencies: babel-runtime: 6.26.0 esutils: 2.0.3 - lodash: 4.18.1 + lodash: 4.17.23 to-fast-properties: 1.0.3 babel6-plugin-strip-class-callcheck@6.0.0: {} @@ -34139,7 +34142,7 @@ snapshots: dependencies: chalk: 4.1.2 date-fns: 2.30.0 - lodash: 4.17.21 + lodash: 4.17.23 rxjs: 7.8.2 shell-quote: 1.8.3 spawn-command: 0.0.2 @@ -37186,7 +37189,7 @@ snapshots: esquery: 1.7.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 - lodash: 4.18.1 + lodash: 4.17.23 pluralize: 8.0.0 read-pkg-up: 7.0.1 regexp-tree: 0.1.27 @@ -39234,7 +39237,7 @@ snapshots: cli-width: 2.2.1 external-editor: 3.1.0 figures: 2.0.0 - lodash: 4.18.1 + lodash: 4.17.23 mute-stream: 0.0.7 run-async: 2.4.1 rxjs: 6.6.7 @@ -48069,7 +48072,7 @@ snapshots: whatwg-url@8.7.0: dependencies: - lodash: 4.18.1 + lodash: 4.17.23 tr46: 2.1.0 webidl-conversions: 6.1.0 @@ -48138,7 +48141,7 @@ snapshots: wide-align@1.1.5: dependencies: - string-width: 1.0.2 + string-width: 4.2.3 word-wrap@1.2.5: {} From c30d9404bb727255a8b4eef97bbfae0f7a058056 Mon Sep 17 00:00:00 2001 From: Evan Hahn <evan@ghost.org> Date: Mon, 27 Apr 2026 15:50:35 -0500 Subject: [PATCH 13/15] Used promises for async Handlebars function in tests (#27586) ref 30ab483ac3377b1e5f4e7ac4ed535e3740f73d9c This test-only cleanup should have no user impact. --- .../unit/frontend/helpers/cancel-link.test.js | 8 +++---- .../unit/frontend/helpers/content.test.js | 22 +++++++++---------- .../unit/frontend/helpers/navigation.test.js | 15 ++++++------- .../unit/frontend/helpers/pagination.test.js | 15 ++++++------- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/ghost/core/test/unit/frontend/helpers/cancel-link.test.js b/ghost/core/test/unit/frontend/helpers/cancel-link.test.js index 37804228494..d34117f035a 100644 --- a/ghost/core/test/unit/frontend/helpers/cancel-link.test.js +++ b/ghost/core/test/unit/frontend/helpers/cancel-link.test.js @@ -6,15 +6,15 @@ const cancel_link = require('../../../../core/frontend/helpers/cancel_link'); const labs = require('../../../../core/shared/labs'); const configUtils = require('../../../utils/config-utils'); const logging = require('@tryghost/logging'); +const {promisify} = require('node:util'); describe('{{cancel_link}} helper', function () { let labsStub; - before(function (done) { + before(async function () { hbs.express4({partialsDir: [configUtils.config.get('paths').helperTemplates]}); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); }); beforeEach(function () { diff --git a/ghost/core/test/unit/frontend/helpers/content.test.js b/ghost/core/test/unit/frontend/helpers/content.test.js index 3d7e0abd820..55ac93ee0c4 100644 --- a/ghost/core/test/unit/frontend/helpers/content.test.js +++ b/ghost/core/test/unit/frontend/helpers/content.test.js @@ -4,6 +4,7 @@ const sinon = require('sinon'); const hbs = require('../../../../core/frontend/services/theme-engine/engine'); const configUtils = require('../../../utils/config-utils'); const path = require('path'); +const {promisify} = require('node:util'); // Stuff we are testing const content = require('../../../../core/frontend/helpers/content'); @@ -13,12 +14,11 @@ const t = require('../../../../core/frontend/helpers/t'); const {setupI18nTest, initLocale} = require('../../../utils/i18n-test-utils'); describe('{{content}} helper', function () { - before(function (done) { + before(async function () { hbs.express4({partialsDir: [configUtils.config.get('paths').helperTemplates]}); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); }); it('renders empty string when null', function () { @@ -84,12 +84,11 @@ describe('{{content}} helper', function () { }); describe('{{content}} helper with no access', function () { - before(function (done) { + before(async function () { hbs.express4({partialsDir: [configUtils.config.get('paths').helperTemplates]}); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); hbs.registerHelper('has', has); hbs.registerHelper('is', is); @@ -222,12 +221,11 @@ describe('{{content}} helper with no access', function () { describe('{{content}} helper with custom template', function () { let optionsData; - before(function (done) { + before(async function () { hbs.express4({partialsDir: [path.resolve(__dirname, './test_tpl')]}); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); hbs.registerHelper('has', has); hbs.registerHelper('is', is); diff --git a/ghost/core/test/unit/frontend/helpers/navigation.test.js b/ghost/core/test/unit/frontend/helpers/navigation.test.js index b224e6ae11e..240d124cbb4 100644 --- a/ghost/core/test/unit/frontend/helpers/navigation.test.js +++ b/ghost/core/test/unit/frontend/helpers/navigation.test.js @@ -3,6 +3,7 @@ const {assertExists} = require('../../../utils/assertions'); const hbs = require('../../../../core/frontend/services/theme-engine/engine'); const configUtils = require('../../../utils/config-utils'); const path = require('path'); +const {promisify} = require('node:util'); const concat = require('../../../../core/frontend/helpers/concat'); const foreach = require('../../../../core/frontend/helpers/foreach'); @@ -18,14 +19,13 @@ const runHelperThunk = data => () => runHelper(data); describe('{{navigation}} helper', function () { let optionsData; - before(function (done) { + before(async function () { hbs.express4({ partialsDir: [configUtils.config.get('paths').helperTemplates] }); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); // The navigation partial expects this helper // @TODO: change to register with Ghost's own registration tools @@ -226,12 +226,11 @@ describe('{{navigation}} helper', function () { describe('{{navigation}} helper with custom template', function () { let optionsData; - before(function (done) { + before(async function () { hbs.express4({partialsDir: [path.resolve(__dirname, './test_tpl')]}); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); }); beforeEach(function () { diff --git a/ghost/core/test/unit/frontend/helpers/pagination.test.js b/ghost/core/test/unit/frontend/helpers/pagination.test.js index 9b7e4f2dcad..0336488d47b 100644 --- a/ghost/core/test/unit/frontend/helpers/pagination.test.js +++ b/ghost/core/test/unit/frontend/helpers/pagination.test.js @@ -4,18 +4,18 @@ const sinon = require('sinon'); const hbs = require('../../../../core/frontend/services/theme-engine/engine'); const configUtils = require('../../../utils/config-utils'); const path = require('path'); +const {promisify} = require('node:util'); const page_url = require('../../../../core/frontend/helpers/page_url'); const t = require('../../../../core/frontend/helpers/t'); const {setupI18nTest, initLocale} = require('../../../utils/i18n-test-utils'); const pagination = require('../../../../core/frontend/helpers/pagination'); describe('{{pagination}} helper', function () { - before(function (done) { + before(async function () { hbs.express4({partialsDir: [configUtils.config.get('paths').helperTemplates]}); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); // The pagination partial expects these helpers // @TODO: change to register with Ghost's own registration tools @@ -165,12 +165,11 @@ describe('{{pagination}} helper', function () { }); describe('{{pagination}} helper with custom template', function () { - before(function (done) { + before(async function () { hbs.express4({partialsDir: [path.resolve(__dirname, './test_tpl')]}); - hbs.cachePartials(function () { - done(); - }); + const cachePartials = promisify(hbs.cachePartials.bind(hbs)); + await cachePartials(); }); it('can render single page with @site.title', function () { From b98d0fd60b0d6015ce929e55618cf6bf90f1dd31 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 16:24:53 -0500 Subject: [PATCH 14/15] Bumped lodash to 4.18.1 in ghost/core + override (#27589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Two changes work together to clear all 3 lingering `lodash` advisories: 1. `ghost/core` direct dep: `lodash` `4.17.23` → `4.18.1` 2. Root `pnpm.overrides`: `"lodash@<4.18.0": "^4.18.0"` The direct edit alone was not enough — transitive consumers `@tryghost/limit-service` (in `admin-x-settings`) and `@testing-library/jest-dom` (in `admin-x-framework`) still pulled `lodash@4.17.x`, keeping the high and moderate advisories alive. The override forces every `lodash` consumer in the tree to `>=4.18.0`, collapsing the resolved tree to a single `lodash@4.18.1` instance. `lodash` 4.17 → 4.18 is a minor bump within the 4.x major; the API and function signatures are unchanged. The override is removable when `@tryghost/limit-service` and `@testing-library/jest-dom` each ship a release that declares `lodash >=4.18.0` directly. --- ghost/core/package.json | 2 +- package.json | 1 + pnpm-lock.yaml | 175 +++++++++++++++++++--------------------- 3 files changed, 85 insertions(+), 93 deletions(-) diff --git a/ghost/core/package.json b/ghost/core/package.json index 4ec704dd045..42836f85e70 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -201,7 +201,7 @@ "knex": "2.4.2", "knex-migrator": "5.3.2", "leaky-bucket": "2.2.0", - "lodash": "4.17.23", + "lodash": "4.18.1", "luxon": "3.7.2", "mailgun.js": "10.4.0", "metascraper": "5.45.15", diff --git a/package.json b/package.json index 8ede6dc74c6..70528140146 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "diff@<3.5.1": "^3.5.1", "diff@>=6.0.0 <8.0.3": "^8.0.3", "handlebars@>=4.0.0 <=4.7.8": "^4.7.9", + "lodash@<4.18.0": "^4.18.0", "minimatch@<3.1.4": "^3.1.4", "minimatch@>=9.0.0 <9.0.7": "^9.0.7", "qs@>=6.7.0 <=6.14.1": "^6.14.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 431c66c824e..f4d91b65dd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,7 @@ overrides: diff@<3.5.1: ^3.5.1 diff@>=6.0.0 <8.0.3: ^8.0.3 handlebars@>=4.0.0 <=4.7.8: ^4.7.9 + lodash@<4.18.0: ^4.18.0 minimatch@<3.1.4: ^3.1.4 minimatch@>=9.0.0 <9.0.7: ^9.0.7 qs@>=6.7.0 <=6.14.1: ^6.14.2 @@ -2081,7 +2082,7 @@ importers: version: 1.0.6 '@tryghost/nodemailer': specifier: 0.3.48 - version: 0.3.48(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8) + version: 0.3.48(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8) '@tryghost/nql': specifier: 0.12.10 version: 0.12.10 @@ -2308,8 +2309,8 @@ importers: specifier: 2.2.0 version: 2.2.0 lodash: - specifier: 4.17.23 - version: 4.17.23 + specifier: 4.18.1 + version: 4.18.1 luxon: specifier: 3.7.2 version: 3.7.2 @@ -11522,7 +11523,7 @@ packages: just: ^0.1.8 liquid-node: ^3.0.1 liquor: ^0.0.5 - lodash: ^4.17.20 + lodash: ^4.18.0 marko: ^3.14.4 mote: ^0.2.0 mustache: ^3.0.0 @@ -11686,7 +11687,7 @@ packages: just: ^0.1.8 liquid-node: ^3.0.1 liquor: ^0.0.5 - lodash: ^4.17.20 + lodash: ^4.18.0 mote: ^0.2.0 mustache: ^4.0.1 nunjucks: ^3.2.2 @@ -16634,12 +16635,6 @@ packages: lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} - lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -24521,7 +24516,7 @@ snapshots: js-string-escape: 1.0.1 jsdom: 16.7.0 json-stable-stringify: 1.3.0 - lodash: 4.17.23 + lodash: 4.18.1 pkg-up: 2.0.0 resolve: 1.22.11 resolve-package-path: 1.2.7 @@ -24571,7 +24566,7 @@ snapshots: babel-import-util: 2.1.1 ember-cli-babel: 7.26.11 find-up: 5.0.0 - lodash: 4.17.23 + lodash: 4.18.1 resolve: 1.22.11 semver: 7.7.4 transitivePeerDependencies: @@ -24584,7 +24579,7 @@ snapshots: babel-import-util: 3.0.1 ember-cli-babel: 7.26.11 find-up: 5.0.0 - lodash: 4.17.23 + lodash: 4.18.1 resolve: 1.22.11 semver: 7.7.4 transitivePeerDependencies: @@ -24594,7 +24589,7 @@ snapshots: dependencies: ember-rfc176-data: 0.3.18 fs-extra: 7.0.1 - lodash: 4.17.23 + lodash: 4.18.1 pkg-up: 3.1.0 resolve-package-path: 1.2.7 semver: 7.7.4 @@ -24606,7 +24601,7 @@ snapshots: ember-rfc176-data: 0.3.18 fs-extra: 9.1.0 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 resolve-package-path: 4.0.3 semver: 7.7.4 typescript-memoize: 1.1.1 @@ -24619,7 +24614,7 @@ snapshots: fs-extra: 9.1.0 is-subdir: 1.2.0 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 minimatch: 3.1.5 pkg-entry-points: 1.1.1 resolve-package-path: 4.0.3 @@ -24636,7 +24631,7 @@ snapshots: fs-extra: 9.1.0 is-subdir: 1.2.0 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 minimatch: 3.1.5 pkg-entry-points: 1.1.1 resolve-package-path: 4.0.3 @@ -24653,7 +24648,7 @@ snapshots: fs-extra: 9.1.0 is-subdir: 1.2.0 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 minimatch: 3.1.5 pkg-entry-points: 1.1.1 resolve-package-path: 4.0.3 @@ -25989,7 +25984,7 @@ snapshots: isostring: 0.0.1 jsdom: 27.4.0(@noble/hashes@1.8.0) jsonrepair: 3.13.3 - lodash: 4.17.23 + lodash: 4.18.1 memoize-one: 6.0.0 microsoft-capitalize: 1.0.7 mime: 3.0.0 @@ -29257,7 +29252,7 @@ snapshots: chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.5.16 - lodash: 4.17.21 + lodash: 4.18.1 redent: 3.0.0 '@testing-library/jest-dom@6.9.1': @@ -29400,7 +29395,7 @@ snapshots: '@tryghost/errors': 1.3.13 ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -29412,14 +29407,14 @@ snapshots: '@tryghost/tpl': 0.1.40 '@tryghost/validator': 0.2.22 json-stable-stringify: 1.3.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color '@tryghost/bookshelf-collision@2.0.3': dependencies: '@tryghost/errors': 1.3.13 - lodash: 4.17.23 + lodash: 4.18.1 moment-timezone: 0.5.45 transitivePeerDependencies: - supports-color @@ -29429,7 +29424,7 @@ snapshots: '@tryghost/bookshelf-eager-load@2.0.3': dependencies: '@tryghost/debug': 2.1.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -29445,26 +29440,26 @@ snapshots: '@tryghost/bookshelf-has-posts@2.1.0': dependencies: '@tryghost/debug': 2.1.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color '@tryghost/bookshelf-include-count@2.0.3': dependencies: '@tryghost/debug': 2.1.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color '@tryghost/bookshelf-order@2.0.3': dependencies: - lodash: 4.17.23 + lodash: 4.18.1 '@tryghost/bookshelf-pagination@2.0.3': dependencies: '@tryghost/errors': 1.3.13 '@tryghost/tpl': 2.1.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -29612,7 +29607,7 @@ snapshots: '@tryghost/html-to-plaintext@1.0.8': dependencies: html-to-text: 8.2.1 - lodash: 4.17.23 + lodash: 4.18.1 '@tryghost/http-cache-utils@0.1.25': {} @@ -29781,7 +29776,7 @@ snapshots: '@tryghost/limit-service@1.5.2': dependencies: '@tryghost/errors': 1.3.13 - lodash: 4.17.23 + lodash: 4.18.1 luxon: 3.7.2 transitivePeerDependencies: - supports-color @@ -29806,7 +29801,7 @@ snapshots: '@tryghost/members-csv@2.0.5': dependencies: fs-extra: 11.3.0 - lodash: 4.17.21 + lodash: 4.18.1 papaparse: 5.3.2 pump: 3.0.2 @@ -29834,7 +29829,7 @@ snapshots: '@tryghost/mongo-utils@0.6.3': dependencies: - lodash: 4.17.23 + lodash: 4.18.1 '@tryghost/mw-error-handler@1.0.13': dependencies: @@ -29842,20 +29837,20 @@ snapshots: '@tryghost/errors': 1.3.13 '@tryghost/http-cache-utils': 0.1.25 '@tryghost/tpl': 0.1.40 - lodash: 4.17.23 + lodash: 4.18.1 semver: 7.7.4 transitivePeerDependencies: - supports-color '@tryghost/mw-vhost@1.0.6': {} - '@tryghost/nodemailer@0.3.48(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8)': + '@tryghost/nodemailer@0.3.48(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8)': dependencies: '@aws-sdk/client-ses': 3.1018.0 '@tryghost/errors': 1.3.13 nodemailer: 6.10.1 nodemailer-direct-transport: 3.3.2 - nodemailer-mailgun-transport: 2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8) + nodemailer-mailgun-transport: 2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8) nodemailer-stub-transport: 1.1.0 transitivePeerDependencies: - arc-templates @@ -29941,7 +29936,7 @@ snapshots: '@tryghost/pretty-stream@0.2.5': dependencies: date-format: 4.0.14 - lodash: 4.17.23 + lodash: 4.18.1 prettyjson: 1.2.5 '@tryghost/pretty-stream@2.1.0': @@ -29975,7 +29970,7 @@ snapshots: '@tryghost/version': 0.1.38 cacheable-lookup: 7.0.0 got: 13.0.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -30038,7 +30033,7 @@ snapshots: '@tryghost/url-utils@5.1.2': dependencies: cheerio: 0.22.0 - lodash: 4.17.23 + lodash: 4.18.1 moment: 2.24.0 moment-timezone: 0.5.45 remark: 11.0.2 @@ -30059,7 +30054,7 @@ snapshots: dependencies: '@tryghost/errors': 1.3.13 '@tryghost/tpl': 0.1.40 - lodash: 4.17.23 + lodash: 4.18.1 moment-timezone: 0.5.45 validator: 7.2.0 transitivePeerDependencies: @@ -31521,7 +31516,7 @@ snapshots: applause@2.0.4: dependencies: - lodash: 4.17.23 + lodash: 4.18.1 optional-require: 1.1.10 optionalDependencies: cson-parser: 4.0.9 @@ -31538,7 +31533,7 @@ snapshots: graceful-fs: 4.2.11 is-stream: 2.0.1 lazystream: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 normalize-path: 3.0.0 readable-stream: 4.7.0 @@ -31763,7 +31758,7 @@ snapshots: async@2.6.4: dependencies: - lodash: 4.17.23 + lodash: 4.18.1 async@3.2.3: {} @@ -31848,7 +31843,7 @@ snapshots: convert-source-map: 1.9.0 debug: 2.6.9(supports-color@1.2.0) json5: 0.5.1 - lodash: 4.17.23 + lodash: 4.18.1 minimatch: 3.1.5 path-is-absolute: 1.0.1 private: 0.1.8 @@ -31864,7 +31859,7 @@ snapshots: babel-types: 6.26.0 detect-indent: 4.0.0 jsesc: 1.3.0 - lodash: 4.17.23 + lodash: 4.18.1 source-map: 0.5.7 trim-right: 1.0.1 @@ -31896,7 +31891,7 @@ snapshots: babel-helper-function-name: 6.24.1 babel-runtime: 6.26.0 babel-types: 6.26.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -31937,7 +31932,7 @@ snapshots: dependencies: babel-runtime: 6.26.0 babel-types: 6.26.0 - lodash: 4.17.23 + lodash: 4.18.1 babel-helper-remap-async-to-generator@6.24.1: dependencies: @@ -32190,7 +32185,7 @@ snapshots: babel-template: 6.26.0 babel-traverse: 6.26.0 babel-types: 6.26.0 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -32415,7 +32410,7 @@ snapshots: babel-runtime: 6.26.0 core-js: 2.6.12 home-or-tmp: 2.0.0 - lodash: 4.17.23 + lodash: 4.18.1 mkdirp: 0.5.6 source-map-support: 0.4.18 transitivePeerDependencies: @@ -32446,7 +32441,7 @@ snapshots: debug: 2.6.9(supports-color@1.2.0) globals: 9.18.0 invariant: 2.2.4 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -32454,7 +32449,7 @@ snapshots: dependencies: babel-runtime: 6.26.0 esutils: 2.0.3 - lodash: 4.17.23 + lodash: 4.18.1 to-fast-properties: 1.0.3 babel6-plugin-strip-class-callcheck@6.0.0: {} @@ -32648,7 +32643,7 @@ snapshots: '@tryghost/errors': 1.3.13 bluebird: 3.7.2 bookshelf: 1.2.0(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7)) - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -32658,7 +32653,7 @@ snapshots: create-error: 0.3.1 inflection: 1.13.4 knex: 2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) - lodash: 4.17.23 + lodash: 4.18.1 boolbase@1.0.0: {} @@ -32835,7 +32830,7 @@ snapshots: find-index: 1.1.1 fs-extra: 8.1.0 fs-tree-diff: 2.0.1 - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - supports-color @@ -34142,7 +34137,7 @@ snapshots: dependencies: chalk: 4.1.2 date-fns: 2.30.0 - lodash: 4.17.23 + lodash: 4.18.1 rxjs: 7.8.2 shell-quote: 1.8.3 spawn-command: 0.0.2 @@ -34194,20 +34189,20 @@ snapshots: ora: 3.4.0 through2: 3.0.2 - consolidate@0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8): + consolidate@0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8): dependencies: bluebird: 3.7.2 optionalDependencies: babel-core: 6.26.3 handlebars: 4.7.9 - lodash: 4.17.23 + lodash: 4.18.1 underscore: 1.13.8 - consolidate@1.0.4(@babel/core@7.29.0)(handlebars@4.7.9)(lodash@4.17.23)(mustache@4.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): + consolidate@1.0.4(@babel/core@7.29.0)(handlebars@4.7.9)(lodash@4.18.1)(mustache@4.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): optionalDependencies: '@babel/core': 7.29.0 handlebars: 4.7.9 - lodash: 4.17.23 + lodash: 4.18.1 mustache: 4.2.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -35333,7 +35328,7 @@ snapshots: handlebars: 4.7.9 is-subdir: 1.2.0 js-string-escape: 1.0.1 - lodash: 4.17.23 + lodash: 4.18.1 mini-css-extract-plugin: 2.10.2(webpack@5.105.4(@swc/core@1.15.21(@swc/helpers@0.5.21))) minimatch: 3.1.5 parse5: 6.0.1 @@ -35652,7 +35647,7 @@ snapshots: broccoli-merge-trees: 1.2.4 broccoli-source: 1.1.0 debug: 2.6.9(supports-color@1.2.0) - lodash: 4.17.23 + lodash: 4.18.1 resolve: 1.22.11 transitivePeerDependencies: - supports-color @@ -36536,7 +36531,7 @@ snapshots: console-ui: 3.1.2 ember-cli-babel: 7.26.11 ember-cli-htmlbars: 5.7.2 - lodash: 4.17.23 + lodash: 4.18.1 safe-stable-stringify: 2.5.0 transitivePeerDependencies: - '@glint/template' @@ -37075,7 +37070,7 @@ snapshots: eslint-plugin-i18next@6.1.3: dependencies: - lodash: 4.17.21 + lodash: 4.18.1 requireindex: 1.1.0 eslint-plugin-mocha@7.0.1(eslint@8.57.1): @@ -37189,7 +37184,7 @@ snapshots: esquery: 1.7.0 indent-string: 4.0.0 is-builtin-module: 3.2.1 - lodash: 4.17.23 + lodash: 4.18.1 pluralize: 8.0.0 read-pkg-up: 7.0.1 regexp-tree: 0.1.27 @@ -37541,7 +37536,7 @@ snapshots: express-hbs@2.5.0: dependencies: handlebars: 4.7.9 - lodash: 4.17.23 + lodash: 4.18.1 readdirp: 3.6.0 optionalDependencies: js-beautify: 1.15.4 @@ -38893,7 +38888,7 @@ snapshots: dependencies: he: 1.2.0 htmlparser2: 3.10.1 - lodash: 4.17.23 + lodash: 4.18.1 minimist: 1.2.8 html-to-text@8.2.1: @@ -39237,7 +39232,7 @@ snapshots: cli-width: 2.2.1 external-editor: 3.1.0 figures: 2.0.0 - lodash: 4.17.23 + lodash: 4.18.1 mute-stream: 0.0.7 run-async: 2.4.1 rxjs: 6.6.7 @@ -39269,7 +39264,7 @@ snapshots: cli-cursor: 3.1.0 cli-width: 3.0.0 figures: 3.2.0 - lodash: 4.17.23 + lodash: 4.18.1 mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 @@ -39289,7 +39284,7 @@ snapshots: cli-cursor: 3.1.0 cli-width: 3.0.0 figures: 3.2.0 - lodash: 4.17.23 + lodash: 4.18.1 mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 @@ -40684,7 +40679,7 @@ snapshots: compare-ver: 2.0.2 debug: 4.4.1 knex: 2.4.2(mysql2@3.14.1)(sqlite3@5.1.7) - lodash: 4.17.21 + lodash: 4.18.1 moment: 2.24.0 mysql2: 3.14.1 nconf: 0.12.1 @@ -40711,7 +40706,7 @@ snapshots: inherits: 2.0.4 interpret: 2.2.0 liftoff: 3.1.0 - lodash: 4.17.23 + lodash: 4.18.1 mkdirp: 0.5.6 pg-connection-string: 2.1.0 tarn: 2.0.0 @@ -40733,7 +40728,7 @@ snapshots: getopts: 2.2.5 interpret: 2.2.0 liftoff: 3.1.0 - lodash: 4.17.23 + lodash: 4.18.1 pg-connection-string: 2.4.0 tarn: 3.0.2 tildify: 2.0.0 @@ -40754,7 +40749,7 @@ snapshots: get-package-type: 0.1.0 getopts: 2.3.0 interpret: 2.2.0 - lodash: 4.17.23 + lodash: 4.18.1 pg-connection-string: 2.5.0 rechoir: 0.8.0 resolve-from: 5.0.0 @@ -40776,7 +40771,7 @@ snapshots: get-package-type: 0.1.0 getopts: 2.3.0 interpret: 2.2.0 - lodash: 4.17.23 + lodash: 4.18.1 pg-connection-string: 2.5.0 rechoir: 0.8.0 resolve-from: 5.0.0 @@ -40798,7 +40793,7 @@ snapshots: get-package-type: 0.1.0 getopts: 2.3.0 interpret: 2.2.0 - lodash: 4.17.21 + lodash: 4.18.1 pg-connection-string: 2.6.2 rechoir: 0.8.0 resolve-from: 5.0.0 @@ -41284,10 +41279,6 @@ snapshots: lodash.upperfirst@4.3.1: {} - lodash@4.17.21: {} - - lodash@4.17.23: {} - lodash@4.18.1: {} log-symbols@2.2.0: @@ -41675,7 +41666,7 @@ snapshots: dependencies: '@keyvhq/memoize': 2.0.3 '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) - lodash: 4.17.23 + lodash: 4.18.1 reachable-url: 1.7.2 transitivePeerDependencies: - '@noble/hashes' @@ -41687,7 +41678,7 @@ snapshots: metascraper-logo@5.45.10(@noble/hashes@1.8.0): dependencies: '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) - lodash: 4.17.23 + lodash: 4.18.1 transitivePeerDependencies: - '@noble/hashes' - bufferutil @@ -41729,7 +41720,7 @@ snapshots: dependencies: '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) cheerio: 1.0.0-rc.12 - lodash: 4.17.23 + lodash: 4.18.1 whoops: 4.1.8 transitivePeerDependencies: - '@noble/hashes' @@ -41936,7 +41927,7 @@ snapshots: dependencies: '@miragejs/pretender-node-polyfill': 0.1.2 inflected: 2.1.0 - lodash: 4.17.23 + lodash: 4.18.1 pretender: 3.4.7 mississippi@3.0.0: @@ -42038,7 +42029,7 @@ snapshots: dependencies: bluebird: 3.7.2 knex: 2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) - lodash: 4.17.23 + lodash: 4.18.1 semver: 5.7.2 module-details-from-path@1.0.4: {} @@ -42357,7 +42348,7 @@ snapshots: base64url: 3.0.1 buffer: 6.0.3 es6-promise: 4.2.8 - lodash: 4.17.23 + lodash: 4.18.1 long: 5.3.2 node-forge: 1.4.0 pako: 2.1.0 @@ -42418,9 +42409,9 @@ snapshots: nodemailer-fetch@1.6.0: {} - nodemailer-mailgun-transport@2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8): + nodemailer-mailgun-transport@2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8): dependencies: - consolidate: 0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8) + consolidate: 0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.8) form-data: 4.0.5 mailgun.js: 8.2.2 transitivePeerDependencies: @@ -44651,7 +44642,7 @@ snapshots: dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 - lodash: 4.17.21 + lodash: 4.18.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-is: 18.3.1 @@ -46538,14 +46529,14 @@ snapshots: charm: 1.0.2 commander: 2.20.3 compression: 1.8.1 - consolidate: 1.0.4(@babel/core@7.29.0)(handlebars@4.7.9)(lodash@4.17.23)(mustache@4.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) + consolidate: 1.0.4(@babel/core@7.29.0)(handlebars@4.7.9)(lodash@4.18.1)(mustache@4.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) execa: 9.6.1 express: 4.21.2 fireworm: 0.7.2 glob: 13.0.6 http-proxy: 1.18.1 js-yaml: 3.14.2 - lodash: 4.17.23 + lodash: 4.18.1 mkdirp: 3.0.1 mustache: 4.2.0 node-notifier: 10.0.1 @@ -48072,7 +48063,7 @@ snapshots: whatwg-url@8.7.0: dependencies: - lodash: 4.17.23 + lodash: 4.18.1 tr46: 2.1.0 webidl-conversions: 6.1.0 @@ -48329,7 +48320,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 '@types/lodash': 4.17.24 - lodash: 4.17.23 + lodash: 4.18.1 lodash-es: 4.18.1 nanoclone: 0.2.1 property-expr: 2.0.6 From 84e3d43a217c5b227d9ef41a7a91638e8ef98288 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 27 Apr 2026 16:27:43 -0500 Subject: [PATCH 15/15] Bumped dompurify to 3.4.1 across ghost/core, activitypub, portal (#27587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Bumps `dompurify` from a vulnerable `3.3.x` release to `3.4.1` (current latest) in the three workspaces that depend on it directly: - `ghost/core`: `3.3.0` → `3.4.1` - `apps/activitypub`: `3.3.1` → `3.4.1` - `apps/portal`: `3.3.1` → `3.4.1` Patch and minor versions within the dompurify 3.x line are backward-compatible; the `sanitize()` API and option shape are unchanged. --- apps/activitypub/package.json | 4 ++-- apps/portal/package.json | 4 ++-- ghost/core/package.json | 2 +- pnpm-lock.yaml | 27 ++++++++++----------------- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/apps/activitypub/package.json b/apps/activitypub/package.json index 02fcabc8358..8a828ecee4d 100644 --- a/apps/activitypub/package.json +++ b/apps/activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/activitypub", - "version": "3.1.13", + "version": "3.1.14", "license": "MIT", "repository": { "type": "git", @@ -82,7 +82,7 @@ "@tryghost/admin-x-framework": "workspace:*", "@tryghost/shade": "workspace:*", "clsx": "2.1.1", - "dompurify": "3.3.1", + "dompurify": "3.4.1", "html2canvas-objectfit-fix": "1.2.0", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/apps/portal/package.json b/apps/portal/package.json index bfeb6c41f32..f8b0c0251cc 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/portal", - "version": "2.68.18", + "version": "2.68.19", "license": "MIT", "repository": "https://github.com/TryGhost/Ghost", "author": "Ghost Foundation", @@ -121,7 +121,7 @@ "@vitest/ui": "3.2.4", "concurrently": "8.2.2", "cross-fetch": "4.1.0", - "dompurify": "3.3.1", + "dompurify": "3.4.1", "eslint": "catalog:", "eslint-plugin-i18next": "6.1.3", "jsdom": "28.1.0", diff --git a/ghost/core/package.json b/ghost/core/package.json index 42836f85e70..e5125234284 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -165,7 +165,7 @@ "csso": "5.0.5", "csv-writer": "1.6.0", "date-fns": "2.30.0", - "dompurify": "3.3.0", + "dompurify": "3.4.1", "downsize": "0.0.8", "entities": "4.5.0", "express": "4.21.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4d91b65dd9..2771d5cf820 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,8 +123,8 @@ importers: specifier: 2.1.1 version: 2.1.1 dompurify: - specifier: 3.3.1 - version: 3.3.1 + specifier: 3.4.1 + version: 3.4.1 html2canvas-objectfit-fix: specifier: 1.2.0 version: 1.2.0 @@ -902,8 +902,8 @@ importers: specifier: 4.1.0 version: 4.1.0(encoding@0.1.13) dompurify: - specifier: 3.3.1 - version: 3.3.1 + specifier: 3.4.1 + version: 3.4.1 eslint: specifier: 'catalog:' version: 8.57.1 @@ -2201,8 +2201,8 @@ importers: specifier: 2.30.0 version: 2.30.0 dompurify: - specifier: 3.3.0 - version: 3.3.0 + specifier: 3.4.1 + version: 3.4.1 downsize: specifier: 0.0.8 version: 0.0.8 @@ -12622,11 +12622,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.3.0: - resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} - - dompurify@3.3.1: - resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dompurify@3.4.1: + resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==} domutils@1.5.1: resolution: {integrity: sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==} @@ -30263,7 +30260,7 @@ snapshots: '@types/dompurify@3.2.0': dependencies: - dompurify: 3.3.1 + dompurify: 3.4.1 '@types/eslint-scope@3.7.7': dependencies: @@ -35094,11 +35091,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.3.0: - optionalDependencies: - '@types/trusted-types': 2.0.7 - - dompurify@3.3.1: + dompurify@3.4.1: optionalDependencies: '@types/trusted-types': 2.0.7