Releases and notable changes to this project are documented here.
+## [v1.3.5] - 2022-02-23
+
+### Added
+- IndieAuth profile logins now support latest spec, id est metadata discovery and issuer validation.
+- Topic content update history is now tracked.
+
+### Fixed
+
+- Fixed potential race condition which could cause a subscriber to miss an update.
+- Fixed postfix listener to more properly deal with errors.
+- Removed accidental logging of response body during HEAD requests.
+- Fixed session logout link on root page.
+- Dependency updates.
+
## [v1.3.4] - 2022-01-23
+
### Fixed
- Dependency updates.
---
-[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.2
+[Unreleased]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=HEAD;hp=v1.3.5
+[v1.3.5]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.5;hp=v1.3.4
+[v1.3.4]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.4;hp=v1.3.3
[v1.3.3]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.3;hp=v1.3.2
[v1.3.2]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.2;hp=v1.3.1
[v1.3.1]: https://git.squeep.com/?p=websub-hub;a=commitdiff;h=v1.3.1;hp=v1.3.0
- *.js - environment specific values, edit these as needed
- server.js - launches the application server
- src/
- - authenticator.js - interact with credentials and validation mechanisms
- common.js - utility functions
- communication.js - outgoing requests and associated logic
- db/
- logger.js - a very simple logging class
- manager.js - process incoming requests
- service.js - defines incoming endpoints, linking the API server framework to the manager methods
- - session-manager.js - process login/logout requests
- template/ - HTML content
- worker.js - maintains a pool of tasks in progress, for sending out updates, performing verifications, et cetera
- static/ - static assets
{
"name": "@squeep/websub-hub",
- "version": "1.3.4",
+ "version": "1.3.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
}
},
"@eslint/eslintrc": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz",
- "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz",
+ "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.2.0",
+ "espree": "^9.3.1",
"globals": "^13.9.0",
"ignore": "^4.0.6",
"import-fresh": "^3.2.1",
}
},
"@humanwhocodes/config-array": {
- "version": "0.9.2",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz",
- "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==",
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz",
+ "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
}
},
"@sinonjs/fake-timers": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz",
- "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.0.0.tgz",
+ "integrity": "sha512-+shXA2X7KNP7H7qNbQTJ3SA+NQc0pZDSBrdvFSRwF8sAo/ohw+ZQFD8Moc+gnz51+1eRXtEQBpKWPiQ4jsRC/w==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.7.0"
}
},
"@sinonjs/samsam": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz",
- "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz",
+ "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.6.0",
}
},
"@squeep/authentication-module": {
- "version": "git+https://git.squeep.com/squeep-authentication-module/#afa866e6bb4d5af652379295c9497c2caf4ecc92",
- "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.2",
+ "version": "git+https://git.squeep.com/squeep-authentication-module/#92658b114da01ab5537c53dee7ff5ad6385fe179",
+ "from": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.0",
"requires": {
"@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
- "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1",
- "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.2",
+ "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2",
+ "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.0",
"@squeep/mystery-box": "git+https://git.squeep.com/squeep-mystery-box/#v1.0.3",
- "argon2": "^0.28.3",
+ "argon2": "^0.28.4",
"node-linux-pam": "^0.2.1"
- },
- "dependencies": {
- "@squeep/html-template-helper": {
- "version": "git+https://git.squeep.com/squeep-html-template-helper#d3f76b9e76b8f133e8158c1087bb01b32c38d9bb",
- "from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.1"
- }
}
},
"@squeep/html-template-helper": {
"from": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2"
},
"@squeep/indieauth-helper": {
- "version": "git+https://git.squeep.com/squeep-indieauth-helper/#a15c4051aee22b76eca268e6a53b0944a9e40d0c",
- "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.2",
+ "version": "git+https://git.squeep.com/squeep-indieauth-helper/#7ece3489799b5349e22e95e3bd9fe7a30a985ebf",
+ "from": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.1.0",
"requires": {
"@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
- "axios": "^0.25.0",
+ "axios": "^0.26.0",
"iconv": "^3.0.1",
"microformats-parser": "^1.4.1"
}
}
},
"argon2": {
- "version": "0.28.3",
- "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz",
- "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==",
+ "version": "0.28.4",
+ "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.4.tgz",
+ "integrity": "sha512-WsfqiDp/tf5+eieLc1+S7RtO7Y3cAiZQ1F6GIaskENoJy/6xuCN5WGBIc8dG7QVPDavy6jUSads8zwZTtrHVag==",
"optional": true,
"requires": {
- "@mapbox/node-pre-gyp": "^1.0.7",
+ "@mapbox/node-pre-gyp": "^1.0.8",
"@phc/format": "^1.0.0",
- "node-addon-api": "^4.2.0",
+ "node-addon-api": "^4.3.0",
"opencollective-postinstall": "^2.0.3"
}
},
"optional": true
},
"axios": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
- "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
+ "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
"requires": {
- "follow-redirects": "^1.14.7"
+ "follow-redirects": "^1.14.8"
}
},
"balanced-match": {
}
},
"chokidar": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
- "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
+ "fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint": {
- "version": "8.7.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.7.0.tgz",
- "integrity": "sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz",
+ "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==",
"dev": true,
"requires": {
- "@eslint/eslintrc": "^1.0.5",
+ "@eslint/eslintrc": "^1.1.0",
"@humanwhocodes/config-array": "^0.9.2",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.1.0",
+ "eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
- "eslint-visitor-keys": "^3.2.0",
- "espree": "^9.3.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.1",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
}
},
"eslint-plugin-sonarjs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.11.0.tgz",
- "integrity": "sha512-ei/WuZiL0wP+qx2KrxKyZs3+eDbxiGAhFSm3GKCOOAUkg+G2ny6TSXDB2j67tvyqHefi+eoQsAgGQvz+nEtIBw==",
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.12.0.tgz",
+ "integrity": "sha512-soxjK67hoYxO8hesKqXWN50GSM+oG2r35N5WnAMehetahO6zoVpv3HZbdziP0jYWNopEe6te/BFUZOYAZI+qhg==",
"dev": true
},
"eslint-scope": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
- "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+ "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"dev": true,
"requires": {
"esrecurse": "^4.3.0",
}
},
"eslint-visitor-keys": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz",
- "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+ "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
"dev": true
},
"espree": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz",
- "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==",
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
+ "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==",
"dev": true,
"requires": {
"acorn": "^8.7.0",
"acorn-jsx": "^5.3.1",
- "eslint-visitor-keys": "^3.1.0"
+ "eslint-visitor-keys": "^3.3.0"
}
},
"esprima": {
}
},
"flatted": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz",
- "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true
},
"follow-redirects": {
- "version": "1.14.7",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
- "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
+ "version": "1.14.8",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
+ "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"foreground-child": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
}
},
"globals": {
- "version": "13.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz",
- "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==",
+ "version": "13.12.1",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz",
+ "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
"optional": true
},
"mocha": {
- "version": "9.1.4",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.4.tgz",
- "integrity": "sha512-+q2aV5VlJZuLgCWoBvGI5zEwPF9eEI0kr/sAA9Jm4xMND7RfIEyF8JE7C0JIg8WXRG+P1sdIAb5ccoHPlXLzcw==",
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.1.tgz",
+ "integrity": "sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ==",
"dev": true,
"requires": {
"@ungap/promise-all-settled": "1.1.2",
"ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
- "chokidar": "3.5.2",
- "debug": "4.3.2",
+ "chokidar": "3.5.3",
+ "debug": "4.3.3",
"diff": "5.0.0",
"escape-string-regexp": "4.0.0",
"find-up": "5.0.0",
- "glob": "7.1.7",
+ "glob": "7.2.0",
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "4.1.0",
"log-symbols": "4.1.0",
"minimatch": "3.0.4",
"ms": "2.1.3",
- "nanoid": "3.1.25",
+ "nanoid": "3.2.0",
"serialize-javascript": "6.0.0",
"strip-json-comments": "3.1.1",
"supports-color": "8.1.1",
"which": "2.0.2",
- "workerpool": "6.1.5",
+ "workerpool": "6.2.0",
"yargs": "16.2.0",
"yargs-parser": "20.2.4",
"yargs-unparser": "2.0.0"
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "debug": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ }
+ }
+ },
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true
},
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nanoid": {
- "version": "3.1.25",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
- "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
+ "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
"dev": true
},
"napi-build-utils": {
"dev": true
},
"nise": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz",
- "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz",
+ "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==",
"dev": true,
"requires": {
- "@sinonjs/commons": "^1.7.0",
- "@sinonjs/fake-timers": "^7.0.4",
+ "@sinonjs/commons": "^1.8.3",
+ "@sinonjs/fake-timers": ">=5",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"path-to-regexp": "^1.7.0"
- },
- "dependencies": {
- "@sinonjs/fake-timers": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
- "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
- "dev": true,
- "requires": {
- "@sinonjs/commons": "^1.7.0"
- }
- }
}
},
"no-case": {
}
},
"sinon": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz",
- "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==",
+ "version": "13.0.1",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.1.tgz",
+ "integrity": "sha512-8yx2wIvkBjIq/MGY1D9h1LMraYW+z1X0mb648KZnKSdvLasvDu7maa0dFaNYdTDczFgbjNw2tOmWdTk9saVfwQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.8.3",
- "@sinonjs/fake-timers": "^8.1.0",
- "@sinonjs/samsam": "^6.0.2",
+ "@sinonjs/fake-timers": "^9.0.0",
+ "@sinonjs/samsam": "^6.1.1",
"diff": "^5.0.0",
- "nise": "^5.1.0",
+ "nise": "^5.1.1",
"supports-color": "^7.2.0"
},
"dependencies": {
"dev": true
},
"workerpool": {
- "version": "6.1.5",
- "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz",
- "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz",
+ "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==",
"dev": true
},
"wrap-ansi": {
{
"name": "@squeep/websub-hub",
- "version": "1.3.4",
+ "version": "1.3.5",
"description": "A WebSub Hub server implementation.",
"main": "server.js",
"scripts": {
],
"dependencies": {
"@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.4",
- "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.1.2",
+ "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.2.0",
"@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.0.2",
- "@squeep/indieauth-helper": "git+https://git.squeep.com/squeep-indieauth-helper/#v1.0.2",
"@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
- "axios": "^0.25.0",
+ "axios": "^0.26.0",
"feedparser": "^2.2.10",
"htmlparser2": "^7.2.0",
"iconv": "^3.0.1"
"pg-promise": "^10.11.1"
},
"devDependencies": {
- "eslint": "^8.7.0",
+ "eslint": "^8.9.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-security": "^1.4.0",
- "eslint-plugin-sonarjs": "^0.11.0",
+ "eslint-plugin-sonarjs": "^0.12.0",
"html-minifier-lint": "^2.0.0",
- "mocha": "^9.1.4",
+ "mocha": "^9.2.1",
"mocha-steps": "^1.3.0",
"nyc": "^15.1.0",
"pre-commit": "^1.2.2",
- "sinon": "^12.0.1"
+ "sinon": "^13.0.1"
}
}
service.dispatch(req, res);
}).listen(PORT, ADDR, (err) => {
if (err) {
- logger.error(_scope, 'error starting server', err);
+ logger.error(_scope, 'error creating server', err);
throw err;
}
logger.info(_scope, 'server started', { version, listenAddress: ADDR, listenPort: PORT });
});
} catch (e) {
(logger || console).error(_scope, 'error starting server', e);
+ db && db.listener && await db.listener.stop();
}
})();
\ No newline at end of file
await this.db.transaction(dbCtx, async (txCtx) => {
await this.db.verificationInsert(txCtx, verification);
- await this.db.subscriptionDeliveryComplete(txCtx, subscription.callback, subscription.topicId);
+ await this.db.subscriptionDeliveryComplete(txCtx, subscription.callback, subscription.topicId, topic.contentUpdated);
});
this.logger.info(_scope, 'update unsubscription for deleted topic', logInfoData);
return;
return;
}
- await this.db.subscriptionDeliveryComplete(dbCtx, subscription.callback, subscription.topicId);
+ await this.db.subscriptionDeliveryComplete(dbCtx, subscription.callback, subscription.topicId, topic.contentUpdated);
this.logger.info(_scope, 'update success', logInfoData);
}
max: {
major: 1,
minor: 0,
- patch: 1,
+ patch: 3,
},
};
this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
for (const v of migrationsWanted) {
const fPath = path.join(__dirname, 'sql', 'schema', v, 'apply.sql');
- const migrationSql = _queryFile(fPath);
- const results = await this.db.multiResult(migrationSql);
- this.logger.debug(_scope, 'executed migration sql', { version: v, results });
- this.logger.info(_scope, 'applied migration', { version: v });
+ try {
+ const migrationSql = _queryFile(fPath);
+ this.logger.debug(_scope, 'applying migration', { version: v });
+ const results = await this.db.multiResult(migrationSql);
+ this.logger.debug(_scope, 'migration results', { results });
+ this.logger.info(_scope, 'applied migration', { version: v });
+ } catch (e) {
+ this.logger.error(_scope, 'migration failed', { error: e, fPath, version: v });
+ throw e;
+ }
}
}
}
- async subscriptionDeliveryComplete(dbCtx, callback, topicId) {
+ async subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated) {
const _scope = _fileScope('subscriptionDeliveryComplete');
- this.logger.debug(_scope, 'called', { callback, topicId });
+ this.logger.debug(_scope, 'called', { callback, topicId, topicContentUpdated });
let result;
try {
await dbCtx.txIf(async (txCtx) => {
- result = await txCtx.result(this.statement.subscriptionDeliverySuccess, { callback, topicId });
+ result = await txCtx.result(this.statement.subscriptionDeliverySuccess, { callback, topicId, topicContentUpdated });
if (result.rowCount != 1) {
throw new DBErrors.UnexpectedResult('did not set subscription delivery success');
}
}
});
} catch (e) {
- this.logger.error(_scope, 'failed', { error: e, callback, topicId });
+ this.logger.error(_scope, 'failed', { error: e, callback, topicId, topicContentUpdated });
throw e;
}
}
if (result.rowCount != 1) {
throw new DBErrors.UnexpectedResult('did not set topic content');
}
+ result = await dbCtx.result(this.statement.topicSetContentHistory, { topicId: data.topicId, contentHash: data.contentHash, contentSize: data.content.length });
+ if (result.rowCount != 1) {
+ throw new DBErrors.UnexpectedResult('did not set topic content history');
+ }
this.logger.debug(_scope, 'success', { ...logData });
return this._engineInfo(result);
} catch (e) {
delete this.reconnectPending;
}
if (this.connection) {
- this.connection.client.removeListener(this.notificationEventName, this.onNotificationBound);
+ this.connection.client.removeListener(this.notificationEventName, this._onNotificationBound);
this.connection.done();
this.connection = null;
await this.options.connectionLostCallback();
this.logger.error(_scope, 'listener connection lost', { error, event });
this.connection = null;
try {
- event.client.removeListener(this.notificationEventName, this.onNotificationBound);
+ event.client.removeListener(this.notificationEventName, this._onNotificationBound);
} catch (e) {
this.logger.error(_scope, 'failed to remove listener', { error: e });
// That's okay, it was probably just gone anyhow.
--- /dev/null
+BEGIN;
+ -- track when content was delivered as separate from latest content delivered
+ -- content_delivered continues to be the time the content was delivered, but becomes only informational
+ -- latest_content_delivered is date on topic content delivered
+ ALTER TABLE subscription
+ ADD COLUMN latest_content_delivered TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT '-infinity'::timestamptz
+ ;
+ CREATE INDEX subscription_latest_content_delivered_idx ON subscription(latest_content_delivered);
+ -- migrate existing values
+ UPDATE subscription SET latest_content_delivered = content_delivered;
+ -- no need for this index
+ DROP INDEX subscription_content_delivered_idx;
+ -- update the view to use latest_cotnent_delivered
+ CREATE OR REPLACE VIEW subscription_delivery_needed AS
+ SELECT s.*
+ FROM subscription s JOIN topic t ON s.topic_id = t.id
+ WHERE
+ s.expires > now()
+ AND
+ s.latest_content_delivered < t.content_updated
+ AND
+ s.delivery_next_attempt < now()
+ AND
+ s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+ ;
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 2);
+COMMIT;
--- /dev/null
+BEGIN;
+ DROP INDEX subscription_latest_content_delivered_idx;
+ CREATE INDEX subscription_content_delivered_idx ON subscription(content_delivered);
+ DROP VIEW subscription_delivery_needed;
+ ALTER TABLE subscription
+ DROP COLUMN latest_content_delivered
+ ;
+ CREATE OR REPLACE VIEW subscription_delivery_needed AS
+ SELECT s.*
+ FROM subscription s JOIN topic t ON s.topic_id = t.id
+ WHERE
+ s.expires > now()
+ AND
+ s.content_delivered < t.content_updated
+ AND
+ s.delivery_next_attempt < now()
+ AND
+ s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+ ;
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 2;
+COMMIT;
--- /dev/null
+BEGIN;
+ -- Track all content updates over time.
+ CREATE TABLE topic_content_history (
+ topic_id UUID NOT NULL REFERENCES topic(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ content_updated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
+ content_size INTEGER NOT NULL,
+ content_hash TEXT NOT NULL
+ );
+ CREATE INDEX topic_content_history_topic_id_idx ON topic_content_history(topic_id);
+ CREATE INDEX topic_content_history_content_updated_idx ON topic_content_history(content_updated);
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 3);
+COMMIT;
--- /dev/null
+BEGIN;
+ DROP INDEX topic_content_history_topic_id_idx;
+ DROP INDEX topic_content_history_content_updated_idx;
+ DROP TABLE topic_content_history;
+
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 3;
+COMMIT;
--
UPDATE subscription SET
content_delivered = now(),
+ latest_content_delivered = $(topicContentUpdated),
delivery_attempts_since_success = 0,
delivery_next_attempt = '-infinity'::timestamptz
WHERE
--- /dev/null
+--
+INSERT INTO topic_content_history
+ (topic_id, content_size, content_hash)
+VALUES
+ ($(topicId), $(contentSize), $(contentHash))
max: {
major: 1,
minor: 0,
- patch: 1,
+ patch: 3,
},
};
// max of signed int64 (2^63 - 1), should be enough
const EPOCH_FOREVER = BigInt('9223372036854775807');
+const epochToDate = (epoch) => new Date(Number(epoch) * 1000);
+const dateToEpoch = (date) => Math.round(date.getTime() / 1000);
class DatabaseSQLite extends Database {
constructor(logger, options) {
this.logger.debug(_scope, 'schema migrations wanted', { migrationsWanted });
migrationsWanted.forEach((v) => {
const fPath = path.join(__dirname, 'sql', 'schema', v, 'apply.sql');
- // eslint-disable-next-line security/detect-non-literal-fs-filename
- const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
- this.logger.info(_scope, 'applying migration', { version: v });
- this.db.exec(fSql);
+ try {
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
+ const fSql = fs.readFileSync(fPath, { encoding: 'utf8' });
+ this.logger.debug(_scope, 'applying migration', { version: v });
+ const results = this.db.exec(fSql);
+ this.logger.debug(_scope, 'migration results', { results });
+ this.logger.info(_scope, 'applied migration', { version: v });
+ } catch (e) {
+ this.logger.error(_scope, 'migration failed', { error: e, fPath, version: v });
+ throw e;
+ }
});
}
* @param {Object} data
*/
static _subscriptionDataToNative(data) {
- const epochToDate = (epoch) => new Date(Number(epoch) * 1000);
if (data) {
['created', 'verified', 'expires', 'contentDelivered'].forEach((field) => {
// eslint-disable-next-line security/detect-object-injection
}
- subscriptionDeliveryComplete(dbCtx, callback, topicId) {
+ subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated) {
const _scope = _fileScope('subscriptionDeliveryComplete');
- this.logger.debug(_scope, 'called', { callback, topicId });
+ this.logger.debug(_scope, 'called', { callback, topicId, topicContentUpdated });
let result;
try {
this.db.transaction(() => {
- result = this.statement.subscriptionDeliverySuccess.run({ callback, topicId });
+ topicContentUpdated = dateToEpoch(topicContentUpdated);
+ result = this.statement.subscriptionDeliverySuccess.run({ callback, topicId, topicContentUpdated });
if (result.changes != 1) {
throw new DBErrors.UnexpectedResult('did not set subscription delivery success');
}
})();
return this._engineInfo(result);
} catch (e) {
- this.logger.error(_scope, 'failed', { error: e, callback, topicId });
+ this.logger.error(_scope, 'failed', { error: e, callback, topicId, topicContentUpdated });
throw e;
}
}
* @param {Object} data
*/
static _topicDataToNative(data) {
- const epochToDate = (epoch) => new Date(Number(epoch) * 1000);
if (data) {
data.isActive = !!data.isActive;
data.isDeleted = !!data.isDeleted;
if (result.changes != 1) {
throw new DBErrors.UnexpectedResult('did not set topic content');
}
+ result = this.statement.topicSetContentHistory.run({ topicId: data.topicId, contentHash: data.contentHash, contentSize: data.content.length });
+ if (result.changes != 1) {
+ throw new DBErrors.UnexpectedResult('did not set topic content history');
+ }
return this._engineInfo(result);
} catch (e) {
this.logger.error(_scope, 'failed', { error: e, ...logData });
--- /dev/null
+BEGIN;
+ -- track when content was delivered as separate from latest content delivered
+ -- content_delivered continues to be the time the content was delivered, but becomes only informational
+ -- latest_content_delivered is date on topic content delivered
+ ALTER TABLE subscription
+ ADD COLUMN latest_content_delivered INTEGER NOT NULL DEFAULT 0
+ ;
+ CREATE INDEX subscription_latest_content_delivered_idx ON subscription(latest_content_delivered);
+ -- migrate existing values
+ UPDATE subscription SET latest_content_delivered = content_delivered;
+ -- no need for this index
+ DROP INDEX subscription_content_delivered_idx;
+ -- update the view to use latest_cotnent_delivered
+ DROP VIEW subscription_delivery_needed;
+ CREATE VIEW subscription_delivery_needed AS
+ SELECT s.*
+ FROM subscription s JOIN topic t ON s.topic_id = t.id
+ WHERE
+ s.expires > strftime('%s', 'now')
+ AND
+ s.latest_content_delivered < t.content_updated
+ AND
+ s.delivery_next_attempt < strftime('%s', 'now')
+ AND
+ s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+ ;
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 2);
+COMMIT;
--- /dev/null
+BEGIN;
+ DROP INDEX subscription_latest_content_delivered_idx;
+ DROP VIEW subscription_delivery_needed;
+ ALTER TABLE subscription
+ DROP COLUMN latest_content_delivered
+ ;
+ CREATE INDEX subscription_content_delivered_idx ON subscription(content_delivered);
+ CREATE VIEW subscription_delivery_needed AS
+ SELECT s.*
+ FROM subscription s JOIN topic t ON s.topic_id = t.id
+ WHERE
+ s.expires > strftime('%s', 'now')
+ AND
+ s.content_delivered < t.content_updated
+ AND
+ s.delivery_next_attempt < strftime('%s', 'now')
+ AND
+ s.id NOT IN (SELECT id FROM subscription_delivery_in_progress_active)
+ ;
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 2;
+COMMIT;
--- /dev/null
+BEGIN;
+ -- Track all content updates over time.
+ CREATE TABLE topic_content_history (
+ topic_id INTEGER NOT NULL REFERENCES topic(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ content_updated INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
+ content_size INTEGER NOT NULL,
+ content_hash TEXT NOT NULL
+ );
+ CREATE INDEX topic_content_history_topic_id_idx ON topic_content_history(topic_id);
+ CREATE INDEX topic_content_history_content_updated_idx ON topic_content_history(content_updated);
+
+ INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 0, 3);
+COMMIT;
--- /dev/null
+BEGIN;
+ DROP INDEX topic_content_history_topic_id_idx;
+ DROP INDEX topic_content_history_content_updated_idx;
+ DROP TABLE topic_content_history;
+
+ DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 0 AND patch = 3;
+COMMIT;
--
UPDATE subscription SET
content_delivered = strftime('%s', 'now'),
+ latest_content_delivered = :topicContentUpdated,
delivery_attempts_since_success = 0,
delivery_next_attempt = 0
WHERE
--- /dev/null
+--
+INSERT INTO topic_content_history
+ (topic_id, content_size, content_hash)
+VALUES
+ (:topicId, :contentSize, :contentHash)
}
+ /**
+ * Wrap the Dingus head handler, to remove the response body from the context,
+ * lest it be logged.
+ * @param {http.ClientRequest} req
+ * @param {http.ServerResponse} res
+ * @param {object} ctx
+ */
+ static setHeadHandler(req, res, ctx) {
+ if (req.method === 'HEAD') {
+ Dingus.setHeadHandler(req, res, ctx);
+ const origEnd = res.end.bind(res);
+ res.end = function (data, encoding, ...rest) {
+ const origResult = origEnd(data, encoding, ...rest);
+ delete ctx.responseBody;
+ return origResult;
+ };
+ }
+ }
/**
* @param {http.ClientRequest} req
];
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ Service.setHeadHandler(req, res, ctx);
this.setResponseType(responseTypes, req, res, ctx);
const _scope = _fileScope('handlerGetHealthcheck');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ Service.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG];
- Dingus.setHeadHandler(req, res, ctx);
+ Service.setHeadHandler(req, res, ctx);
this.setResponseType(responseTypes, req, res, ctx);
const _scope = _fileScope('handlerGetAdminOverview');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ Service.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const _scope = _fileScope('handlerGetAdminTopicDetails');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ Service.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const _scope = _fileScope('handlerGetAdminLogin');
this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx });
- Dingus.setHeadHandler(req, res, ctx);
+ Service.setHeadHandler(req, res, ctx);
this.setResponseType(this.responseTypes, req, res, ctx);
const th = require('./template-helper');
function hAppSection(pageTitle, logoUrl) {
- return ` <section class="h-app hidden">
+ return ` <section hidden class="h-app">
<h2>h-app Information for IndieAuth Logins</h2>
<img src="${logoUrl}" class="u-logo">
<a href="" class="u-url p-name">${pageTitle}</a>
contactSection(contactHTML),
hAppSection(pageTitle, options.manager.logoUrl),
];
- return th.htmlPage(1, ctx, htmlOptions, content);
+ return th.htmlPage(0, ctx, htmlOptions, content);
};
\ No newline at end of file
step('complete subscription', async function () {
const { callback } = testData.subscriptionUpsert;
await db.context(async (dbCtx) => {
- await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+ const topic = await db.topicGetById(dbCtx, topicId);
+ await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topic.contentUpdated);
const subscription = await db.subscriptionGetById(dbCtx, subscriptionId);
assert.strictEqual(Number(subscription.deliveryAttemptsSinceSuccess), 0);
});
});
it('covers migration', async function() {
sinon.stub(db.db, 'oneOrNone').resolves({});
- sinon.stub(db.db, 'multiResult');
- sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.max);
+ sinon.stub(db.db, 'multiResult').resolves({});
+ sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.min);
sinon.stub(db.db, 'one').resolves(db.schemaVersionsSupported.max);
await db.initialize();
});
+ it('covers migration failure', async function() {
+ const expected = new Error('oh no');
+ sinon.stub(db.db, 'oneOrNone').resolves({});
+ sinon.stub(db.db, 'multiResult').rejects(expected);
+ sinon.stub(db, '_currentSchema').resolves(db.schemaVersionsSupported.min);
+ sinon.stub(db.db, 'one').resolves(db.schemaVersionsSupported.max);
+ try {
+ await db.initialize();
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert.deepStrictEqual(e, expected);
+ }
+ });
it('covers listener', async function() {
db.listener = {
start: sinon.stub(),
}); // subscriptionDeliveryClaimById
describe('subscriptionDeliveryComplete', function () {
+ let topicContentUpdated;
+ before(function () {
+ topicContentUpdated = new Date();
+ });
it('success', async function() {
const dbResult = {
rowCount: 1,
};
sinon.stub(db.db, 'result').resolves(dbResult);
- await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+ await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
});
it('failure', async function () {
const dbResult = {
};
sinon.stub(db.db, 'result').onCall(0).resolves(dbResult);
try {
- await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+ await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
assert.fail(noExpectedException);
} catch (e) {
assert(e instanceof DBErrors.UnexpectedResult);
};
sinon.stub(db.db, 'result').onCall(0).resolves(dbResult0).onCall(1).resolves(dbResult1);
try {
- await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+ await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
assert.fail(noExpectedException);
} catch (e) {
assert(e instanceof DBErrors.UnexpectedResult);
contentType: 'text/plain',
contentHash: 'abc123',
};
+ sinon.stub(db.db, 'result');
});
it('success', async function() {
const dbResult = {
lastInsertRowid: undefined,
duration: 10,
};
- sinon.stub(db.db, 'result').resolves(dbResult);
+ db.db.result.resolves(dbResult);
const result = await db.topicSetContent(dbCtx, data);
assert.deepStrictEqual(result, expected);
});
rows: [],
duration: 10,
};
- sinon.stub(db.db, 'result').resolves(dbResult);
+ db.db.result.resolves(dbResult);
+ try {
+ await db.topicSetContent(dbCtx, data);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof DBErrors.UnexpectedResult);
+ }
+ });
+ it('failure 2', async function () {
+ const dbResultSuccess = {
+ rowCount: 1,
+ rows: [],
+ duration: 10,
+ };
+ const dbResultFail = {
+ rowCount: 0,
+ rows: [],
+ duration: 10,
+ };
+ db.db.result
+ .onCall(0).resolves(dbResultSuccess)
+ .onCall(1).resolves(dbResultFail);
try {
await db.topicSetContent(dbCtx, data);
assert.fail(noExpectedException);
});
}); // Implementation
+ describe('_initTables', function () {
+ let preparedGet;
+ beforeEach(function () {
+ preparedGet = sinon.stub();
+ sinon.stub(db.db, 'prepare').returns({
+ pluck: () => ({
+ bind: () => ({
+ get: preparedGet,
+ }),
+ }),
+ });
+ sinon.stub(db, '_currentSchema').returns(db.schemaVersionsSupported.min);
+ sinon.stub(db.db, 'exec');
+ });
+ it('covers migration', async function() {
+ preparedGet.returns({});
+ await db._initTables();
+ assert(db.db.exec.called);
+ });
+ it('covers migration failure', async function() {
+ const expected = new Error('oh no');
+ preparedGet.returns({});
+ db.db.exec.throws(expected);
+ try {
+ await db._initTables();
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert.deepStrictEqual(e, expected);
+ }
+ });
+ }); // _initTables
+
describe('_currentSchema', function () {
it('covers', async function () {
const version = { major: 1, minor: 0, patch: 0 };
}); // subscriptionDeliveryClaimById
describe('subscriptionDeliveryComplete', function () {
+ let topicContentUpdated;
+ before(function () {
+ topicContentUpdated = new Date();
+ });
it('success', async function() {
const dbResult = {
changes: 1,
};
sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult);
sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
- await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+ await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
});
it('failure', async function () {
const dbResult = {
sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult);
sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult);
try {
- await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+ await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
assert.fail(noExpectedException);
} catch (e) {
assert(e instanceof DBErrors.UnexpectedResult);
sinon.stub(db.statement.subscriptionDeliverySuccess, 'run').returns(dbResult0);
sinon.stub(db.statement.subscriptionDeliveryDone, 'run').returns(dbResult1);
try {
- await db.subscriptionDeliveryComplete(dbCtx, callback, topicId);
+ await db.subscriptionDeliveryComplete(dbCtx, callback, topicId, topicContentUpdated);
assert.fail(noExpectedException);
} catch (e) {
assert(e instanceof DBErrors.UnexpectedResult);
contentType: 'text/plain',
contentHash: 'abc123',
};
+ sinon.stub(db.statement.topicSetContent, 'run');
+ sinon.stub(db.statement.topicSetContentHistory, 'run');
});
it('success', async function() {
const dbResult = {
changes: 1,
lastInsertRowid: undefined,
};
- sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+ db.statement.topicSetContent.run.returns(dbResult);
+ db.statement.topicSetContentHistory.run.returns(dbResult);
const result = await db.topicSetContent(dbCtx, data);
assert.deepStrictEqual(result, expected);
});
changes: 0,
lastInsertRowid: undefined,
};
- sinon.stub(db.statement.topicSetContent, 'run').returns(dbResult);
+ db.statement.topicSetContent.run.returns(dbResult);
+ try {
+ await db.topicSetContent(dbCtx, data);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof DBErrors.UnexpectedResult);
+ }
+ });
+ it('failure 2', async function () {
+ const dbResultSuccess = {
+ changes: 1,
+ lastInsertRowid: undefined,
+ };
+ const dbResultFail = {
+ changes: 0,
+ lastInsertRowid: undefined,
+ };
+ db.statement.topicSetContent.run.returns(dbResultSuccess);
+ db.statement.topicSetContentHistory.run.returns(dbResultFail);
try {
await db.topicSetContent(dbCtx, data);
assert.fail(noExpectedException);
});
}); // maybeIngestBody
+ describe('setHeadHandler', function () {
+ it('covers', function () {
+ const origEnd = res.end;
+ sinon.stub(Service.__proto__, 'setHeadHandler');
+ ctx.responseBody = 'data';
+ req.method = 'HEAD';
+ Service.setHeadHandler(req, res, ctx);
+ res.end('foop');
+ assert(Service.__proto__.setHeadHandler.called);
+ assert(origEnd.called);
+ assert(!('responseBody' in ctx));
+ });
+ }); // setHeadHandler
+
describe('handlerPostRoot', function () {
it('covers public mode', async function () {
await service.handlerPostRoot(req, res, ctx);