From 04bdc348f943032554b359ed2efc6125b3856b0f Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sun, 17 Sep 2023 17:48:07 -0500 Subject: [PATCH 1/9] feat: Make datastore async/await. feat: throw error instead of calling process.exit in try/catch. Let the process fail on it's own. fix: Shell adapter wasn't closing the Readline stream properly. It was causing test scenarios to hang. fix: adapter.emote had a typo fix: call removeAllListeners in close method. fix: Response.play() wasn't working. fix: argument being sent to the catchAll callback was not correct. It's supposed to be a CatchAllMessage type. chore: Add @deprecated for Brain wrapper methods in Adapter chore: Remove is-circular chore: Replace mocha with Node's Test Runner chore: Remove chai, sinon and sinon-chai BREAKING CHANGE: Updating datastore to async/await API. --- package-lock.json | 621 --------------- package.json | 7 +- src/adapter.js | 24 +- src/adapters/shell.js | 27 +- src/brain.js | 1 + src/datastore.js | 53 +- src/datastores/memory.js | 6 +- src/response.js | 2 +- src/robot.js | 37 +- test/adapter_test.js | 120 +-- test/brain_test.js | 456 ++++++----- test/datastore_test.js | 189 ++--- test/es2015_test.js | 135 ++-- test/hubot_test.js | 14 +- test/listener_test.js | 341 ++++---- test/message_test.js | 38 +- test/middleware_test.js | 188 ++--- test/robot_test.js | 1594 ++++++++++++++++++++------------------ test/shell_test.js | 103 +-- test/user_test.js | 13 +- 20 files changed, 1698 insertions(+), 2271 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59720c8a6..969d9c79f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,12 +21,7 @@ "hubot": "bin/hubot" }, "devDependencies": { - "chai": "^4.3.7", - "is-circular": "^1.0.2", - "mocha": "^10.2.0", "semantic-release": "^21.0.1", - "sinon": "^15.0.4", - "sinon-chai": "^3.7.0", "standard": "^17.1.0" }, "engines": { @@ -835,50 +830,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1008,15 +959,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", @@ -1062,19 +1004,6 @@ "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", "dev": true }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1228,15 +1157,6 @@ "node": ">=0.10.0" } }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/asynciterator.prototype": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", @@ -1313,15 +1233,6 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -1399,15 +1310,6 @@ "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", "dev": true }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -1420,12 +1322,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1526,24 +1422,6 @@ "cdl": "bin/cdl.js" } }, - "node_modules/chai": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", - "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1572,42 +1450,6 @@ "node": ">=8" } }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/clean-stack": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", @@ -1658,17 +1500,6 @@ "node": ">=0.8.x" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/coffeescript": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.7.0.tgz", @@ -1972,18 +1803,6 @@ "node": ">=0.10.0" } }, - "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2053,15 +1872,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3323,15 +3133,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", @@ -3443,20 +3244,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -3498,15 +3285,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -3831,15 +3609,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, "node_modules/hook-std": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", @@ -4184,18 +3953,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -4224,12 +3981,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-circular": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-circular/-/is-circular-1.0.2.tgz", - "integrity": "sha512-YttjnrswnUYRVJvxCvu8z+PGMUSzC2JttP0OEXezlAEdp3EXzhf7IZ3j0gRAybJBQupedIZFhY61Tga6E0qASA==", - "dev": true - }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -4501,18 +4252,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -4715,12 +4454,6 @@ "node": ">=4.0" } }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -4825,12 +4558,6 @@ "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "dev": true }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -4861,22 +4588,6 @@ "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", "dev": true }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4889,15 +4600,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" - } - }, "node_modules/lru-cache": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", @@ -5230,18 +4932,6 @@ "node": ">=4" } }, - "node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -5265,75 +4955,6 @@ "node": ">= 6" } }, - "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/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 - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", @@ -5400,18 +5021,6 @@ "node": ">=0.6" } }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5438,43 +5047,6 @@ "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", "dev": true }, - "node_modules/nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^10.0.2", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, - "node_modules/nise/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/nise/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -5523,15 +5095,6 @@ "node": ">=10" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-url": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", @@ -9096,15 +8659,6 @@ "node": ">=8" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -9357,15 +8911,6 @@ "node": ">= 0.8" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9661,18 +9206,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -10153,15 +9686,6 @@ "node": ">=0.6" } }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -10338,55 +9862,6 @@ "node": ">=4" } }, - "node_modules/sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.3.0", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.4", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon-chai": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", - "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", - "dev": true, - "peerDependencies": { - "chai": "^4.0.0", - "sinon": ">=4.0.0" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/sinon/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -10859,21 +10334,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/supports-hyperlinks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", @@ -11090,15 +10550,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", @@ -11421,12 +10872,6 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, - "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -11483,24 +10928,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yargs-parser": { "version": "20.2.4", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", @@ -11510,54 +10937,6 @@ "node": ">=10" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 04453015e..afc9e3c94 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,7 @@ "pino": "^8.11.0" }, "devDependencies": { - "chai": "^4.3.7", - "is-circular": "^1.0.2", - "mocha": "^10.2.0", "semantic-release": "^21.0.1", - "sinon": "^15.0.4", - "sinon-chai": "^3.7.0", "standard": "^17.1.0" }, "engines": { @@ -43,7 +38,7 @@ "scripts": { "start": "bin/hubot", "pretest": "standard", - "test": "mocha --exit", + "test": "node --test --experimental-test-coverage", "test:smoke": "node src/**/*.js", "test:e2e": "bin/e2e-test.sh" }, diff --git a/src/adapter.js b/src/adapter.js index 6b3edd06f..4675db9f9 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -27,7 +27,7 @@ class Adapter extends EventEmitter { // // Returns results from adapter. async emote (envelope, ...strings) { - return this.senda(envelope, ...strings) + return this.send(envelope, ...strings) } // Public: Raw method for building a reply and sending it back to the chat @@ -63,7 +63,9 @@ class Adapter extends EventEmitter { // Public: Raw method for shutting the bot down. Extend this. // // Returns nothing. - close () {} + close () { + this.removeAllListeners() + } // Public: Dispatch a received message to the robot. // @@ -75,24 +77,27 @@ class Adapter extends EventEmitter { // Public: Get an Array of User objects stored in the brain. // // Returns an Array of User objects. + // @deprecated Use @robot.brain users () { - this.robot.logger.warning('@users() is going to be deprecated in 3.0.0 use @robot.brain.users()') + this.robot.logger.warning('@users() is going to be deprecated in 11.0.0 use @robot.brain.users()') return this.robot.brain.users() } // Public: Get a User object given a unique identifier. // // Returns a User instance of the specified user. + // @deprecated Use @robot.brain userForId (id, options) { - this.robot.logger.warning('@userForId() is going to be deprecated in 3.0.0 use @robot.brain.userForId()') + this.robot.logger.warning('@userForId() is going to be deprecated in 11.0.0 use @robot.brain.userForId()') return this.robot.brain.userForId(id, options) } // Public: Get a User object given a name. // // Returns a User instance for the user with the specified name. + // @deprecated Use @robot.brain userForName (name) { - this.robot.logger.warning('@userForName() is going to be deprecated in 3.0.0 use @robot.brain.userForName()') + this.robot.logger.warning('@userForName() is going to be deprecated in 11.0.0 use @robot.brain.userForName()') return this.robot.brain.userForName(name) } @@ -101,8 +106,9 @@ class Adapter extends EventEmitter { // nicknames, etc. // // Returns an Array of User instances matching the fuzzy name. + // @deprecated Use @robot.brain usersForRawFuzzyName (fuzzyName) { - this.robot.logger.warning('@userForRawFuzzyName() is going to be deprecated in 3.0.0 use @robot.brain.userForRawFuzzyName()') + this.robot.logger.warning('@userForRawFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForRawFuzzyName()') return this.robot.brain.usersForRawFuzzyName(fuzzyName) } @@ -111,8 +117,9 @@ class Adapter extends EventEmitter { // fuzzyName is a raw fuzzy match (see usersForRawFuzzyName). // // Returns an Array of User instances matching the fuzzy name. + // @deprecated Use @robot.brain usersForFuzzyName (fuzzyName) { - this.robot.logger.warning('@userForFuzzyName() is going to be deprecated in 3.0.0 use @robot.brain.userForFuzzyName()') + this.robot.logger.warning('@userForFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForFuzzyName()') return this.robot.brain.usersForFuzzyName(fuzzyName) } @@ -122,8 +129,9 @@ class Adapter extends EventEmitter { // send the request. // // Returns a ScopedClient instance. + // @deprecated Use node.js fetch. http (url) { - this.robot.logger.warning('@http() is going to be deprecated in 3.0.0 use @robot.http()') + this.robot.logger.warning('@http() is going to be deprecated in 11.0.0 use @robot.http()') return this.robot.http(url) } } diff --git a/src/adapters/shell.js b/src/adapters/shell.js index 0d6ce7ba9..21545fded 100644 --- a/src/adapters/shell.js +++ b/src/adapters/shell.js @@ -17,6 +17,7 @@ const historyPath = '.hubot_history' const bold = str => `\x1b[1m${str}\x1b[22m` class Shell extends Adapter { + #rl = null constructor (robot) { super(robot) this.name = 'Shell' @@ -37,19 +38,22 @@ class Shell extends Adapter { run () { this.buildCli() - loadHistory((error, history) => { + + this.#rl = loadHistory((error, history) => { if (error) { - console.log(error.message) + console.error(error) } this.cli.history(history) - this.cli.interact(`${this.robot.name}> `) + this.cli.interact(`${this.robot.name ?? this.robot.alias}> `) return this.emit('connected', this) }) } - shutdown () { - this.robot.shutdown() - return process.exit(0) + close () { + super.close() + this.#rl.close() + this.cli.removeAllListeners() + this.cli.close() } buildCli () { @@ -86,7 +90,7 @@ class Shell extends Adapter { history = this.cli.history() if (history.length <= historySize) { - return this.shutdown() + return } const startIndex = history.length - historySize @@ -96,11 +100,11 @@ class Shell extends Adapter { } const outstream = fs.createWriteStream(historyPath, fileOpts) - outstream.on('end', this.shutdown.bind(this)) for (i = 0, len = history.length; i < len; i++) { item = history[i] outstream.write(item + '\n') } + outstream.end() }) } } @@ -124,13 +128,16 @@ function loadHistory (callback) { const items = [] - readline.createInterface({ input: instream, output: outstream, terminal: false }) + const rl = readline.createInterface({ input: instream, output: outstream, terminal: false }) .on('line', function (line) { line = line.trim() if (line.length > 0) { items.push(line) } }) - .on('close', () => callback(null, items)) + .on('close', () => { + callback(null, items) + }) .on('error', callback) + return rl } diff --git a/src/brain.js b/src/brain.js index 92b72a45b..c063977c7 100644 --- a/src/brain.js +++ b/src/brain.js @@ -109,6 +109,7 @@ class Brain extends EventEmitter { clearInterval(this.saveInterval) this.save() this.emit('close') + this.removeAllListeners() } // Public: Enable or disable the automatic saving diff --git a/src/datastore.js b/src/datastore.js index 6a0d73d8f..57fab9ab5 100644 --- a/src/datastore.js +++ b/src/datastore.js @@ -13,52 +13,49 @@ class DataStore { // write has completed. // // Value can be any JSON-serializable type. - set (key, value) { - return this._set(key, value, 'global') + async set (key, value) { + return await this._set(key, value, 'global') } // Public: Assuming `key` represents an object in the database, // sets its `objectKey` to `value`. If `key` isn't already // present, it's instantiated as an empty object. - setObject (key, objectKey, value) { - return this.get(key).then((object) => { - const target = object || {} - target[objectKey] = value - return this.set(key, target) - }) + async setObject (key, objectKey, value) { + const object = await this.get(key) + const target = object || {} + target[objectKey] = value + return await this.set(key, target) } // Public: Adds the supplied value(s) to the end of the existing // array in the database marked by `key`. If `key` isn't already // present, it's instantiated as an empty array. - setArray (key, value) { - return this.get(key).then((object) => { - const target = object || [] - // Extend the array if the value is also an array, otherwise - // push the single value on the end. - if (Array.isArray(value)) { - return this.set(key, target.push.apply(target, value)) - } else { - return this.set(key, target.concat(value)) - } - }) + async setArray (key, value) { + const object = await this.get(key) + const target = object ?? [] + // Extend the array if the value is also an array, otherwise + // push the single value on the end. + if (Array.isArray(value)) { + return await this.set(key, target.concat(value)) + } else { + return await this.set(key, target.concat([value])) + } } // Public: Get value by key if in the database or return `undefined` // if not found. Returns a promise which resolves to the // requested value. - get (key) { - return this._get(key, 'global') + async get (key) { + return await this._get(key, 'global') } // Public: Digs inside the object at `key` for a key named // `objectKey`. If `key` isn't already present, or if it doesn't // contain an `objectKey`, returns `undefined`. - getObject (key, objectKey) { - return this.get(key).then((object) => { - const target = object || {} - return target[objectKey] - }) + async getObject (key, objectKey) { + const object = await this.get(key) + const target = object || {} + return target[objectKey] } // Private: Implements the underlying `set` logic for the datastore. @@ -70,7 +67,7 @@ class DataStore { // This returns a resolved promise when the `set` operation is // successful, and a rejected promise if the operation fails. _set (key, value, table) { - return Promise.reject(new DataStoreUnavailable('Setter called on the abstract class.')) + throw new DataStoreUnavailable('Setter called on the abstract class.') } // Private: Implements the underlying `get` logic for the datastore. @@ -82,7 +79,7 @@ class DataStore { // This returns a resolved promise containing the fetched value on // success, and a rejected promise if the operation fails. _get (key, table) { - return Promise.reject(new DataStoreUnavailable('Getter called on the abstract class.')) + throw new DataStoreUnavailable('Getter called on the abstract class.') } } diff --git a/src/datastores/memory.js b/src/datastores/memory.js index 506989911..43268ec26 100644 --- a/src/datastores/memory.js +++ b/src/datastores/memory.js @@ -1,6 +1,6 @@ 'use strict' -const DataStore = require('../datastore').DataStore +const DataStore = require('../datastore.js').DataStore class InMemoryDataStore extends DataStore { constructor (robot) { @@ -11,11 +11,11 @@ class InMemoryDataStore extends DataStore { } } - _get (key, table) { + async _get (key, table) { return Promise.resolve(this.data[table][key]) } - _set (key, value, table) { + async _set (key, value, table) { return Promise.resolve(this.data[table][key] = value) } } diff --git a/src/response.js b/src/response.js index fb9816d59..3f0f1fc7b 100644 --- a/src/response.js +++ b/src/response.js @@ -65,7 +65,7 @@ class Response { // // Returns result from middleware. async play (...strings) { - return await this.#runWithMiddleware('play', ...strings) + return await this.#runWithMiddleware('play', {}, ...strings) } // Public: Posts a message in an unlogged room diff --git a/src/robot.js b/src/robot.js index 8b15770f3..ce001a333 100644 --- a/src/robot.js +++ b/src/robot.js @@ -50,11 +50,6 @@ class Robot { name, level: process.env.HUBOT_LOG_LEVEL || 'info' }) - Reflect.defineProperty(this.logger, 'warning', { - value: this.logger.warn, - enumerable: true, - configurable: true - }) this.pingIntervalId = null this.globalHttpOptions = {} @@ -72,10 +67,6 @@ class Robot { this.on('error', (err, res) => { return this.invokeErrorHandlers(err, res) }) - this.onUncaughtException = err => { - return this.emit('error', err) - } - process.on('uncaughtException', this.onUncaughtException) } // Public: Adds a custom Listener with the provided matcher, options, and @@ -232,9 +223,11 @@ class Robot { options = {} } - this.listen(isCatchAllMessage, options, function listenCallback (msg) { - msg.message = msg.message.message - callback(msg) + this.listen(isCatchAllMessage, options, async msg => { + // TODO: Delete these commented out lines. + // console.log('catch all', msg.message) + // msg.message = msg.message.message + await callback(msg.message) }) } @@ -376,7 +369,7 @@ class Robot { this.parseHelp(full) } catch (error) { this.logger.error(`Unable to load ${full}: ${error.stack}`) - process.exit(1) + throw error } } @@ -411,7 +404,7 @@ class Robot { Object.keys(packages).forEach(key => require(key)(this, packages[key])) } catch (error) { this.logger.error(`Error loading scripts from npm package - ${error.stack}`) - process.exit(1) + throw error } } @@ -459,9 +452,8 @@ class Robot { this.server = app.listen(port, address) this.router = app } catch (error) { - const err = error - this.logger.error(`Error trying to start HTTP server: ${err}\n${err.stack}`) - process.exit(1) + this.logger.error(`Error trying to start HTTP server: ${error}\n${error.stack}`) + throw error } let herokuUrl = process.env.HEROKU_URL @@ -520,9 +512,9 @@ class Robot { } } } - } catch (err) { - this.logger.error(`Cannot load adapter ${adapterPath ?? '[no path set]'} ${this.adapterName} - ${err}`) - process.exit(1) + } catch (error) { + this.logger.error(`Cannot load adapter ${adapterPath ?? '[no path set]'} ${this.adapterName} - ${error}`) + throw error } } @@ -668,13 +660,12 @@ class Robot { if (this.pingIntervalId != null) { clearInterval(this.pingIntervalId) } - process.removeListener('uncaughtException', this.onUncaughtException) - this.adapter.close() + this.adapter?.close() if (this.server) { this.server.close() } - this.brain.close() + this.events.removeAllListeners() } // Public: The version of Hubot from npm diff --git a/test/adapter_test.js b/test/adapter_test.js index 79d4640fe..6eb90b144 100644 --- a/test/adapter_test.js +++ b/test/adapter_test.js @@ -1,97 +1,107 @@ 'use strict' - -/* global describe, beforeEach, it */ - -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) - -const expect = chai.expect - +const { describe, it, beforeEach, afterEach } = require('node:test') +const assert = require('assert/strict') const Adapter = require('../src/adapter') +const { TextMessage } = require('../src/message.js') +const User = require('../src/user.js') -describe('Adapter', function () { - beforeEach(function () { - this.robot = { receive: sinon.spy() } +describe('Adapter', () => { + let robot = null + beforeEach(() => { + robot = { receive (msg) {} } }) - describe('Public API', function () { - beforeEach(function () { - this.adapter = new Adapter(this.robot) + describe('Public API', () => { + let adapter = null + beforeEach(() => { + adapter = new Adapter(robot) + }) + afterEach(() => { + adapter.close() + process.removeAllListeners() }) - it('assigns robot', function () { - expect(this.adapter.robot).to.equal(this.robot) + it('assigns robot', () => { + assert.deepEqual(adapter.robot, robot, 'The adapter should have a reference to the robot.') }) - describe('send', function () { - it('is a function', function () { - expect(this.adapter.send).to.be.a('function') + describe('send', () => { + it('is a function', () => { + assert.ok(typeof adapter.send === 'function', 'The adapter should have a send method.') }) - it('does nothing', function () { - this.adapter.send({}, 'nothing') + it('does nothing', () => { + adapter.send({}, 'nothing') }) }) - describe('reply', function () { - it('is a function', function () { - expect(this.adapter.reply).to.be.a('function') + describe('reply', () => { + it('is a function', () => { + assert.ok(typeof adapter.reply === 'function', 'The adapter should have a reply method.') }) - it('does nothing', function () { - this.adapter.reply({}, 'nothing') + it('does nothing', () => { + adapter.reply({}, 'nothing') }) }) + describe('emote', () => { + it('is a function', () => { + assert.ok(typeof adapter.emote === 'function', 'The adapter should have a emote method.') + }) - describe('topic', function () { - it('is a function', function () { - expect(this.adapter.topic).to.be.a('function') + it('does nothing', () => { + adapter.emote({}, 'nothing') + }) + }) + describe('topic', () => { + it('is a function', () => { + assert.ok(typeof adapter.topic === 'function', 'The adapter should have a topic method.') }) - it('does nothing', function () { - this.adapter.topic({}, 'nothing') + it('does nothing', () => { + adapter.topic({}, 'nothing') }) }) - describe('play', function () { - it('is a function', function () { - expect(this.adapter.play).to.be.a('function') + describe('play', () => { + it('is a function', () => { + assert.ok(typeof adapter.play === 'function', 'The adapter should have a play method.') }) - it('does nothing', function () { - this.adapter.play({}, 'nothing') + it('does nothing', () => { + adapter.play({}, 'nothing') }) }) - describe('run', function () { - it('is a function', function () { - expect(this.adapter.run).to.be.a('function') + describe('run', () => { + it('is a function', () => { + assert.ok(typeof adapter.run === 'function', 'The adapter should have a run method.') }) - it('does nothing', function () { - this.adapter.run() + it('does nothing', () => { + adapter.run() }) }) - describe('close', function () { - it('is a function', function () { - expect(this.adapter.close).to.be.a('function') + describe('close', () => { + it('is a function', () => { + assert.ok(typeof adapter.close === 'function', 'The adapter should have a close method.') }) - it('does nothing', function () { - this.adapter.close() + it('does nothing', () => { + adapter.close() }) }) }) - it('dispatches received messages to the robot', function () { - this.robot.receive = sinon.spy() - this.adapter = new Adapter(this.robot) - this.message = sinon.spy() - - this.adapter.receive(this.message) - - expect(this.robot.receive).to.have.been.calledWith(this.message) + it('dispatches received messages to the robot', (t, done) => { + const adapter = new Adapter(robot) + const message = new TextMessage(new User('node'), 'hello', 1) + robot.receive = (msg) => { + assert.deepEqual(msg, message, 'The message should be passed through.') + done() + } + adapter.receive(message) + adapter.close() }) }) diff --git a/test/brain_test.js b/test/brain_test.js index 0e2fb7e2d..11d9c0b77 100644 --- a/test/brain_test.js +++ b/test/brain_test.js @@ -1,159 +1,199 @@ 'use strict' -/* global describe, beforeEach, afterEach, it */ /* eslint-disable no-unused-expressions */ -// Assertions and Stubbing -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) - -const expect = chai.expect - -const isCircular = require('is-circular') +const { describe, it, beforeEach, afterEach } = require('node:test') +const assert = require('assert/strict') // Hubot classes -const Brain = require('../src/brain') -const User = require('../src/user') - -describe('Brain', function () { - beforeEach(function () { - this.clock = sinon.useFakeTimers() - this.mockRobot = { - emit () {}, - on () {} - } - - // This *should* be callsArgAsync to match the 'on' API, but that makes - // the tests more complicated and seems irrelevant. - sinon.stub(this.mockRobot, 'on').withArgs('running').callsArg(1) - - this.brain = new Brain(this.mockRobot) - - this.user1 = this.brain.userForId('1', { name: 'Guy One' }) - this.user2 = this.brain.userForId('2', { name: 'Guy One Two' }) - this.user3 = this.brain.userForId('3', { name: 'Girl Three' }) +const User = require('../src/user.js') +const Robot = require('../src/robot.js') +const { hook, reset } = require('./fixtures/RequireMocker.js') +const mockAdapter = require('./fixtures/mock-adapter.js') + +describe('Brain', () => { + let mockRobot = null + let user1 = null + let user2 = null + let user3 = null + beforeEach(async () => { + hook('hubot-mock-adapter', mockAdapter) + mockRobot = new Robot('hubot-mock-adapter', false, 'TestHubot') + await mockRobot.loadAdapter() + mockRobot.run() + user1 = mockRobot.brain.userForId('1', { name: 'Guy One' }) + user2 = mockRobot.brain.userForId('2', { name: 'Guy One Two' }) + user3 = mockRobot.brain.userForId('3', { name: 'Girl Three' }) }) - - afterEach(function () { - this.clock.restore() + afterEach(() => { + mockRobot.shutdown() + reset() + process.removeAllListeners() }) - - describe('Unit Tests', function () { - describe('#mergeData', function () { - it('performs a proper merge with the new data taking precedent', function () { - this.brain.data = { + describe('Unit Tests', () => { + describe('#mergeData', () => { + it('performs a proper merge with the new data taking precedent', () => { + mockRobot.brain.data = { 1: 'old', 2: 'old' } - this.brain.mergeData({ 2: 'new' }) + mockRobot.brain.mergeData({ 2: 'new' }) - expect(this.brain.data).to.deep.equal({ + assert.deepEqual(mockRobot.brain.data, { 1: 'old', 2: 'new' - }) + }, 'The data should be merged properly.') }) - it('emits a loaded event with the new data', function () { - sinon.spy(this.brain, 'emit') - this.brain.mergeData({}) - expect(this.brain.emit).to.have.been.calledWith('loaded', this.brain.data) + it('emits a loaded event with the new data', (t, done) => { + const loadedListener = (data) => { + assert.ok(typeof data === 'object', 'data should be an object.') + mockRobot.brain.off('loaded', loadedListener) + done() + } + mockRobot.brain.on('loaded', loadedListener) + mockRobot.brain.mergeData({}) }) - it('coerces loaded data into User objects', function () { - this.brain.mergeData({ users: { 4: { name: 'new', id: '4' } } }) - const user = this.brain.userForId('4') - expect(user.constructor.name).to.equal('User') - expect(user.id).to.equal('4') - expect(user.name).to.equal('new') - expect(isCircular(this.brain)).to.be.false + it('coerces loaded data into User objects', () => { + mockRobot.brain.mergeData({ users: { 4: { name: 'new', id: '4' } } }) + const user = mockRobot.brain.userForId('4') + assert.ok(user instanceof User) + assert.equal(user.id, '4') + assert.equal(user.name, 'new') }) }) - describe('#save', () => it('emits a save event', function () { - sinon.spy(this.brain, 'emit') - this.brain.save() - expect(this.brain.emit).to.have.been.calledWith('save', this.brain.data) + describe('#save', () => it('emits a save event', (t, done) => { + const saveListener = (data) => { + assert.deepEqual(data, mockRobot.brain.data) + mockRobot.brain.off('save', saveListener) + done() + } + mockRobot.brain.on('save', saveListener) + mockRobot.brain.save() })) - describe('#resetSaveInterval', () => it('updates the auto-save interval', function () { - sinon.spy(this.brain, 'save') - // default is 5s - this.brain.resetSaveInterval(10) - // make sure autosave is on - this.brain.setAutoSave(true) - - this.clock.tick(5000) - // old interval has passed - expect(this.brain.save).to.not.have.been.called - this.clock.tick(5000) - // new interval has passed - expect(this.brain.save).to.have.been.calledOnce - })) + describe('#resetSaveInterval', () => { + it('updates the auto-save interval', async () => { + let wasCalled = false + const shouldNotBeCalled = (data) => { + assert.fail('save event should not have been emitted') + } + const shouldBeCalled = (data) => { + mockRobot.brain.off('save', shouldBeCalled) + wasCalled = true + } + mockRobot.brain.on('save', shouldNotBeCalled) + mockRobot.brain.on('save', shouldBeCalled) + // make sure autosave is on + mockRobot.brain.setAutoSave(true) + // default is 5s + mockRobot.brain.resetSaveInterval(6) + + await Promise.all([ + new Promise((resolve, reject) => { + setTimeout(() => { + assert.deepEqual(wasCalled, true, 'save event should have been emitted') + resolve() + }, 1000 * 6) + }), + new Promise((resolve, reject) => { + setTimeout(() => { + assert.notEqual(wasCalled, true) + mockRobot.brain.off('save', shouldNotBeCalled) + resolve() + }, 1000 * 5) + }) + ]) + }) + }) - describe('#close', function () { - it('saves', function () { - sinon.spy(this.brain, 'save') - this.brain.close() - expect(this.brain.save).to.have.been.calledOnce + describe('#close', () => { + it('saves', (t, done) => { + const saveListener = data => { + mockRobot.brain.off('save', saveListener) + assert.ok(data) + done() + } + mockRobot.brain.on('save', saveListener) + mockRobot.brain.close() }) - it('emits a close event', function () { - sinon.spy(this.brain, 'emit') - this.brain.close() - expect(this.brain.emit).to.have.been.calledWith('close') + it('emits a close event', (t, done) => { + const closeListener = () => { + mockRobot.brain.off('close', closeListener) + assert.ok(true) + done() + } + mockRobot.brain.on('close', closeListener) + mockRobot.brain.close() }) - it('saves before emitting the close event', function () { - sinon.spy(this.brain, 'save') - sinon.spy(this.brain, 'emit').withArgs('close') - this.brain.close() - expect(this.brain.save).to.have.been.calledBefore(this.brain.emit) + it('saves before emitting the close event', (t, done) => { + let wasSaveCalled = false + const saveListener = data => { + mockRobot.brain.off('save', saveListener) + wasSaveCalled = true + } + const closeListener = () => { + mockRobot.brain.off('close', closeListener) + assert.ok(wasSaveCalled) + done() + } + mockRobot.brain.on('save', saveListener) + mockRobot.brain.on('close', closeListener) + mockRobot.brain.close() }) - it('stops auto-saving', function () { + it('stops auto-saving', (t, done) => { // make sure autosave is on - this.brain.setAutoSave(true) - this.brain.close() + mockRobot.brain.setAutoSave(true) + mockRobot.brain.close() // set up the spy after because 'close' calls 'save' - sinon.spy(this.brain, 'save') - - this.clock.tick(2 * 5000) - expect(this.brain.save).to.not.have.been.called + const saveListener = data => { + assert.fail('save event should not have been emitted') + } + mockRobot.brain.on('save', saveListener) + setTimeout(() => { + assert.ok(true) + mockRobot.brain.off('save', saveListener) + done() + }, 1000 * 10) }) }) - describe('#get', function () { - it('returns the saved value', function () { - this.brain.data._private['test-key'] = 'value' - expect(this.brain.get('test-key')).to.equal('value') + describe('#get', () => { + it('returns the saved value', () => { + mockRobot.brain.data._private['test-key'] = 'value' + assert.equal(mockRobot.brain.get('test-key'), 'value') }) - it('returns null if object is not found', function () { - expect(this.brain.get('not a real key')).to.be.null + it('returns null if object is not found', () => { + assert.equal(mockRobot.brain.get('not a real key'), null) }) }) - describe('#set', function () { - it('saves the value', function () { - this.brain.set('test-key', 'value') - expect(this.brain.data._private['test-key']).to.equal('value') + describe('#set', () => { + it('saves the value', () => { + mockRobot.brain.set('test-key', 'value') + assert.equal(mockRobot.brain.data._private['test-key'], 'value') }) - it('sets multiple keys at once if an object is provided', function () { - this.brain.data._private = { + it('sets multiple keys at once if an object is provided', () => { + mockRobot.brain.data._private = { key1: 'val1', key2: 'val1' } - this.brain.set({ + mockRobot.brain.set({ key2: 'val2', key3: 'val2' }) - expect(this.brain.data._private).to.deep.equal({ + assert.deepEqual(mockRobot.brain.data._private, { key1: 'val1', key2: 'val2', key3: 'val2' @@ -162,175 +202,187 @@ describe('Brain', function () { // Unable to understand why this behavior is needed, but adding a test // case to protect it - it('emits loaded', function () { - sinon.spy(this.brain, 'emit') - this.brain.set('test-key', 'value') - expect(this.brain.emit).to.have.been.calledWith('loaded', this.brain.data) + it('emits loaded', (t, done) => { + const loadedListener = (data) => { + assert.deepEqual(data, mockRobot.brain.data) + mockRobot.brain.off('loaded', loadedListener) + done() + } + mockRobot.brain.on('loaded', loadedListener) + mockRobot.brain.set('test-key', 'value') }) - it('returns the brain', function () { - expect(this.brain.set('test-key', 'value')).to.equal(this.brain) + it('returns the mockRobot.brain', () => { + assert.deepEqual(mockRobot.brain.set('test-key', 'value'), mockRobot.brain) }) }) - describe('#remove', () => it('removes the specified key', function () { - this.brain.data._private['test-key'] = 'value' - this.brain.remove('test-key') - expect(this.brain.data._private).to.not.include.keys('test-key') + describe('#remove', () => it('removes the specified key', () => { + mockRobot.brain.data._private['test-key'] = 'value' + mockRobot.brain.remove('test-key') + assert.deepEqual(Object.keys(mockRobot.brain.data._private).includes('test-key'), false) })) - describe('#userForId', function () { - it('returns the user object', function () { - expect(this.brain.userForId(1)).to.equal(this.user1) + describe('#userForId', () => { + it('returns the user object', () => { + assert.deepEqual(mockRobot.brain.userForId(1), user1) }) - it('does an exact match', function () { - const user4 = this.brain.userForId('FOUR') - expect(this.brain.userForId('four')).to.not.equal(user4) + it('does an exact match', () => { + const user4 = mockRobot.brain.userForId('FOUR') + assert.notDeepEqual(mockRobot.brain.userForId('four'), user4) }) // Cannot understand why this behavior is needed, but adding a test case // to protect it - it('recreates the user if the room option differs from the user object', function () { - expect(this.brain.userForId(1).room).to.be.undefined + it('recreates the user if the room option differs from the user object', () => { + assert.equal(mockRobot.brain.userForId(1).room, undefined) // undefined -> having a room - const newUser1 = this.brain.userForId(1, { room: 'room1' }) - expect(newUser1).to.not.equal(this.user1) + const newUser1 = mockRobot.brain.userForId(1, { room: 'room1' }) + assert.notDeepEqual(newUser1, user1) // changing the room - const newUser2 = this.brain.userForId(1, { room: 'room2' }) - expect(newUser2).to.not.equal(newUser1) + const newUser2 = mockRobot.brain.userForId(1, { room: 'room2' }) + assert.notDeepEqual(newUser2, newUser1) }) - describe('when there is no matching user ID', function () { - it('creates a new User', function () { - expect(this.brain.data.users).to.not.include.key('all-new-user') - const newUser = this.brain.userForId('all-new-user') - expect(newUser).to.be.instanceof(User) - expect(newUser.id).to.equal('all-new-user') - expect(this.brain.data.users).to.include.key('all-new-user') + describe('when there is no matching user ID', () => { + it('creates a new User', () => { + assert.notEqual(Object.keys(mockRobot.brain.data.users).includes('all-new-user'), true) + const newUser = mockRobot.brain.userForId('all-new-user') + assert.ok(newUser instanceof User) + assert.equal(newUser.id, 'all-new-user') + assert.ok(Object.keys(mockRobot.brain.data.users).includes('all-new-user')) }) - it('passes the provided options to the new User', function () { - const newUser = this.brain.userForId('all-new-user', { name: 'All New User', prop: 'mine' }) - expect(newUser.name).to.equal('All New User') - expect(newUser.prop).to.equal('mine') + it('passes the provided options to the new User', () => { + const newUser = mockRobot.brain.userForId('all-new-user', { name: 'All New User', prop: 'mine' }) + assert.equal(newUser.name, 'All New User') + assert.equal(newUser.prop, 'mine') }) }) }) - describe('#userForName', function () { - it('returns the user with a matching name', function () { - expect(this.brain.userForName('Guy One')).to.equal(this.user1) + describe('#userForName', () => { + it('returns the user with a matching name', () => { + assert.deepEqual(mockRobot.brain.userForName('Guy One'), user1) }) - it('does a case-insensitive match', function () { - expect(this.brain.userForName('guy one')).to.equal(this.user1) + it('does a case-insensitive match', () => { + assert.deepEqual(mockRobot.brain.userForName('guy one'), user1) }) - it('returns null if no user matches', function () { - expect(this.brain.userForName('not a real user')).to.be.null + it('returns null if no user matches', () => { + assert.equal(mockRobot.brain.userForName('not a real user'), null) }) }) - describe('#usersForRawFuzzyName', function () { - it('does a case-insensitive match', function () { - expect(this.brain.usersForRawFuzzyName('guy')).to.have.members([this.user1, this.user2]) + describe('#usersForRawFuzzyName', () => { + it('does a case-insensitive match', () => { + assert.ok(mockRobot.brain.usersForRawFuzzyName('guy').includes(user1) && mockRobot.brain.usersForRawFuzzyName('guy').includes(user2)) }) - it('returns all matching users (prefix match) when there is not an exact match (case-insensitive)', function () { - expect(this.brain.usersForRawFuzzyName('Guy')).to.have.members([this.user1, this.user2]) + it('returns all matching users (prefix match) when there is not an exact match (case-insensitive)', () => { + assert.ok(mockRobot.brain.usersForRawFuzzyName('Guy').includes(user1) && mockRobot.brain.usersForRawFuzzyName('Guy').includes(user2)) }) - it('returns all matching users (prefix match) when there is an exact match (case-insensitive)', function () { + it('returns all matching users (prefix match) when there is an exact match (case-insensitive)', () => { // Matched case - expect(this.brain.usersForRawFuzzyName('Guy One')).to.deep.equal([this.user1, this.user2]) + assert.deepEqual(mockRobot.brain.usersForRawFuzzyName('Guy One'), [user1, user2]) // Mismatched case - expect(this.brain.usersForRawFuzzyName('guy one')).to.deep.equal([this.user1, this.user2]) + assert.deepEqual(mockRobot.brain.usersForRawFuzzyName('guy one'), [user1, user2]) }) - it('returns an empty array if no users match', function () { - const result = this.brain.usersForRawFuzzyName('not a real user') - expect(result).to.be.an('array') - expect(result).to.be.empty + it('returns an empty array if no users match', () => { + const result = mockRobot.brain.usersForRawFuzzyName('not a real user') + assert.equal(result.length, 0) }) }) - describe('#usersForFuzzyName', function () { - it('does a case-insensitive match', function () { - expect(this.brain.usersForFuzzyName('guy')).to.have.members([this.user1, this.user2]) + describe('#usersForFuzzyName', () => { + it('does a case-insensitive match', () => { + assert.ok(mockRobot.brain.usersForFuzzyName('guy').includes(user1) && mockRobot.brain.usersForFuzzyName('guy').includes(user2)) }) - it('returns all matching users (prefix match) when there is not an exact match', function () { - expect(this.brain.usersForFuzzyName('Guy')).to.have.members([this.user1, this.user2]) + it('returns all matching users (prefix match) when there is not an exact match', () => { + assert.ok(mockRobot.brain.usersForFuzzyName('Guy').includes(user1) && mockRobot.brain.usersForFuzzyName('Guy').includes(user2)) }) - it('returns just the user when there is an exact match (case-insensitive)', function () { + it('returns just the user when there is an exact match (case-insensitive)', () => { // Matched case - expect(this.brain.usersForFuzzyName('Guy One')).to.deep.equal([this.user1]) + assert.deepEqual(mockRobot.brain.usersForFuzzyName('Guy One'), [user1]) // Mismatched case - expect(this.brain.usersForFuzzyName('guy one')).to.deep.equal([this.user1]) + assert.deepEqual(mockRobot.brain.usersForFuzzyName('guy one'), [user1]) }) - it('returns an empty array if no users match', function () { - const result = this.brain.usersForFuzzyName('not a real user') - expect(result).to.be.an('array') - expect(result).to.be.empty + it('returns an empty array if no users match', () => { + const result = mockRobot.brain.usersForFuzzyName('not a real user') + assert.equal(result.length, 0) }) }) }) - describe('Auto-Save', function () { - it('is on by default', function () { - expect(this.brain.autoSave).to.equal(true) + describe('Auto-Save', () => { + it('is on by default', () => { + assert.deepEqual(mockRobot.brain.autoSave, true) }) - it('automatically saves every 5 seconds when turned on', function () { - sinon.spy(this.brain, 'save') - - this.brain.setAutoSave(true) - - this.clock.tick(5000) - expect(this.brain.save).to.have.been.called + it('automatically saves every 5 seconds when turned on', (t, done) => { + let wasCalled = false + const saveListener = data => { + mockRobot.brain.off('save', saveListener) + wasCalled = true + } + mockRobot.brain.on('save', saveListener) + mockRobot.brain.setAutoSave(true) + setTimeout(() => { + mockRobot.brain.off('save', saveListener) + assert.ok(wasCalled) + done() + }, 1000 * 5) }) - it('does not auto-save when turned off', function () { - sinon.spy(this.brain, 'save') - - this.brain.setAutoSave(false) - - this.clock.tick(2 * 5000) - expect(this.brain.save).to.not.have.been.called + it('does not auto-save when turned off', (t, done) => { + let wasCalled = false + const saveListener = data => { + wasCalled = true + assert.fail('save event should not have been emitted') + } + mockRobot.brain.setAutoSave(false) + mockRobot.brain.on('save', saveListener) + setTimeout(() => { + assert.notEqual(wasCalled, true) + mockRobot.brain.off('save', saveListener) + done() + }, 1000 * 10) }) }) - describe('User Searching', function () { - it('finds users by ID', function () { - expect(this.brain.userForId('1')).to.equal(this.user1) + describe('User Searching', () => { + it('finds users by ID', () => { + assert.deepEqual(mockRobot.brain.userForId('1'), user1) }) - it('finds users by exact name', function () { - expect(this.brain.userForName('Guy One')).to.equal(this.user1) + it('finds users by exact name', () => { + assert.deepEqual(mockRobot.brain.userForName('Guy One'), user1) }) - it('finds users by fuzzy name (prefix match)', function () { - const result = this.brain.usersForFuzzyName('Guy') - expect(result).to.have.members([this.user1, this.user2]) - expect(result).to.not.have.members([this.user3]) + it('finds users by fuzzy name (prefix match)', () => { + const result = mockRobot.brain.usersForFuzzyName('Guy') + assert.ok(result.includes(user1) && result.includes(user2)) + assert.ok(!result.includes(user3)) }) - it('returns User objects, not POJOs', function () { - expect(this.brain.userForId('1').constructor.name).to.equal('User') - for (const user of this.brain.usersForFuzzyName('Guy')) { - expect(user.constructor.name).to.equal('User') + it('returns User objects, not POJOs', () => { + assert.ok(mockRobot.brain.userForId('1') instanceof User) + for (const user of mockRobot.brain.usersForFuzzyName('Guy')) { + assert.ok(user instanceof User) } - for (const user of this.brain.usersForRawFuzzyName('Guy One')) { - expect(user.constructor.name).to.equal('User') + for (const user of mockRobot.brain.usersForRawFuzzyName('Guy One')) { + assert.ok(user instanceof User) } - - expect(isCircular(this.brain)).to.be.false }) }) }) diff --git a/test/datastore_test.js b/test/datastore_test.js index fc1be96f2..a10edb710 100644 --- a/test/datastore_test.js +++ b/test/datastore_test.js @@ -1,154 +1,127 @@ 'use strict' -/* global describe, beforeEach, it */ +const { describe, it, beforeEach, afterEach } = require('node:test') +const assert = require('assert/strict') -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) +const Brain = require('../src/brain.js') +const InMemoryDataStore = require('../src/datastores/memory.js') -const expect = chai.expect - -const Brain = require('../src/brain') -const InMemoryDataStore = require('../src/datastores/memory') - -describe('Datastore', function () { - beforeEach(function () { - this.clock = sinon.useFakeTimers() - this.robot = { +describe('Datastore', () => { + let robot = null + beforeEach(() => { + robot = { emit () {}, on () {}, - receive: sinon.spy() + receive (msg) {} } - - // This *should* be callsArgAsync to match the 'on' API, but that makes - // the tests more complicated and seems irrelevant. - sinon.stub(this.robot, 'on').withArgs('running').callsArg(1) - - this.robot.brain = new Brain(this.robot) - this.robot.datastore = new InMemoryDataStore(this.robot) - this.robot.brain.userForId('1', { name: 'User One' }) - this.robot.brain.userForId('2', { name: 'User Two' }) + robot.brain = new Brain(robot) + robot.datastore = new InMemoryDataStore(robot) + robot.brain.userForId('1', { name: 'User One' }) + robot.brain.userForId('2', { name: 'User Two' }) }) - - this.afterEach(function () { - this.clock.restore() + afterEach(() => { + robot.brain.close() + // Getting warning about too many listeners, so remove them all + process.removeAllListeners() }) - describe('global scope', function () { - it('returns undefined for values not in the datastore', function () { - return this.robot.datastore.get('blah').then(function (value) { - expect(value).to.be.an('undefined') - }) + describe('global scope', () => { + it('returns undefined for values not in the datastore', async () => { + const value = await robot.datastore.get('blah') + assert.deepEqual(value, undefined) }) - it('can store simple values', function () { - return this.robot.datastore.set('key', 'value').then(() => { - return this.robot.datastore.get('key').then((value) => { - expect(value).to.equal('value') - }) - }) + it('can store simple values', async () => { + await robot.datastore.set('key', 'value') + const value = await robot.datastore.get('key') + assert.equal(value, 'value') }) - it('can store arbitrary JavaScript values', function () { + it('can store arbitrary JavaScript values', async () => { const object = { name: 'test', data: [1, 2, 3] } - return this.robot.datastore.set('key', object).then(() => { - return this.robot.datastore.get('key').then((value) => { - expect(value.name).to.equal('test') - expect(value.data).to.deep.equal([1, 2, 3]) - }) - }) + await robot.datastore.set('key', object) + const value = await robot.datastore.get('key') + assert.equal(value.name, 'test') + assert.deepEqual(value.data, [1, 2, 3]) }) - it('can dig inside objects for values', function () { + it('can dig inside objects for values', async () => { const object = { a: 'one', b: 'two' } - return this.robot.datastore.set('key', object).then(() => { - return this.robot.datastore.getObject('key', 'a').then((value) => { - expect(value).to.equal('one') - }) - }) + await robot.datastore.set('key', object) + const value = await robot.datastore.getObject('key', 'a') + assert.equal(value, 'one') }) - it('can set individual keys inside objects', function () { + it('can set individual keys inside objects', async () => { const object = { a: 'one', b: 'two' } - return this.robot.datastore.set('object', object).then(() => { - return this.robot.datastore.setObject('object', 'c', 'three').then(() => { - return this.robot.datastore.get('object').then((value) => { - expect(value.a).to.equal('one') - expect(value.b).to.equal('two') - expect(value.c).to.equal('three') - }) - }) - }) + await robot.datastore.set('object', object) + await robot.datastore.setObject('object', 'c', 'three') + const value = await robot.datastore.get('object') + assert.equal(value.a, 'one') + assert.equal(value.b, 'two') + assert.equal(value.c, 'three') }) - it('creates an object from scratch when none exists', function () { - return this.robot.datastore.setObject('object', 'key', 'value').then(() => { - return this.robot.datastore.get('object').then((value) => { - const expected = { key: 'value' } - expect(value).to.deep.equal(expected) - }) - }) + it('creates an object from scratch when none exists', async () => { + await robot.datastore.setObject('object', 'key', 'value') + const value = await robot.datastore.get('object') + assert.deepEqual(value, { key: 'value' }) }) - it('can append to an existing array', function () { - return this.robot.datastore.set('array', [1, 2, 3]).then(() => { - return this.robot.datastore.setArray('array', 4).then(() => { - return this.robot.datastore.get('array').then((value) => { - expect(value).to.deep.equal([1, 2, 3, 4]) - }) - }) - }) + it('can append to an existing array', async () => { + await robot.datastore.set('array', [1, 2, 3]) + await robot.datastore.setArray('array', 4) + const value = await robot.datastore.get('array') + assert.deepEqual(value, [1, 2, 3, 4]) }) - it('creates an array from scratch when none exists', function () { - return this.robot.datastore.setArray('array', 4).then(() => { - return this.robot.datastore.get('array').then((value) => { - expect(value).to.deep.equal([4]) - }) - }) + it('creates an array from scratch when none exists', async () => { + await robot.datastore.setArray('array', 4) + const value = await robot.datastore.get('array') + assert.deepEqual(value, [4]) + }) + it('creates an array with an array', async () => { + const expected = [1, 2, 3] + await robot.datastore.setArray('array', [1, 2, 3]) + const actual = await robot.datastore.get('array') + assert.deepEqual(actual, expected) }) }) - describe('User scope', function () { - it('has access to the robot object', function () { - const user = this.robot.brain.userForId('1') - expect(user._getRobot()).to.equal(this.robot) + describe('User scope', () => { + it('has access to the robot object', () => { + const user = robot.brain.userForId('1') + assert.deepEqual(user._getRobot(), robot) }) - it('can store user data which is separate from global data', function () { - const user = this.robot.brain.userForId('1') - return user.set('blah', 'blah').then(() => { - return user.get('blah').then((userBlah) => { - return this.robot.datastore.get('blah').then((datastoreBlah) => { - expect(userBlah).to.not.equal(datastoreBlah) - expect(userBlah).to.equal('blah') - expect(datastoreBlah).to.be.an('undefined') - }) - }) - }) + it('can store user data which is separate from global data', async () => { + const user = robot.brain.userForId('1') + await user.set('blah', 'blah') + const userBlah = await user.get('blah') + const datastoreBlah = await robot.datastore.get('blah') + assert.notDeepEqual(userBlah, datastoreBlah) + assert.equal(userBlah, 'blah') + assert.deepEqual(datastoreBlah, undefined) }) - it('stores user data separate per-user', function () { - const userOne = this.robot.brain.userForId('1') - const userTwo = this.robot.brain.userForId('2') - return userOne.set('blah', 'blah').then(() => { - return userOne.get('blah').then((valueOne) => { - return userTwo.get('blah').then((valueTwo) => { - expect(valueOne).to.not.equal(valueTwo) - expect(valueOne).to.equal('blah') - expect(valueTwo).to.be.an('undefined') - }) - }) - }) + it('stores user data separate per-user', async () => { + const userOne = robot.brain.userForId('1') + const userTwo = robot.brain.userForId('2') + await userOne.set('blah', 'blah') + const valueOne = await userOne.get('blah') + const valueTwo = await userTwo.get('blah') + assert.notDeepEqual(valueOne, valueTwo) + assert.equal(valueOne, 'blah') + assert.deepEqual(valueTwo, undefined) }) }) }) diff --git a/test/es2015_test.js b/test/es2015_test.js index 2aca712c1..a87ec62e6 100644 --- a/test/es2015_test.js +++ b/test/es2015_test.js @@ -1,18 +1,14 @@ 'use strict' -/* global describe, it */ /* eslint-disable no-unused-expressions */ -// Assertions and Stubbing -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) -const { hook, reset } = require('./fixtures/RequireMocker.js') +const { describe, it } = require('node:test') +const assert = require('assert/strict') -const expect = chai.expect +const { hook, reset } = require('./fixtures/RequireMocker.js') // Hubot classes -const Hubot = require('../es2015') +const Hubot = require('../es2015.js') const User = Hubot.User const Brain = Hubot.Brain const Robot = Hubot.Robot @@ -26,52 +22,52 @@ const EnterMessage = Hubot.EnterMessage const LeaveMessage = Hubot.LeaveMessage const TopicMessage = Hubot.TopicMessage const CatchAllMessage = Hubot.CatchAllMessage -const loadBot = Hubot.loadBot -describe('hubot/es2015', function () { - it('exports User class', function () { +describe('hubot/es2015', () => { + it('exports User class', () => { class MyUser extends User {} const user = new MyUser('id123', { foo: 'bar' }) - expect(user).to.be.an.instanceof(User) - expect(user.id).to.equal('id123') - expect(user.foo).to.equal('bar') + assert.ok(user instanceof User) + assert.equal(user.id, 'id123') + assert.equal(user.foo, 'bar') }) - it('exports Brain class', function () { + it('exports Brain class', () => { class MyBrain extends Brain {} const robotMock = { - on: sinon.spy() + on () { + assert.ok(true) + } } const brain = new MyBrain(robotMock) - expect(brain).to.be.an.instanceof(Brain) - expect(robotMock.on).to.have.been.called - + assert.ok(brain instanceof Brain) brain.set('foo', 'bar') - expect(brain.get('foo')).to.equal('bar') + assert.equal(brain.get('foo'), 'bar') }) - it('exports Robot class', async function () { + it('exports Robot class', async () => { hook('hubot-mock-adapter', require('./fixtures/mock-adapter.js')) class MyRobot extends Robot {} const robot = new MyRobot('hubot-mock-adapter', false, 'TestHubot') await robot.loadAdapter() - expect(robot).to.be.an.instanceof(Robot) - expect(robot.name).to.equal('TestHubot') + assert.ok(robot instanceof Robot) + assert.equal(robot.name, 'TestHubot') + robot.shutdown() reset() }) - it('exports Adapter class', function () { + it('exports Adapter class', () => { class MyAdapter extends Adapter {} const adapter = new MyAdapter('myrobot') - expect(adapter).to.be.an.instanceof(Adapter) - expect(adapter.robot).to.equal('myrobot') + assert.ok(adapter instanceof Adapter) + assert.equal(adapter.robot, 'myrobot') }) - it('exports Response class', function () { + it('exports Response class', () => { class MyResponse extends Response {} const robotMock = 'robotMock' const messageMock = { @@ -81,95 +77,93 @@ describe('hubot/es2015', function () { const matchMock = 'matchMock' const response = new MyResponse(robotMock, messageMock, matchMock) - expect(response).to.be.an.instanceof(Response) - expect(response.message).to.equal(messageMock) - expect(response.match).to.equal(matchMock) + assert.ok(response instanceof Response) + assert.deepEqual(response.message, messageMock) + assert.equal(response.match, matchMock) }) - it('exports Listener class', function () { + it('exports Listener class', () => { class MyListener extends Listener {} const robotMock = 'robotMock' const matcherMock = 'matchMock' - const callback = sinon.spy() + const callback = () => {} const listener = new MyListener(robotMock, matcherMock, callback) - expect(listener).to.be.an.instanceof(Listener) - expect(listener.robot).to.equal(robotMock) - expect(listener.matcher).to.equal(matcherMock) - expect(listener.options).to.deep.include({ - id: null - }) - expect(listener.callback).to.equal(callback) + assert.ok(listener instanceof Listener) + assert.deepEqual(listener.robot, robotMock) + assert.equal(listener.matcher, matcherMock) + assert.equal(listener.options.id, null) + assert.deepEqual(listener.callback, callback) }) - it('exports TextListener class', function () { + it('exports TextListener class', () => { class MyTextListener extends TextListener {} const robotMock = 'robotMock' const regex = /regex/ - const callback = sinon.spy() + const callback = () => {} const textListener = new MyTextListener(robotMock, regex, callback) - expect(textListener).to.be.an.instanceof(TextListener) - expect(textListener.regex).to.equal(regex) + assert.ok(textListener instanceof TextListener) + assert.deepEqual(textListener.regex, regex) }) - it('exports Message class', function () { + it('exports Message class', () => { class MyMessage extends Message {} const userMock = { room: 'room' } const message = new MyMessage(userMock) - expect(message).to.be.an.instanceof(Message) - expect(message.user).to.equal(userMock) + assert.ok(message instanceof Message) + assert.deepEqual(message.user, userMock) }) - it('exports TextMessage class', function () { + it('exports TextMessage class', () => { class MyTextMessage extends TextMessage {} const userMock = { room: 'room' } const textMessage = new MyTextMessage(userMock, 'bla blah') - expect(textMessage).to.be.an.instanceof(TextMessage) - expect(textMessage).to.be.an.instanceof(Message) - expect(textMessage.text).to.equal('bla blah') + assert.ok(textMessage instanceof TextMessage) + assert.ok(textMessage instanceof Message) + assert.equal(textMessage.text, 'bla blah') }) - it('exports EnterMessage class', function () { + it('exports EnterMessage class', () => { class MyEnterMessage extends EnterMessage {} const userMock = { room: 'room' } const enterMessage = new MyEnterMessage(userMock) - expect(enterMessage).to.be.an.instanceof(EnterMessage) - expect(enterMessage).to.be.an.instanceof(Message) + assert.ok(enterMessage instanceof EnterMessage) + assert.ok(enterMessage instanceof Message) }) - it('exports LeaveMessage class', function () { + it('exports LeaveMessage class', () => { class MyLeaveMessage extends LeaveMessage {} const userMock = { room: 'room' } const leaveMessage = new MyLeaveMessage(userMock) - expect(leaveMessage).to.be.an.instanceof(LeaveMessage) - expect(leaveMessage).to.be.an.instanceof(Message) + assert.ok(leaveMessage instanceof LeaveMessage) + assert.ok(leaveMessage instanceof Message) }) - it('exports TopicMessage class', function () { + it('exports TopicMessage class', () => { class MyTopicMessage extends TopicMessage {} const userMock = { room: 'room' } const topicMessage = new MyTopicMessage(userMock) - expect(topicMessage).to.be.an.instanceof(TopicMessage) - expect(topicMessage).to.be.an.instanceof(Message) + assert.ok(topicMessage instanceof TopicMessage) + assert.ok(topicMessage instanceof Message) }) - it('exports CatchAllMessage class', function () { + it('exports CatchAllMessage class', () => { class MyCatchAllMessage extends CatchAllMessage {} const messageMock = { user: { @@ -178,18 +172,17 @@ describe('hubot/es2015', function () { } const catchAllMessage = new MyCatchAllMessage(messageMock) - expect(catchAllMessage).to.be.an.instanceof(CatchAllMessage) - expect(catchAllMessage).to.be.an.instanceof(Message) - expect(catchAllMessage.message).to.equal(messageMock) - expect(catchAllMessage.user).to.equal(messageMock.user) + assert.ok(catchAllMessage instanceof CatchAllMessage) + assert.ok(catchAllMessage instanceof Message) + assert.deepEqual(catchAllMessage.message, messageMock) + assert.deepEqual(catchAllMessage.user, messageMock.user) }) - it('exports loadBot function', function () { - sinon.stub(Hubot, 'Robot') - - expect(loadBot).to.be.a('function') - Hubot.loadBot('adapter', 'enableHttpd', 'botName', 'botAlias') - expect(Hubot.Robot).to.be.called.calledWith('adapter', 'enableHttpd', 'botName', 'botAlias') - sinon.restore() + it('exports loadBot function', () => { + assert.ok(Hubot.loadBot && typeof Hubot.loadBot === 'function') + const robot = Hubot.loadBot('adapter', false, 'botName', 'botAlias') + assert.equal(robot.name, 'botName') + assert.equal(robot.alias, 'botAlias') + robot.shutdown() }) }) diff --git a/test/hubot_test.js b/test/hubot_test.js index ddefdb4e3..8b83646a8 100644 --- a/test/hubot_test.js +++ b/test/hubot_test.js @@ -1,14 +1,12 @@ 'use strict' -/* global describe, it */ /* eslint-disable no-unused-expressions */ -const path = require('path') -const chai = require('chai') -chai.use(require('sinon-chai')) -const expect = chai.expect +const { describe, it } = require('node:test') +const assert = require('assert/strict') const root = __dirname.replace(/test$/, '') const { TextMessage, User } = require('../index.js') +const path = require('node:path') describe('Running bin/hubot.js', () => { it('should load adapter from HUBOT_FILE environment variable', async function () { @@ -20,15 +18,15 @@ describe('Running bin/hubot.js', () => { await new Promise(resolve => setTimeout(resolve, 100)) } hubot.adapter.on('reply', (envelope, ...strings) => { - expect(strings[0]).to.equal('test response from .mjs script') + assert.equal(strings[0], 'test response from .mjs script') delete process.env.HUBOT_FILE delete process.env.HUBOT_HTTPD hubot.shutdown() }) try { await hubot.receive(new TextMessage(new User('mocha', { room: '#mocha' }), '@Hubot test')) - expect(hubot.hasLoadedTestMjsScript).to.be.true - expect(hubot.name).to.equal('Hubot') + assert.deepEqual(hubot.hasLoadedTestMjsScript, true) + assert.equal(hubot.name, 'Hubot') } finally { hubot.shutdown() } diff --git a/test/listener_test.js b/test/listener_test.js index b62cf745d..4f6ac220e 100644 --- a/test/listener_test.js +++ b/test/listener_test.js @@ -1,130 +1,118 @@ 'use strict' -/* global describe, beforeEach, it */ /* eslint-disable no-unused-expressions */ -// Assertions and Stubbing -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) - -const expect = chai.expect +const { describe, it } = require('node:test') +const assert = require('node:assert/strict') // Hubot classes -const EnterMessage = require('../src/message').EnterMessage -const TextMessage = require('../src/message').TextMessage -const Listener = require('../src/listener').Listener -const TextListener = require('../src/listener').TextListener -const Response = require('../src/response') -const User = require('../src/user') -const Middleware = require('../src/middleware') - -describe('Listener', function () { - beforeEach(function () { - // Dummy robot - this.robot = { - // Re-throw AssertionErrors for clearer test failures - emit (name, err, response) { - if (err.constructor.name === 'AssertionError') { - return process.nextTick(function () { - throw err - }) - } - }, - // Ignore log messages - logger: { - debug () {}, - error (...args) { - // console.error(...args) - } - }, - // Why is this part of the Robot object?? - Response - } - - // Test user - this.user = new User({ - id: 1, - name: 'hubottester', - room: '#mocha' - }) +const EnterMessage = require('../src/message.js').EnterMessage +const TextMessage = require('../src/message.js').TextMessage +const Listener = require('../src/listener.js').Listener +const TextListener = require('../src/listener.js').TextListener +const Response = require('../src/response.js') +const User = require('../src/user.js') +const Middleware = require('../src/middleware.js') + +describe('Listener', () => { + const robot = { + // Re-throw AssertionErrors for clearer test failures + emit (name, err, response) { + if (err.constructor.name === 'AssertionError') { + return process.nextTick(() => { + throw err + }) + } + }, + // Ignore log messages + logger: { + debug () {}, + error (...args) { + // console.error(...args) + } + }, + // Why is this part of the Robot object?? + Response + } + const user = new User({ + id: 1, + name: 'hubottester', + room: '#mocha' }) - describe('Unit Tests', function () { - describe('#call', function () { - it('calls the matcher', async function () { - const testMessage = new TextMessage(this.user, 'message') + describe('Unit Tests', () => { + describe('#call', () => { + it('calls the matcher', async () => { + const testMessage = new TextMessage(user, 'message') const testMatcher = message => { - expect(message).to.be.equal(testMessage) + assert.deepEqual(message, testMessage) return true } - const middleware = new Middleware(this.robot) + const middleware = new Middleware(robot) middleware.register(async context => { - expect(context.listener).to.be.equal(testListener) + assert.deepEqual(context.listener, testListener) }) - const testListener = new Listener(this.robot, testMatcher, async response => true) + const testListener = new Listener(robot, testMatcher, async response => true) await testListener.call(testMessage, middleware) }) - it('the response object should have the match results so listeners can have access to it', async function () { + it('the response object should have the match results so listeners can have access to it', async () => { const matcherResult = {} const testMatcher = message => { return matcherResult } - const testMessage = new TextMessage(this.user, 'response should have match') - const listenerCallback = response => expect(response.match).to.be.equal(matcherResult) - const testListener = new Listener(this.robot, testMatcher, listenerCallback) + const testMessage = new TextMessage(user, 'response should have match') + const listenerCallback = response => assert.deepEqual(response.match, matcherResult) + const testListener = new Listener(robot, testMatcher, listenerCallback) await testListener.call(testMessage, null) }) - describe('if the matcher returns true', function () { - beforeEach(function () { - this.createListener = function (cb) { - return new Listener(this.robot, () => true, cb) - } - }) + describe('if the matcher returns true', () => { + const createListener = cb => { + return new Listener(robot, () => true, cb) + } - it('executes the listener callback', async function () { + it('executes the listener callback', async () => { const listenerCallback = async response => { - expect(response.message).to.be.equal(testMessage) + assert.deepEqual(response.message, testMessage) } const testMessage = {} - const testListener = this.createListener(listenerCallback) - await testListener.call(testMessage, async function (_) {}) + const testListener = createListener(listenerCallback) + await testListener.call(testMessage, async (_) => {}) }) - it('returns true', function () { + it('returns true', () => { const testMessage = {} - const testListener = this.createListener(function () {}) + const testListener = createListener(() => {}) const result = testListener.call(testMessage) - expect(result).to.be.ok + assert.ok(result) }) - it('calls the provided callback with true', function (done) { + it('calls the provided callback with true', (t, done) => { const testMessage = {} - const testListener = this.createListener(function () {}) - testListener.call(testMessage, function (result) { - expect(result).to.be.ok + const testListener = createListener(() => {}) + testListener.call(testMessage, async result => { + assert.ok(result) done() }) }) - it('calls the provided callback after the function returns', function (done) { + it('calls the provided callback after the function returns', (t, done) => { const testMessage = {} - const testListener = this.createListener(function () {}) + const testListener = createListener(() => {}) let finished = false - testListener.call(testMessage, function (result) { - expect(finished).to.be.ok + testListener.call(testMessage, async result => { + assert.ok(finished) done() }) finished = true }) - it('handles uncaught errors from the listener callback', async function () { + it('handles uncaught errors from the listener callback', async () => { const testMessage = {} const theError = new Error() @@ -132,190 +120,195 @@ describe('Listener', function () { throw theError } - this.robot.emit = function (name, err, response) { - expect(name).to.equal('error') - expect(err).to.equal(theError) - expect(response.message).to.equal(testMessage) + robot.emit = (name, err, response) => { + assert.equal(name, 'error') + assert.deepEqual(err, theError) + assert.deepEqual(response.message, testMessage) } - const testListener = this.createListener(listenerCallback) + const testListener = createListener(listenerCallback) await testListener.call(testMessage, async response => {}) }) - it('calls the provided callback with true if there is an error thrown by the listener callback', function (done) { + it('calls the provided callback with true if there is an error thrown by the listener callback', (t, done) => { const testMessage = {} const theError = new Error() - const listenerCallback = function (response) { + const listenerCallback = async response => { throw theError } - const testListener = this.createListener(listenerCallback) - testListener.call(testMessage, function (result) { - expect(result).to.be.ok + const testListener = createListener(listenerCallback) + testListener.call(testMessage, async result => { + assert.ok(result) done() }) }) - it('calls the listener callback with a Response that wraps the Message', async function () { + it('calls the listener callback with a Response that wraps the Message', async () => { const testMessage = {} const listenerCallback = async response => { - expect(response.message).to.equal(testMessage) + assert.deepEqual(response.message, testMessage) } - const testListener = this.createListener(listenerCallback) + const testListener = createListener(listenerCallback) await testListener.call(testMessage, async response => {}) }) - it('passes through the provided middleware stack', async function () { + it('passes through the provided middleware stack', async () => { const testMessage = {} - const testListener = this.createListener(function () {}) + const testListener = createListener(async () => {}) const testMiddleware = { execute (context, next, done) { - expect(context.listener).to.be.equal(testListener) - expect(context.response).to.be.instanceof(Response) - expect(context.response.message).to.be.equal(testMessage) - expect(next).to.be.a('function') - expect(done).to.be.a('function') + assert.deepEqual(context.listener, testListener) + assert.ok(context.response instanceof Response) + assert.deepEqual(context.response.message, testMessage) + assert.ok(typeof next === 'function') + assert.ok(typeof done === 'function') } } await testListener.call(testMessage, testMiddleware) }) - it('executes the listener callback if middleware succeeds', async function () { - const listenerCallback = sinon.spy() + it('executes the listener callback if middleware succeeds', async () => { + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + } const testMessage = {} - const testListener = this.createListener(listenerCallback) + const testListener = createListener(listenerCallback) - await testListener.call(testMessage, function (result) { - expect(result).to.be.ok + await testListener.call(testMessage, async result => { + assert.ok(result) }) - expect(listenerCallback).to.have.been.called + assert.deepEqual(wasCalled, true) }) - it('does not execute the listener callback if middleware fails', async function () { - const listenerCallback = sinon.spy() + it('does not execute the listener callback if middleware fails', async () => { + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + } const testMessage = {} - const testListener = this.createListener(listenerCallback) + const testListener = createListener(listenerCallback) const testMiddleware = { async execute (context) { return false } } - await testListener.call(testMessage, testMiddleware, function (result) { - expect(result).to.be.ok + await testListener.call(testMessage, testMiddleware, async result => { + assert.ok(result) }) - expect(listenerCallback).to.not.have.been.called + assert.deepEqual(wasCalled, false) }) }) - describe('if the matcher returns false', function () { - beforeEach(function () { - this.createListener = function (cb) { - return new Listener(this.robot, () => false, cb) - } - }) + describe('if the matcher returns false', () => { + const createListener = cb => { + return new Listener(robot, () => false, cb) + } - it('does not execute the listener callback', async function () { - const listenerCallback = sinon.spy() + it('does not execute the listener callback', async () => { + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + } const testMessage = {} - const testListener = this.createListener(listenerCallback) + const testListener = createListener(listenerCallback) await testListener.call(testMessage, async context => { - expect(listenerCallback).to.not.have.been.called + assert.deepEqual(wasCalled, false) }) }) - it('returns null', async function () { + it('returns null', async () => { const testMessage = {} - const testListener = this.createListener(function () {}) + const testListener = createListener(async () => {}) const result = await testListener.call(testMessage) - expect(result).to.be.null + assert.deepEqual(result, null) }) - it('returns null because there is no matched listener', async function () { + it('returns null because there is no matched listener', async () => { const testMessage = {} - const testListener = this.createListener(function () {}) - const middleware = sinon.spy(new Middleware(this.robot).execute) + const testListener = createListener(async () => {}) + const middleware = context => { + throw new Error('Should not be called') + } const result = await testListener.call(testMessage, middleware) - expect(result).to.be.null - expect(middleware).to.not.have.been.called - }) - - it('does not execute any middleware', async function () { - const testMessage = {} - const testListener = this.createListener(function () {}) - const testMiddleware = { execute: sinon.spy() } - await testListener.call(testMessage, result => { - expect(testMiddleware.execute).to.not.have.been.called - }) + assert.deepEqual(result, null) }) }) }) - describe('#constructor', function () { - it('requires a matcher', () => expect(function () { - return new Listener(this.robot, undefined, {}, sinon.spy()) - }).to.throw(Error)) + describe('#constructor', () => { + it('requires a matcher', () => { + assert.throws(() => { + return new Listener(robot, undefined, {}, async () => {}) + }, Error) + }) - it('requires a callback', function () { + it('requires a callback', () => { // No options - expect(function () { - return new Listener(this.robot, sinon.spy()) - }).to.throw(Error) + assert.throws(() => { + return new Listener(robot, () => {}) + }, Error) // With options - expect(function () { - return new Listener(this.robot, sinon.spy(), {}) - }).to.throw(Error) + assert.throws(() => { + return new Listener(robot, () => {}, {}) + }, Error) }) - it('gracefully handles missing options', function () { - const testMatcher = sinon.spy() - const listenerCallback = sinon.spy() - const testListener = new Listener(this.robot, testMatcher, listenerCallback) + it('gracefully handles missing options', () => { + const testMatcher = () => {} + const listenerCallback = async () => {} + const testListener = new Listener(robot, testMatcher, listenerCallback) // slightly brittle because we are testing for the default options Object - expect(testListener.options).to.deep.equal({ id: null }) - expect(testListener.callback).to.be.equal(listenerCallback) + assert.deepEqual(testListener.options, { id: null }) + assert.deepEqual(testListener.callback, listenerCallback) }) - it('gracefully handles a missing ID (set to null)', function () { - const testMatcher = sinon.spy() - const listenerCallback = sinon.spy() - const testListener = new Listener(this.robot, testMatcher, {}, listenerCallback) - expect(testListener.options.id).to.be.null + it('gracefully handles a missing ID (set to null)', () => { + const testMatcher = () => {} + const listenerCallback = async () => {} + const testListener = new Listener(robot, testMatcher, {}, listenerCallback) + assert.deepEqual(testListener.options.id, null) }) }) describe('TextListener', () => - describe('#matcher', function () { - it('matches TextMessages', function () { - const callback = sinon.spy() - const testMessage = new TextMessage(this.user, 'test') - testMessage.match = sinon.stub().returns(true) + describe('#matcher', () => { + it('matches TextMessages', () => { + const callback = async () => {} + const testMessage = new TextMessage(user, 'test') + + testMessage.match = regex => { + assert.deepEqual(regex, testRegex) + return true + } const testRegex = /test/ - const testListener = new TextListener(this.robot, testRegex, callback) + const testListener = new TextListener(robot, testRegex, callback) const result = testListener.matcher(testMessage) - expect(result).to.be.ok - expect(testMessage.match).to.have.been.calledWith(testRegex) + assert.ok(result) }) - it('does not match EnterMessages', function () { - const callback = sinon.spy() - const testMessage = new EnterMessage(this.user) - testMessage.match = sinon.stub().returns(true) + it('does not match EnterMessages', () => { + const callback = async () => {} + const testMessage = new EnterMessage(user) + testMessage.match = () => { + assert.fail('match should not be called') + } const testRegex = /test/ - const testListener = new TextListener(this.robot, testRegex, callback) + const testListener = new TextListener(robot, testRegex, callback) const result = testListener.matcher(testMessage) - expect(result).to.not.be.ok - expect(testMessage.match).to.not.have.been.called + assert.deepEqual(result, undefined) }) }) ) diff --git a/test/message_test.js b/test/message_test.js index 23083f770..9a6dbba91 100644 --- a/test/message_test.js +++ b/test/message_test.js @@ -1,44 +1,38 @@ 'use strict' -/* global describe, beforeEach, it */ /* eslint-disable no-unused-expressions */ -// Assertions and Stubbing -const chai = require('chai') -chai.use(require('sinon-chai')) - -const expect = chai.expect +const { describe, it } = require('node:test') +const assert = require('node:assert/strict') // Hubot classes const User = require('../src/user') const Message = require('../src/message').Message const TextMessage = require('../src/message').TextMessage -describe('Message', function () { - beforeEach(function () { - this.user = new User({ - id: 1, - name: 'hubottester', - room: '#mocha' - }) +describe('Message', () => { + const user = new User({ + id: 1, + name: 'hubottester', + room: '#mocha' }) - describe('Unit Tests', function () { + describe('Unit Tests', () => { describe('#finish', () => - it('marks the message as done', function () { - const testMessage = new Message(this.user) - expect(testMessage.done).to.not.be.ok + it('marks the message as done', () => { + const testMessage = new Message(user) + assert.deepEqual(testMessage.done, false) testMessage.finish() - expect(testMessage.done).to.be.ok + assert.deepEqual(testMessage.done, true) }) ) describe('TextMessage', () => describe('#match', () => - it('should perform standard regex matching', function () { - const testMessage = new TextMessage(this.user, 'message123') - expect(testMessage.match(/^message123$/)).to.be.ok - expect(testMessage.match(/^does-not-match$/)).to.not.be.ok + it('should perform standard regex matching', () => { + const testMessage = new TextMessage(user, 'message123') + assert.equal(testMessage.match(/^message123$/)[0], 'message123') + assert.deepEqual(testMessage.match(/^does-not-match$/), null) }) ) ) diff --git a/test/middleware_test.js b/test/middleware_test.js index 74e2a252b..9bcdc007a 100644 --- a/test/middleware_test.js +++ b/test/middleware_test.js @@ -1,14 +1,9 @@ 'use strict' -/* global describe, beforeEach, it, afterEach */ /* eslint-disable no-unused-expressions */ -// Assertions and Stubbing -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) - -const expect = chai.expect +const { describe, it, beforeEach, afterEach } = require('node:test') +const assert = require('node:assert/strict') // Hubot classes const Robot = require('../src/robot') @@ -18,26 +13,28 @@ const Middleware = require('../src/middleware') const { hook, reset } = require('./fixtures/RequireMocker.js') -describe('Middleware', function () { - describe('Unit Tests', function () { - beforeEach(function () { - // Stub out event emitting - this.robot = { emit: sinon.spy() } - - this.middleware = new Middleware(this.robot) +describe('Middleware', () => { + describe('Unit Tests', () => { + let robot = null + let middleware = null + beforeEach(() => { + robot = { emit () {} } + middleware = new Middleware(robot) }) - describe('#execute', function () { - it('executes synchronous middleware', async function () { - const testMiddleware = sinon.spy(async context => { + describe('#execute', () => { + it('executes synchronous middleware', async () => { + let wasCalled = false + const testMiddleware = async context => { + wasCalled = true return true - }) - this.middleware.register(testMiddleware) - await this.middleware.execute({}) - expect(testMiddleware).to.have.been.called + } + middleware.register(testMiddleware) + await middleware.execute({}) + assert.deepEqual(wasCalled, true) }) - it('executes all registered middleware in definition order', async function () { + it('executes all registered middleware in definition order', async () => { const middlewareExecution = [] const testMiddlewareA = async context => { middlewareExecution.push('A') @@ -45,14 +42,14 @@ describe('Middleware', function () { const testMiddlewareB = async context => { middlewareExecution.push('B') } - this.middleware.register(testMiddlewareA) - this.middleware.register(testMiddlewareB) - await this.middleware.execute({}) - expect(middlewareExecution).to.deep.equal(['A', 'B']) + middleware.register(testMiddlewareA) + middleware.register(testMiddlewareB) + await middleware.execute({}) + assert.deepEqual(middlewareExecution, ['A', 'B']) }) - describe('error handling', function () { - it('does not execute subsequent middleware after the error is thrown', async function () { + describe('error handling', () => { + it('does not execute subsequent middleware after the error is thrown', async () => { const middlewareExecution = [] const testMiddlewareA = async context => { @@ -68,26 +65,26 @@ describe('Middleware', function () { middlewareExecution.push('C') } - this.middleware.register(testMiddlewareA) - this.middleware.register(testMiddlewareB) - this.middleware.register(testMiddlewareC) - await this.middleware.execute({}) - expect(middlewareExecution).to.deep.equal(['A', 'B']) + middleware.register(testMiddlewareA) + middleware.register(testMiddlewareB) + middleware.register(testMiddlewareC) + await middleware.execute({}) + assert.deepEqual(middlewareExecution, ['A', 'B']) }) }) }) - describe('#register', function () { - it('adds to the list of middleware', function () { + describe('#register', () => { + it('adds to the list of middleware', () => { const testMiddleware = async context => {} - this.middleware.register(testMiddleware) - expect(this.middleware.stack).to.include(testMiddleware) + middleware.register(testMiddleware) + assert.ok(middleware.stack.includes(testMiddleware)) }) - it('validates the arity of middleware', function () { - const testMiddleware = function (context, next, done, extra) {} + it('validates the arity of middleware', () => { + const testMiddleware = async (context, next, done, extra) => {} - expect(() => this.middleware.register(testMiddleware)).to.throw(/Incorrect number of arguments/) + assert.throws(() => middleware.register(testMiddleware), 'Incorrect number of arguments') }) }) }) @@ -95,98 +92,79 @@ describe('Middleware', function () { // Per the documentation in docs/scripting.md // Any new fields that are exposed to middleware should be explicitly // tested for. - describe('Public Middleware APIs', function () { - beforeEach(async function () { + describe('Public Middleware APIs', () => { + let robot = null + let user = null + let testListener = null + let testMessage = null + beforeEach(async () => { hook('hubot-mock-adapter', require('./fixtures/mock-adapter.js')) - process.env.EXPRESS_PORT = 0 - this.robot = new Robot('hubot-mock-adapter', true, 'TestHubot') - await this.robot.loadAdapter() - this.robot.run + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + await robot.loadAdapter() + robot.run // Re-throw AssertionErrors for clearer test failures - this.robot.on('error', function (err, response) { + robot.on('error', function (err, response) { if (__guard__(err != null ? err.constructor : undefined, x => x.name) === 'AssertionError') { - process.nextTick(function () { + process.nextTick(() => { throw err }) } }) - this.user = this.robot.brain.userForId('1', { + user = robot.brain.userForId('1', { name: 'hubottester', room: '#mocha' }) - - // Dummy middleware - this.middleware = sinon.spy(async context => true) - - this.testMessage = new TextMessage(this.user, 'message123') - this.robot.hear(/^message123$/, function (response) {}) - this.testListener = this.robot.listeners[0] + testMessage = new TextMessage(user, 'message123') + robot.hear(/^message123$/, async response => {}) + testListener = robot.listeners[0] }) - afterEach(function () { + afterEach(() => { reset() - this.robot.shutdown() + robot.shutdown() }) - describe('listener middleware context', function () { - beforeEach(function () { - this.robot.listenerMiddleware(async context => { - await this.middleware(context) - }) - }) - - describe('listener', function () { - it('is the listener object that matched', async function () { - await this.robot.receive(this.testMessage) - expect(this.middleware).to.have.been.calledWithMatch( - sinon.match.has('listener', - sinon.match.same(this.testListener)) // context - ) - }) - - it('has options.id (metadata)', async function () { - await this.robot.receive(this.testMessage) - expect(this.middleware).to.have.been.calledWithMatch( - sinon.match.has('listener', - sinon.match.has('options', - sinon.match.has('id'))) // context - ) + describe('listener middleware context', () => { + describe('listener', () => { + it('is the listener object that matched, has metadata in options object with id', async () => { + robot.listenerMiddleware(async context => { + assert.deepEqual(context.listener, testListener) + assert.ok(context.listener.options) + assert.deepEqual(context.listener.options.id, null) + return true + }) + await robot.receive(testMessage) }) }) describe('response', () => - it('is a Response that wraps the message', async function () { - await this.robot.receive(this.testMessage) - expect(this.middleware).to.have.been.calledWithMatch( - sinon.match.has('response', - sinon.match.instanceOf(Response).and( - sinon.match.has('message', - sinon.match.same(this.testMessage)))) // context - ) + it('is a Response that wraps the message', async () => { + robot.listenerMiddleware(async context => { + assert.ok(context.response instanceof Response) + assert.ok(context.response.message) + assert.deepEqual(context.response.message, testMessage) + return true + }) + await robot.receive(testMessage) }) ) }) - describe('receive middleware context', function () { - beforeEach(function () { - this.robot.receiveMiddleware(async context => { - await this.middleware(context) - }) - }) + describe('receive middleware context', () => { + describe('response', () => { + it('is a match-less Response object', async () => { + robot.receiveMiddleware(async context => { + assert.ok(context.response instanceof Response) + assert.ok(context.response.message) + assert.deepEqual(context.response.message, testMessage) + return true + }) - describe('response', () => - it('is a match-less Response object', async function () { - await this.robot.receive(this.testMessage) - expect(this.middleware).to.have.been.calledWithMatch( - sinon.match.has('response', - sinon.match.instanceOf(Response).and( - sinon.match.has('message', - sinon.match.same(this.testMessage)))) // context - ) + await robot.receive(testMessage) }) - ) + }) }) }) }) diff --git a/test/robot_test.js b/test/robot_test.js index 5c3c30abf..f2da62ede 100644 --- a/test/robot_test.js +++ b/test/robot_test.js @@ -1,969 +1,1021 @@ 'use strict' -/* global describe, beforeEach, it, afterEach */ /* eslint-disable no-unused-expressions */ - -// Assertions and Stubbing -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) - -const expect = chai.expect +require('coffeescript/register.js') +const { describe, it, beforeEach, afterEach } = require('node:test') +const assert = require('assert/strict') // Hubot classes -const Robot = require('../src/robot') -const CatchAllMessage = require('../src/message').CatchAllMessage -const EnterMessage = require('../src/message').EnterMessage -const LeaveMessage = require('../src/message').LeaveMessage -const TextMessage = require('../src/message').TextMessage -const TopicMessage = require('../src/message').TopicMessage - +const Robot = require('../src/robot.js') +const CatchAllMessage = require('../src/message.js').CatchAllMessage +const EnterMessage = require('../src/message.js').EnterMessage +const LeaveMessage = require('../src/message.js').LeaveMessage +const TextMessage = require('../src/message.js').TextMessage +const TopicMessage = require('../src/message.js').TopicMessage +const User = require('../src/user.js') const path = require('path') const { hook, reset } = require('./fixtures/RequireMocker.js') - -describe('Robot', function () { - beforeEach(async function () { - hook('hubot-mock-adapter', require('./fixtures/mock-adapter.js')) - process.env.EXPRESS_PORT = 0 - this.robot = new Robot('hubot-mock-adapter', true, 'TestHubot') - this.robot.alias = 'Hubot' - await this.robot.loadAdapter() - this.robot.run() - - // Re-throw AssertionErrors for clearer test failures - this.robot.on('error', function (err, response) { - if (err?.constructor.name === 'AssertionError' || err instanceof chai.AssertionError) { - process.nextTick(function () { - throw err - }) - } +const mockAdapter = require('./fixtures/mock-adapter.js') +describe('Robot', () => { + describe('#http', () => { + let robot = null + beforeEach(() => { + robot = new Robot(null, false, 'TestHubot') }) - - this.user = this.robot.brain.userForId('1', { - name: 'hubottester', - room: '#mocha' + afterEach(() => { + robot.shutdown() + }) + it('API', () => { + const agent = {} + const httpClient = robot.http('http://example.com', { agent }) + assert.ok(httpClient.get) + assert.ok(httpClient.post) + }) + it('passes options through to the ScopedHttpClient', () => { + const agent = {} + const httpClient = robot.http('http://example.com', { agent }) + assert.deepEqual(httpClient.options.agent, agent) + }) + it('sets a user agent', () => { + const httpClient = robot.http('http://example.com') + assert.ok(httpClient.options.headers['User-Agent'].indexOf('Hubot') > -1) + }) + it('meges global http options', () => { + const agent = {} + robot.globalHttpOptions = { agent } + const httpClient = robot.http('http://localhost') + assert.deepEqual(httpClient.options.agent, agent) + }) + it('local options override global http options', () => { + const agentA = {} + const agentB = {} + robot.globalHttpOptions = { agent: agentA } + const httpClient = robot.http('http://localhost', { agent: agentB }) + assert.deepEqual(httpClient.options.agent, agentB) + }) + it('builds the url correctly from a string', () => { + const httpClient = robot.http('http://localhost') + const options = httpClient.buildOptions('http://localhost:3001') + assert.equal(options.host, 'localhost:3001') + assert.equal(options.pathname, '/') + assert.equal(options.protocol, 'http:') + assert.equal(options.port, '3001') }) }) - afterEach(function () { - reset() - this.robot.shutdown() - }) - - describe('Unit Tests', function () { - describe('#http', function () { - beforeEach(function () { - const url = 'http://localhost' - this.httpClient = this.robot.http(url) - }) + describe('#respondPattern', () => { + let robot = null + beforeEach(() => { + robot = new Robot('hubot-mock-adapter', false, 'TestHubot', 't-bot') + }) + afterEach(() => { + robot.shutdown() + }) + it('matches messages starting with robot\'s name', () => { + const testMessage = robot.name + 'message123' + const testRegex = /(.*)/ + + const pattern = robot.respondPattern(testRegex) + assert.match(testMessage, pattern) + const match = testMessage.match(pattern)[1] + assert.equal(match, 'message123') + }) + it("matches messages starting with robot's alias", () => { + const testMessage = robot.alias + 'message123' + const testRegex = /(.*)/ + + const pattern = robot.respondPattern(testRegex) + assert.match(testMessage, pattern) + const match = testMessage.match(pattern)[1] + assert.equal(match, 'message123') + }) - it('creates a new ScopedHttpClient', function () { - // 'instanceOf' check doesn't work here due to the design of - // ScopedHttpClient - expect(this.httpClient).to.have.property('get') - expect(this.httpClient).to.have.property('post') - }) + it('does not match unaddressed messages', () => { + const testMessage = 'message123' + const testRegex = /(.*)/ - it('passes options through to the ScopedHttpClient', function () { - const agent = {} - const httpClient = this.robot.http('http://localhost', { agent }) - expect(httpClient.options.agent).to.equal(agent) - }) + const pattern = robot.respondPattern(testRegex) + assert.doesNotMatch(testMessage, pattern) + }) - it('sets a sane user agent', function () { - expect(this.httpClient.options.headers['User-Agent']).to.contain('Hubot') - }) + it('matches properly when name is substring of alias', () => { + robot.name = 'Meg' + robot.alias = 'Megan' + const testMessage1 = robot.name + ' message123' + const testMessage2 = robot.alias + ' message123' + const testRegex = /(.*)/ - it('merges in any global http options', function () { - const agent = {} - this.robot.globalHttpOptions = { agent } - const httpClient = this.robot.http('http://localhost') - expect(httpClient.options.agent).to.equal(agent) - }) + const pattern = robot.respondPattern(testRegex) - it('local options override global http options', function () { - const agentA = {} - const agentB = {} - this.robot.globalHttpOptions = { agent: agentA } - const httpClient = this.robot.http('http://localhost', { agent: agentB }) - expect(httpClient.options.agent).to.equal(agentB) - }) + assert.match(testMessage1, pattern) + const match1 = testMessage1.match(pattern)[1] + assert.equal(match1, 'message123') - it('builds the url correctly from a string', function () { - const options = this.httpClient.buildOptions('http://localhost:3001') - expect(options.host).to.equal('localhost:3001') - expect(options.pathname).to.equal('/') - expect(options.protocol).to.equal('http:') - expect(options.port).to.equal('3001') - }) + assert.match(testMessage2, pattern) + const match2 = testMessage2.match(pattern)[1] + assert.equal(match2, 'message123') }) - describe('#respondPattern', function () { - it('matches messages starting with robot\'s name', function () { - const testMessage = this.robot.name + 'message123' - const testRegex = /(.*)/ + it('matches properly when alias is substring of name', () => { + robot.name = 'Megan' + robot.alias = 'Meg' + const testMessage1 = robot.name + ' message123' + const testMessage2 = robot.alias + ' message123' + const testRegex = /(.*)/ - const pattern = this.robot.respondPattern(testRegex) - expect(testMessage).to.match(pattern) - const match = testMessage.match(pattern)[1] - expect(match).to.equal('message123') - }) + const pattern = robot.respondPattern(testRegex) - it('matches messages starting with robot\'s alias', function () { - const testMessage = this.robot.alias + 'message123' - const testRegex = /(.*)/ + assert.match(testMessage1, pattern) + const match1 = testMessage1.match(pattern)[1] + assert.equal(match1, 'message123') - const pattern = this.robot.respondPattern(testRegex) - expect(testMessage).to.match(pattern) - const match = testMessage.match(pattern)[1] - expect(match).to.equal('message123') - }) + assert.match(testMessage2, pattern) + const match2 = testMessage2.match(pattern)[1] + assert.equal(match2, 'message123') + }) + }) + describe('Listening API', () => { + let robot = null + beforeEach(() => { + robot = new Robot(null, false, 'TestHubot') + }) + afterEach(() => { + robot.shutdown() + }) + it('#listen: registers a new listener directly', () => { + assert.equal(robot.listeners.length, 0) + robot.listen(() => {}, () => {}) + assert.equal(robot.listeners.length, 1) + }) - it('does not match unaddressed messages', function () { - const testMessage = 'message123' - const testRegex = /(.*)/ + it('#hear: registers a new listener directly', () => { + assert.equal(robot.listeners.length, 0) + robot.hear(/.*/, () => {}) + assert.equal(robot.listeners.length, 1) + }) - const pattern = this.robot.respondPattern(testRegex) - expect(testMessage).to.not.match(pattern) - }) + it('#respond: registers a new listener using respond', () => { + assert.equal(robot.listeners.length, 0) + robot.respond(/.*/, () => {}) + assert.equal(robot.listeners.length, 1) + }) - it('matches properly when name is substring of alias', function () { - this.robot.name = 'Meg' - this.robot.alias = 'Megan' - const testMessage1 = this.robot.name + ' message123' - const testMessage2 = this.robot.alias + ' message123' - const testRegex = /(.*)/ + it('#enter: registers a new listener using listen', () => { + assert.equal(robot.listeners.length, 0) + robot.enter(() => {}) + assert.equal(robot.listeners.length, 1) + }) - const pattern = this.robot.respondPattern(testRegex) + it('#leave: registers a new listener using listen', () => { + assert.equal(robot.listeners.length, 0) + robot.leave(() => {}) + assert.equal(robot.listeners.length, 1) + }) + it('#topic: registers a new listener using listen', () => { + assert.equal(robot.listeners.length, 0) + robot.topic(() => {}) + assert.equal(robot.listeners.length, 1) + }) - expect(testMessage1).to.match(pattern) - const match1 = testMessage1.match(pattern)[1] - expect(match1).to.equal('message123') + it('#catchAll: registers a new listener using listen', () => { + assert.equal(robot.listeners.length, 0) + robot.catchAll(() => {}) + assert.equal(robot.listeners.length, 1) + }) + }) + describe('#receive', () => { + let robot = null + let user = null + beforeEach(() => { + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + user = new User('1', { name: 'node', room: '#test' }) + }) + afterEach(() => { + robot.shutdown() + }) + it('calls all registered listeners', async () => { + // Need to use a real Message so that the CatchAllMessage constructor works + const testMessage = new TextMessage(user, 'message123') + let counter = 0 + const listener = async message => { + counter++ + } + robot.listen(() => true, null, listener) + robot.listen(() => true, null, listener) + robot.listen(() => true, null, listener) + robot.listen(() => true, null, listener) + await robot.receive(testMessage) + assert.equal(counter, 4) + }) - expect(testMessage2).to.match(pattern) - const match2 = testMessage2.match(pattern)[1] - expect(match2).to.equal('message123') + it('sends a CatchAllMessage if no listener matches', async () => { + const testMessage = new TextMessage(user, 'message123') + robot.listeners = [] + robot.catchAll(async (message) => { + assert.ok(message instanceof CatchAllMessage) + assert.deepEqual(message.message, testMessage) }) + await robot.receive(testMessage) + }) - it('matches properly when alias is substring of name', function () { - this.robot.name = 'Megan' - this.robot.alias = 'Meg' - const testMessage1 = this.robot.name + ' message123' - const testMessage2 = this.robot.alias + ' message123' - const testRegex = /(.*)/ - - const pattern = this.robot.respondPattern(testRegex) + it('does not trigger a CatchAllMessage if a listener matches', async () => { + const testMessage = new TextMessage(user, 'message123') - expect(testMessage1).to.match(pattern) - const match1 = testMessage1.match(pattern)[1] - expect(match1).to.equal('message123') + const matchingListener = async response => { + assert.deepEqual(response.message, testMessage) + } - expect(testMessage2).to.match(pattern) - const match2 = testMessage2.match(pattern)[1] - expect(match2).to.equal('message123') + robot.listen(() => true, null, matchingListener) + robot.catchAll(null, () => { + throw new Error('Should not have triggered catchAll') }) + await robot.receive(testMessage) }) - describe('#listen', () => - it('registers a new listener directly', function () { - expect(this.robot.listeners).to.have.length(0) - this.robot.listen(function () {}, function () {}) - expect(this.robot.listeners).to.have.length(1) - }) - ) - - describe('#hear', () => - it('registers a new listener directly', function () { - expect(this.robot.listeners).to.have.length(0) - this.robot.hear(/.*/, function () {}) - expect(this.robot.listeners).to.have.length(1) - }) - ) + it('stops processing if a listener marks the message as done', async () => { + const testMessage = new TextMessage(user, 'message123') - describe('#respond', () => - it('registers a new listener using hear', function () { - sinon.spy(this.robot, 'hear') - this.robot.respond(/.*/, function () {}) - expect(this.robot.hear).to.have.been.called - }) - ) - - describe('#enter', () => - it('registers a new listener using listen', function () { - sinon.spy(this.robot, 'listen') - this.robot.enter(function () {}) - expect(this.robot.listen).to.have.been.called - }) - ) + const matchingListener = async response => { + response.message.done = true + assert.deepEqual(response.message, testMessage) + } + const listenerSpy = async message => { + assert.fail('Should not have triggered listener') + } + robot.listen(() => true, null, matchingListener) + robot.listen(() => true, null, listenerSpy) + await robot.receive(testMessage) + }) - describe('#leave', () => - it('registers a new listener using listen', function () { - sinon.spy(this.robot, 'listen') - this.robot.leave(function () {}) - expect(this.robot.listen).to.have.been.called - }) - ) + it('gracefully handles listener uncaughtExceptions (move on to next listener)', async () => { + const testMessage = {} + const theError = new Error('Expected error') - describe('#topic', () => - it('registers a new listener using listen', function () { - sinon.spy(this.robot, 'listen') - this.robot.topic(function () {}) - expect(this.robot.listen).to.have.been.called - }) - ) + const badListener = async () => { + throw theError + } - describe('#catchAll', () => - it('registers a new listener using listen', function () { - sinon.spy(this.robot, 'listen') - this.robot.catchAll(function () {}) - expect(this.robot.listen).to.have.been.called - }) - ) - - describe('#receive', function () { - it('calls all registered listeners', async function () { - // Need to use a real Message so that the CatchAllMessage constructor works - const testMessage = new TextMessage(this.user, 'message123') - let counter = 0 - const listener = async message => { - counter++ - } - this.robot.listen(() => true, null, listener) - this.robot.listen(() => true, null, listener) - this.robot.listen(() => true, null, listener) - this.robot.listen(() => true, null, listener) - await this.robot.receive(testMessage) - expect(counter).to.equal(4) - }) + let goodListenerCalled = false + const goodListener = async message => { + goodListenerCalled = true + } - // TODO: catchAll doesn't take a function for first arg - it('sends a CatchAllMessage if no listener matches', async function () { - const testMessage = new TextMessage(this.user, 'message123') - this.robot.listeners = [] - this.robot.catchAll(null, async (message) => { - expect(message).to.be.instanceof(CatchAllMessage) - expect(message.message).to.be.equal(testMessage) - }) - await this.robot.receive(testMessage) + robot.listen(() => true, null, badListener) + robot.listen(() => true, null, goodListener) + robot.on('error', (err, response) => { + assert.deepEqual(err, theError) + assert.deepEqual(response.message, testMessage) }) + await robot.receive(testMessage) + assert.ok(goodListenerCalled) + }) + }) + describe('#loadFile', () => { + let robot = null + beforeEach(() => { + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + }) + afterEach(() => { + robot.shutdown() + process.removeAllListeners() + }) + it('should require the specified file', async () => { + await robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.js') + assert.deepEqual(robot.hasLoadedTestJsScript, true) + }) - it('does not trigger a CatchAllMessage if a listener matches', async function () { - const testMessage = new TextMessage(this.user, 'message123') - - const matchingListener = async response => { - expect(response.message).to.be.equal(testMessage) - } + it('should load an .mjs file', async () => { + await robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.mjs') + assert.deepEqual(robot.hasLoadedTestMjsScript, true) + }) - this.robot.listen(() => true, null, matchingListener) - this.robot.catchAll(null, () => { - throw new Error('Should not have triggered catchAll') - }) - await this.robot.receive(testMessage) + describe('proper script', () => { + it('should parse the script documentation', async () => { + await robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.js') + assert.deepEqual(robot.helpCommands(), ['hubot test - Responds with a test response']) }) + }) - it('stops processing if a listener marks the message as done', async function () { - const testMessage = new TextMessage(this.user, 'message123') - - const matchingListener = async response => { - response.message.done = true - expect(response.message).to.be.equal(testMessage) - } - const listenerSpy = async message => { - expect.fail('Should not have triggered listener') + describe('non-Function script', () => { + it('logs a warning for a .js file that does not export the correct API', async () => { + let wasCalled = false + robot.logger.warning = (...args) => { + wasCalled = true + assert.ok(args) } - this.robot.listen(() => true, null, matchingListener) - this.robot.listen(() => true, null, listenerSpy) - await this.robot.receive(testMessage) + await robot.loadFile(path.resolve('./test/fixtures'), 'TestScriptIncorrectApi.js') + assert.deepEqual(wasCalled, true) }) - it('gracefully handles listener uncaughtExceptions (move on to next listener)', async function () { - const testMessage = {} - const theError = new Error() - - const badListener = async () => { - throw theError - } - - let goodListenerCalled = false - const goodListener = async message => { - goodListenerCalled = true + it('logs a warning for a .mjs file that does not export the correct API', async () => { + let wasCalled = false + robot.logger.warning = (...args) => { + wasCalled = true + assert.ok(args) } - - this.robot.listen(() => true, null, badListener) - this.robot.listen(() => true, null, goodListener) - this.robot.on('error', (err, response) => { - expect(err).to.equal(theError) - expect(response.message).to.equal(testMessage) - }) - await this.robot.receive(testMessage) - expect(goodListenerCalled).to.be.ok + await robot.loadFile(path.resolve('./test/fixtures'), 'TestScriptIncorrectApi.mjs') + assert.deepEqual(wasCalled, true) }) }) - describe('#loadFile', function () { - beforeEach(function () { - this.sandbox = sinon.createSandbox() - }) - - afterEach(function () { - this.sandbox.restore() - }) - - it('should require the specified file', async function () { - await this.robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.js') - expect(this.robot.hasLoadedTestJsScript).to.be.true - }) - - it('should load an .mjs file', async function () { - await this.robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.mjs') - expect(this.robot.hasLoadedTestMjsScript).to.be.true - }) - - describe('proper script', function () { - it('should parse the script documentation', async function () { - await this.robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.js') - expect(this.robot.helpCommands()).to.eql(['hubot test - Responds with a test response']) - }) - }) - - describe('non-Function script', function () { - it('logs a warning for a .js file that does not export the correct API', async function () { - sinon.stub(this.robot.logger, 'warning') - await this.robot.loadFile(path.resolve('./test/fixtures'), 'TestScriptIncorrectApi.js') - expect(this.robot.logger.warning).to.have.been.called - }) - - it('logs a warning for a .mjs file that does not export the correct API', async function () { - sinon.stub(this.robot.logger, 'warning') - await this.robot.loadFile(path.resolve('./test/fixtures'), 'TestScriptIncorrectApi.mjs') - expect(this.robot.logger.warning).to.have.been.called - }) - }) - - describe('unsupported file extension', function () { - it('should not be loaded by the Robot', async function () { - sinon.spy(this.robot.logger, 'debug') - await this.robot.loadFile(path.resolve('./test/fixtures'), 'unsupported.yml') - expect(this.robot.logger.debug).to.have.been.calledWithMatch(/unsupported file type/) - }) + describe('unsupported file extension', () => { + it('should not be loaded by the Robot', async () => { + let wasCalled = false + robot.logger.debug = (...args) => { + wasCalled = true + assert.match(args[0], /unsupported file type/) + } + await robot.loadFile(path.resolve('./test/fixtures'), 'unsupported.yml') + assert.deepEqual(wasCalled, true) }) }) + }) - describe('#send', function () { - beforeEach(function () { - sinon.spy(this.robot.adapter, 'send') - }) - - it('delegates to adapter "send" with proper context', async function () { - await this.robot.send({}, 'test message') - expect(this.robot.adapter.send).to.have.been.calledOn(this.robot.adapter) - }) + describe('Sending API', () => { + let robot = null + beforeEach(async () => { + hook('hubot-mock-adapter', mockAdapter) + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + await robot.loadAdapter() + robot.run() }) - - describe('#reply', function () { - beforeEach(function () { - sinon.spy(this.robot.adapter, 'reply') - }) - - it('delegates to adapter "reply" with proper context', async function () { - await this.robot.reply({}, 'test message') - expect(this.robot.adapter.reply).to.have.been.calledOn(this.robot.adapter) - }) + afterEach(() => { + robot.shutdown() + reset() }) - describe('#messageRoom', function () { - beforeEach(function () { - sinon.spy(this.robot.adapter, 'send') - }) - - it('delegates to adapter "send" with proper context', async function () { - await this.robot.messageRoom('testRoom', 'messageRoom test') - expect(this.robot.adapter.send).to.have.been.calledOn(this.robot.adapter) - }) + it('#send: delegates to adapter "send" with proper context', async () => { + let wasCalled = false + robot.adapter.send = async (envelop, ...strings) => { + wasCalled = true + assert.deepEqual(strings, ['test message'], 'The strings should be passed through.') + } + await robot.send({}, 'test message') + assert.deepEqual(wasCalled, true) }) - describe('#on', function () { - beforeEach(function () { - sinon.spy(this.robot.events, 'on') - }) - - it('delegates to events "on" with proper context', function () { - this.robot.on('event', function () {}) - expect(this.robot.events.on).to.have.been.calledOn(this.robot.events) - }) + it('#reply: delegates to adapter "reply" with proper context', async () => { + let wasCalled = false + robot.adapter.reply = async (envelop, ...strings) => { + assert.deepEqual(strings, ['test message'], 'The strings should be passed through.') + wasCalled = true + } + await robot.reply({}, 'test message') + assert.deepEqual(wasCalled, true) }) - describe('#emit', function () { - beforeEach(function () { - sinon.spy(this.robot.events, 'emit') - }) - - it('delegates to events "emit" with proper context', function () { - this.robot.emit('event', function () {}) - expect(this.robot.events.emit).to.have.been.calledOn(this.robot.events) - }) + it('#messageRoom: delegates to adapter "send" with proper context', async () => { + let wasCalled = false + robot.adapter.send = async (envelop, ...strings) => { + assert.equal(envelop.room, 'testRoom', 'The room should be passed through.') + assert.deepEqual(strings, ['messageRoom test'], 'The strings should be passed through.') + wasCalled = true + } + await robot.messageRoom('testRoom', 'messageRoom test') + assert.deepEqual(wasCalled, true) }) }) + describe('Listener Registration', () => { + let robot = null + let user = null + beforeEach(() => { + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + user = new User('1', { name: 'node', room: '#test' }) + }) + afterEach(() => { + robot.shutdown() + }) + it('#listen: forwards the matcher, options, and callback to Listener', () => { + const callback = async () => {} + const matcher = () => {} + const options = {} - describe('Listener Registration', function () { - describe('#listen', () => - it('forwards the matcher, options, and callback to Listener', function () { - const callback = sinon.spy() - const matcher = sinon.spy() - const options = {} - - this.robot.listen(matcher, options, callback) - const testListener = this.robot.listeners[0] + robot.listen(matcher, options, callback) + const testListener = robot.listeners[0] - expect(testListener.matcher).to.equal(matcher) - expect(testListener.callback).to.equal(callback) - expect(testListener.options).to.equal(options) - }) - ) + assert.deepEqual(testListener.matcher, matcher) + assert.deepEqual(testListener.callback, callback) + assert.deepEqual(testListener.options, options) + }) - describe('#hear', function () { - it('matches TextMessages', function () { - const callback = sinon.spy() - const testMessage = new TextMessage(this.user, 'message123') - const testRegex = /^message123$/ + it('#hear: matches TextMessages', () => { + const callback = async () => {} + const testMessage = new TextMessage(user, 'message123') + const testRegex = /^message123$/ - this.robot.hear(testRegex, callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.hear(testRegex, callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.be.ok - }) + assert.ok(result) + }) - it('does not match EnterMessages', function () { - const callback = sinon.spy() - const testMessage = new EnterMessage(this.user) - const testRegex = /.*/ + it('does not match EnterMessages', () => { + const callback = async () => {} + const testMessage = new EnterMessage(user) + const testRegex = /.*/ - this.robot.hear(testRegex, callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.hear(testRegex, callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.not.be.ok - }) + assert.deepEqual(result, undefined) }) - describe('#respond', function () { - it('matches TextMessages addressed to the robot', function () { - const callback = sinon.spy() - const testMessage = new TextMessage(this.user, 'TestHubot message123') - const testRegex = /message123$/ + it('#respond: matches TextMessages addressed to the robot', () => { + const callback = async () => {} + const testMessage = new TextMessage(user, 'TestHubot message123') + const testRegex = /message123$/ - this.robot.respond(testRegex, callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.respond(testRegex, callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.be.ok - }) + assert.ok(result) + }) - it('does not match EnterMessages', function () { - const callback = sinon.spy() - const testMessage = new EnterMessage(this.user) - const testRegex = /.*/ + it('does not match EnterMessages', () => { + const callback = async () => {} + const testMessage = new EnterMessage(user) + const testRegex = /.*/ - this.robot.respond(testRegex, callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.respond(testRegex, callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.not.be.ok - }) + assert.deepEqual(result, undefined) }) + it('#enter: matches EnterMessages', () => { + const callback = async () => {} + const testMessage = new EnterMessage(user) - describe('#enter', function () { - it('matches EnterMessages', function () { - const callback = sinon.spy() - const testMessage = new EnterMessage(this.user) + robot.enter(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - this.robot.enter(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) - - expect(result).to.be.ok - }) + assert.ok(result) + }) - it('does not match TextMessages', function () { - const callback = sinon.spy() - const testMessage = new TextMessage(this.user, 'message123') + it('does not match TextMessages', () => { + const callback = async () => {} + const testMessage = new TextMessage(user, 'message123') - this.robot.enter(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.enter(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.not.be.ok - }) + assert.deepEqual(result, false) }) - describe('#leave', function () { - it('matches LeaveMessages', function () { - const callback = sinon.spy() - const testMessage = new LeaveMessage(this.user) + it('#leave: matches LeaveMessages', () => { + const callback = async () => {} + const testMessage = new LeaveMessage(user) - this.robot.leave(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.leave(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.be.ok - }) + assert.ok(result) + }) - it('does not match TextMessages', function () { - const callback = sinon.spy() - const testMessage = new TextMessage(this.user, 'message123') + it('does not match TextMessages', () => { + const callback = async () => {} + const testMessage = new TextMessage(user, 'message123') - this.robot.leave(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.leave(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.not.be.ok - }) + assert.deepEqual(result, false) }) + it('#topic: matches TopicMessages', () => { + const callback = async () => {} + const testMessage = new TopicMessage(user) - describe('#topic', function () { - it('matches TopicMessages', function () { - const callback = sinon.spy() - const testMessage = new TopicMessage(this.user) + robot.topic(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - this.robot.topic(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) - - expect(result).to.be.ok - }) + assert.deepEqual(result, true) + }) - it('does not match TextMessages', function () { - const callback = sinon.spy() - const testMessage = new TextMessage(this.user, 'message123') + it('does not match TextMessages', () => { + const callback = async () => {} + const testMessage = new TextMessage(user, 'message123') - this.robot.topic(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.topic(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.not.be.ok - }) + assert.deepEqual(result, false) }) - describe('#catchAll', function () { - it('matches CatchAllMessages', function () { - const callback = sinon.spy() - const testMessage = new CatchAllMessage(new TextMessage(this.user, 'message123')) + it('#catchAll: matches CatchAllMessages', () => { + const callback = async () => {} + const testMessage = new CatchAllMessage(new TextMessage(user, 'message123')) - this.robot.catchAll(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.catchAll(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.be.ok - }) + assert.deepEqual(result, true) + }) - it('does not match TextMessages', function () { - const callback = sinon.spy() - const testMessage = new TextMessage(this.user, 'message123') + it('does not match TextMessages', () => { + const callback = async () => {} + const testMessage = new TextMessage(user, 'message123') - this.robot.catchAll(callback) - const testListener = this.robot.listeners[0] - const result = testListener.matcher(testMessage) + robot.catchAll(callback) + const testListener = robot.listeners[0] + const result = testListener.matcher(testMessage) - expect(result).to.not.be.ok - }) + assert.deepEqual(result, false) }) }) - - describe('Message Processing', function () { - it('calls a matching listener', async function () { - const testMessage = new TextMessage(this.user, 'message123') - this.robot.hear(/^message123$/, async function (response) { - expect(response.message).to.equal(testMessage) + describe('Message Processing', () => { + let robot = null + let user = null + beforeEach(() => { + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + user = new User('1', { name: 'node', room: '#test' }) + }) + afterEach(() => { + robot.shutdown() + }) + it('calls a matching listener', async () => { + const testMessage = new TextMessage(user, 'message123') + robot.hear(/^message123$/, async response => { + assert.deepEqual(response.message, testMessage) }) - await this.robot.receive(testMessage) + await robot.receive(testMessage) }) - it('calls multiple matching listeners', async function () { - const testMessage = new TextMessage(this.user, 'message123') + it('calls multiple matching listeners', async () => { + const testMessage = new TextMessage(user, 'message123') let listenersCalled = 0 - const listenerCallback = function (response) { - expect(response.message).to.equal(testMessage) + const listenerCallback = async response => { + assert.deepEqual(response.message, testMessage) listenersCalled++ } - this.robot.hear(/^message123$/, listenerCallback) - this.robot.hear(/^message123$/, listenerCallback) + robot.hear(/^message123$/, listenerCallback) + robot.hear(/^message123$/, listenerCallback) - await this.robot.receive(testMessage) - expect(listenersCalled).to.equal(2) + await robot.receive(testMessage) + assert.equal(listenersCalled, 2) }) - it('calls the catch-all listener if no listeners match', async function () { - const testMessage = new TextMessage(this.user, 'message123') + it('calls the catch-all listener if no listeners match', async () => { + const testMessage = new TextMessage(user, 'message123') - const listenerCallback = sinon.spy() - this.robot.hear(/^no-matches$/, listenerCallback) + const listenerCallback = async () => { + assert.fail('Should not have called listener') + } + robot.hear(/^no-matches$/, listenerCallback) - this.robot.catchAll(async function (response) { - expect(listenerCallback).to.not.have.been.called - expect(response.message).to.equal(testMessage) + robot.catchAll(async response => { + assert.deepEqual(response.message, testMessage) }) - await this.robot.receive(testMessage) + await robot.receive(testMessage) }) - it('does not call the catch-all listener if any listener matched', async function () { - const testMessage = new TextMessage(this.user, 'message123') - - const listenerCallback = sinon.spy() - this.robot.hear(/^message123$/, listenerCallback) + it('does not call the catch-all listener if any listener matched', async () => { + const testMessage = new TextMessage(user, 'message123') + let counter = 0 + const listenerCallback = async () => { + counter++ + } + robot.hear(/^message123$/, listenerCallback) - const catchAllCallback = sinon.spy() - this.robot.catchAll(catchAllCallback) + const catchAllCallback = async () => { + assert.fail('Should not have been called') + } + robot.catchAll(catchAllCallback) - await this.robot.receive(testMessage) - expect(listenerCallback).to.have.been.calledOnce - expect(catchAllCallback).to.not.have.been.called + await robot.receive(testMessage) + assert.equal(counter, 1) }) - it('stops processing if message.finish() is called synchronously', async function () { - const testMessage = new TextMessage(this.user, 'message123') - - this.robot.hear(/^message123$/, response => response.message.finish()) + it('stops processing if message.finish() is called synchronously', async () => { + const testMessage = new TextMessage(user, 'message123') - const listenerCallback = sinon.spy() - this.robot.hear(/^message123$/, listenerCallback) + robot.hear(/^message123$/, async response => response.message.finish()) + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + assert.fail('Should not have been called') + } + robot.hear(/^message123$/, listenerCallback) - await this.robot.receive(testMessage) - expect(listenerCallback).to.not.have.been.called + await robot.receive(testMessage) + assert.equal(wasCalled, false) }) - it('calls non-TextListener objects', async function () { - const testMessage = new EnterMessage(this.user) + it('calls non-TextListener objects', async () => { + const testMessage = new EnterMessage(user) - this.robot.enter(function (response) { - expect(response.message).to.equal(testMessage) + robot.enter(async response => { + assert.deepEqual(response.message, testMessage) }) - await this.robot.receive(testMessage) + await robot.receive(testMessage) }) - it('gracefully handles hearer uncaughtExceptions (move on to next hearer)', async function () { - const testMessage = new TextMessage(this.user, 'message123') - const theError = new Error() + it('gracefully handles hearer uncaughtExceptions (move on to next hearer)', async () => { + const testMessage = new TextMessage(user, 'message123') + const theError = new Error('Expected error to be thrown') - this.robot.hear(/^message123$/, async function () { + robot.hear(/^message123$/, async () => { throw theError }) let goodListenerCalled = false - this.robot.hear(/^message123$/, async response => { + robot.hear(/^message123$/, async response => { goodListenerCalled = true }) - this.robot.on('error', (err, response) => { - expect(err).to.equal(theError) - expect(response.message).to.equal(testMessage) + robot.on('error', (err, response) => { + assert.deepEqual(err, theError) + assert.deepEqual(response.message, testMessage) }) - await this.robot.receive(testMessage) - expect(goodListenerCalled).to.be.ok + await robot.receive(testMessage) + assert.deepEqual(goodListenerCalled, true) }) + }) + describe('Listener Middleware', () => { + let robot = null + let user = null + beforeEach(() => { + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + user = new User('1', { name: 'node', room: '#test' }) + }) + afterEach(() => { + robot.shutdown() + }) + it('allows listener callback execution', async () => { + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + } + robot.hear(/^message123$/, listenerCallback) + robot.listenerMiddleware(async context => true) - describe('Listener Middleware', function () { - it('allows listener callback execution', async function () { - const listenerCallback = sinon.spy() - this.robot.hear(/^message123$/, listenerCallback) - this.robot.listenerMiddleware(async context => true) - - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - expect(listenerCallback).to.have.been.called - }) - - it('can block listener callback execution', async function () { - const listenerCallback = sinon.spy() - this.robot.hear(/^message123$/, listenerCallback) - this.robot.listenerMiddleware(async context => false) + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + assert.deepEqual(wasCalled, true) + }) - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - expect(listenerCallback).to.not.have.been.called - }) + it('can block listener callback execution', async () => { + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + assert.fail('Should not have been called') + } + robot.hear(/^message123$/, listenerCallback) + robot.listenerMiddleware(async context => false) - it('receives the correct arguments', async function () { - this.robot.hear(/^message123$/, function () {}) - const testListener = this.robot.listeners[0] - const testMessage = new TextMessage(this.user, 'message123') + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + assert.deepEqual(wasCalled, false) + }) - this.robot.listenerMiddleware(async context => { - // Escape middleware error handling for clearer test failures - expect(context.listener).to.equal(testListener) - expect(context.response.message).to.equal(testMessage) - return true - }) + it('receives the correct arguments', async () => { + robot.hear(/^message123$/, async () => {}) + const testListener = robot.listeners[0] + const testMessage = new TextMessage(user, 'message123') - await this.robot.receive(testMessage) + robot.listenerMiddleware(async context => { + // Escape middleware error handling for clearer test failures + assert.deepEqual(context.listener, testListener) + assert.deepEqual(context.response.message, testMessage) + return true }) - it('executes middleware in order of definition', async function () { - const execution = [] + await robot.receive(testMessage) + }) - const testMiddlewareA = async context => { - execution.push('middlewareA') - } + it('executes middleware in order of definition', async () => { + const execution = [] - const testMiddlewareB = async context => { - execution.push('middlewareB') - } + const testMiddlewareA = async context => { + execution.push('middlewareA') + } - this.robot.listenerMiddleware(testMiddlewareA) - this.robot.listenerMiddleware(testMiddlewareB) + const testMiddlewareB = async context => { + execution.push('middlewareB') + } - this.robot.hear(/^message123$/, () => execution.push('listener')) + robot.listenerMiddleware(testMiddlewareA) + robot.listenerMiddleware(testMiddlewareB) - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - execution.push('done') - expect(execution).to.deep.equal([ - 'middlewareA', - 'middlewareB', - 'listener', - 'done' - ]) - }) - }) + robot.hear(/^message123$/, () => execution.push('listener')) - describe('Receive Middleware', function () { - it('fires for all messages, including non-matching ones', async function () { - const middlewareSpy = sinon.spy() - const listenerCallback = sinon.spy() - this.robot.hear(/^message123$/, listenerCallback) - this.robot.receiveMiddleware(async context => { - middlewareSpy() - }) + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + execution.push('done') + assert.deepEqual(execution, [ + 'middlewareA', + 'middlewareB', + 'listener', + 'done' + ]) + }) + }) + describe('Receive Middleware', () => { + let robot = null + let user = null + beforeEach(() => { + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + user = new User('1', { name: 'node', room: '#test' }) + }) + afterEach(() => { + robot.shutdown() + }) + it('fires for all messages, including non-matching ones', async () => { + let middlewareWasCalled = false + const middlewareSpy = async () => { + middlewareWasCalled = true + } + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + assert.fail('Should not have been called') + } + robot.hear(/^message123$/, listenerCallback) + robot.receiveMiddleware(async context => { + middlewareSpy() + }) - const testMessage = new TextMessage(this.user, 'not message 123') + const testMessage = new TextMessage(user, 'not message 123') - await this.robot.receive(testMessage) - expect(listenerCallback).to.not.have.been.called - expect(middlewareSpy).to.have.been.called - }) + await robot.receive(testMessage) + assert.deepEqual(wasCalled, false) + assert.deepEqual(middlewareWasCalled, true) + }) - it('can block listener execution', async function () { - const middlewareSpy = sinon.spy() - const listenerCallback = sinon.spy() - this.robot.hear(/^message123$/, listenerCallback) - this.robot.receiveMiddleware(async context => { - middlewareSpy() - return false - }) - - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - expect(listenerCallback).to.not.have.been.called - expect(middlewareSpy).to.have.been.called + it('can block listener execution', async () => { + let middlewareWasCalled = false + const middlewareSpy = async () => { + middlewareWasCalled = true + } + let wasCalled = false + const listenerCallback = async () => { + wasCalled = true + assert.fail('Should not have been called') + } + robot.hear(/^message123$/, listenerCallback) + robot.receiveMiddleware(async context => { + middlewareSpy() + return false }) - it('receives the correct arguments', async function () { - this.robot.hear(/^message123$/, function () {}) - const testMessage = new TextMessage(this.user, 'message123') + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + assert.deepEqual(wasCalled, false) + assert.deepEqual(middlewareWasCalled, true) + }) - this.robot.receiveMiddleware(async context => { - expect(context.response.message).to.equal(testMessage) - }) + it('receives the correct arguments', async () => { + robot.hear(/^message123$/, () => {}) + const testMessage = new TextMessage(user, 'message123') - await this.robot.receive(testMessage) + robot.receiveMiddleware(async context => { + assert.deepEqual(context.response.message, testMessage) }) - it('executes receive middleware in order of definition', async function () { - const execution = [] + await robot.receive(testMessage) + }) - const testMiddlewareA = async context => { - execution.push('middlewareA') - } + it('executes receive middleware in order of definition', async () => { + const execution = [] - const testMiddlewareB = async context => { - execution.push('middlewareB') - } + const testMiddlewareA = async context => { + execution.push('middlewareA') + } - this.robot.receiveMiddleware(testMiddlewareA) - this.robot.receiveMiddleware(testMiddlewareB) - this.robot.hear(/^message123$/, () => execution.push('listener')) - - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - execution.push('done') - expect(execution).to.deep.equal([ - 'middlewareA', - 'middlewareB', - 'listener', - 'done' - ]) - }) + const testMiddlewareB = async context => { + execution.push('middlewareB') + } - it('allows editing the message portion of the given response', async function () { - const testMiddlewareA = async context => { - context.response.message.text = 'foobar' - } + robot.receiveMiddleware(testMiddlewareA) + robot.receiveMiddleware(testMiddlewareB) + robot.hear(/^message123$/, () => execution.push('listener')) + + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + execution.push('done') + assert.deepEqual(execution, [ + 'middlewareA', + 'middlewareB', + 'listener', + 'done' + ]) + }) - const testMiddlewareB = async context => { - expect(context.response.message.text).to.equal('foobar') - } + it('allows editing the message portion of the given response', async () => { + const testMiddlewareA = async context => { + context.response.message.text = 'foobar' + } - this.robot.receiveMiddleware(testMiddlewareA) - this.robot.receiveMiddleware(testMiddlewareB) + const testMiddlewareB = async context => { + assert.equal(context.response.message.text, 'foobar') + } - const testCallback = sinon.spy() - // We'll never get to this if testMiddlewareA has not modified the message. - this.robot.hear(/^foobar$/, testCallback) + robot.receiveMiddleware(testMiddlewareA) + robot.receiveMiddleware(testMiddlewareB) + let wasCalled = false + const testCallback = () => { + wasCalled = true + } + // We'll never get to this if testMiddlewareA has not modified the message. + robot.hear(/^foobar$/, testCallback) - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - expect(testCallback).to.have.been.called - }) + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + assert.deepEqual(wasCalled, true) + }) + }) + describe('Response Middleware', () => { + let robot = null + let user = null + beforeEach(async () => { + hook('hubot-mock-adapter', mockAdapter) + robot = new Robot('hubot-mock-adapter', false, 'TestHubot') + user = new User('1', { name: 'node', room: '#test' }) + robot.alias = 'Hubot' + await robot.loadAdapter() + robot.run() + }) + afterEach(() => { + robot.shutdown() }) + it('executes response middleware in order', async () => { + let wasCalled = false + robot.adapter.send = async (envelope, ...strings) => { + assert.deepEqual(strings, ['replaced bar-foo, sir, replaced bar-foo.']) + wasCalled = true + } + robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.')) + + robot.responseMiddleware(async context => { + context.strings[0] = context.strings[0].replace(/foobar/g, 'barfoo') + }) - describe('Response Middleware', function () { - it('executes response middleware in order', async function () { - let sendSpy - this.robot.adapter.send = (sendSpy = sinon.spy()) - this.robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.')) + robot.responseMiddleware(async context => { + context.strings[0] = context.strings[0].replace(/barfoo/g, 'replaced bar-foo') + }) - this.robot.responseMiddleware(async context => { - context.strings[0] = context.strings[0].replace(/foobar/g, 'barfoo') - }) + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + assert.deepEqual(wasCalled, true) + }) - this.robot.responseMiddleware(async context => { - context.strings[0] = context.strings[0].replace(/barfoo/g, 'replaced bar-foo') - }) + it('allows replacing outgoing strings', async () => { + let wasCalled = false + robot.adapter.send = async (envelope, ...strings) => { + wasCalled = true + assert.deepEqual(strings, ['whatever I want.']) + } + robot.hear(/^message123$/, async response => response.send('foobar, sir, foobar.')) - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - expect(sendSpy.getCall(0).args[1]).to.equal('replaced bar-foo, sir, replaced bar-foo.') + robot.responseMiddleware(async context => { + context.strings = ['whatever I want.'] }) - it('allows replacing outgoing strings', async function () { - let sendSpy - this.robot.adapter.send = (sendSpy = sinon.spy()) - this.robot.hear(/^message123$/, response => response.send('foobar, sir, foobar.')) + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + assert.deepEqual(wasCalled, true) + }) - this.robot.responseMiddleware(async context => { - context.strings = ['whatever I want.'] - }) + it('marks plaintext as plaintext', async () => { + robot.adapter.send = async (envelope, ...strings) => { + assert.deepEqual(strings, ['foobar, sir, foobar.']) + } + robot.adapter.play = async (envelope, ...strings) => { + assert.deepEqual(strings, ['good luck with that']) + } - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - expect(sendSpy.getCall(0).args[1]).to.deep.equal('whatever I want.') - }) + robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.')) + robot.hear(/^message456$/, async response => await response.play('good luck with that')) - it('marks plaintext as plaintext', async function () { - const sendSpy = sinon.spy() - this.robot.adapter.send = sendSpy - this.robot.hear(/^message123$/, response => response.send('foobar, sir, foobar.')) - this.robot.hear(/^message456$/, response => response.play('good luck with that')) - - let method - let plaintext - this.robot.responseMiddleware(async context => { - method = context.method - plaintext = context.plaintext - }) - - const testMessage = new TextMessage(this.user, 'message123') - - await this.robot.receive(testMessage) - expect(plaintext).to.equal(true) - expect(method).to.equal('send') - const testMessage2 = new TextMessage(this.user, 'message456') - await this.robot.receive(testMessage2) - expect(plaintext).to.equal(undefined) - expect(method).to.equal('play') + let method + let plaintext + robot.responseMiddleware(async context => { + method = context.method + plaintext = context.plaintext }) - it('does not send trailing functions to middleware', async function () { - let sendSpy - this.robot.adapter.send = (sendSpy = sinon.spy()) - let asserted = false - this.robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.')) - - this.robot.responseMiddleware(async context => { - // We don't send the callback function to middleware, so it's not here. - expect(context.strings).to.deep.equal(['foobar, sir, foobar.']) - expect(context.method).to.equal('send') - asserted = true - }) - - const testMessage = new TextMessage(this.user, 'message123') - await this.robot.receive(testMessage) - expect(asserted).to.equal(true) - expect(sendSpy.getCall(0).args[1]).to.equal('foobar, sir, foobar.') - }) + const testMessage = new TextMessage(user, 'message123') + + await robot.receive(testMessage) + assert.deepEqual(plaintext, true) + assert.equal(method, 'send') + const testMessage2 = new TextMessage(user, 'message456') + await robot.receive(testMessage2) + assert.deepEqual(plaintext, undefined) + assert.equal(method, 'play') }) - }) -}) -describe('Robot Defaults', () => { - let robot = null - beforeEach(async () => { - process.env.EXPRESS_PORT = 0 - robot = new Robot(null, true, 'TestHubot') - robot.alias = 'Hubot' - await robot.loadAdapter() - robot.run() - }) - afterEach(() => { - robot.shutdown() - }) - it('should load the builtin shell adapter by default', async () => { - expect(robot.adapter.name).to.equal('Shell') - }) -}) + it('does not send trailing functions to middleware', async () => { + let wasCalled = false + robot.adapter.send = async (envelope, ...strings) => { + wasCalled = true + assert.deepEqual(strings, ['foobar, sir, foobar.']) + } -describe('Robot ES6', () => { - let robot = null - beforeEach(async () => { - process.env.EXPRESS_PORT = 0 - robot = new Robot('MockAdapter', true, 'TestHubot') - robot.alias = 'Hubot' - await robot.loadAdapter('./test/fixtures/MockAdapter.mjs') - await robot.loadFile(path.resolve('./test/fixtures/'), 'TestScript.js') - robot.run() - }) - afterEach(() => { - robot.shutdown() - }) - it('should load an ES6 module adapter from a file', async () => { - const { MockAdapter } = await import('./fixtures/MockAdapter.mjs') - expect(robot.adapter).to.be.an.instanceOf(MockAdapter) - expect(robot.adapter.name).to.equal('MockAdapter') - }) - it('should respond to a message', async () => { - const sent = (envelop, strings) => { - expect(strings).to.deep.equal(['test response']) - } - robot.adapter.on('send', sent) - await robot.adapter.receive(new TextMessage('tester', 'hubot test')) - }) -}) + let asserted = false + robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.')) -describe('Robot Coffeescript', () => { - let robot = null - beforeEach(async () => { - process.env.EXPRESS_PORT = 0 - robot = new Robot('MockAdapter', true, 'TestHubot') - robot.alias = 'Hubot' - await robot.loadAdapter('./test/fixtures/MockAdapter.coffee') - await robot.loadFile(path.resolve('./test/fixtures/'), 'TestScript.coffee') - robot.run() + robot.responseMiddleware(async context => { + // We don't send the callback function to middleware, so it's not here. + assert.deepEqual(context.strings, ['foobar, sir, foobar.']) + assert.equal(context.method, 'send') + asserted = true + }) + + const testMessage = new TextMessage(user, 'message123') + await robot.receive(testMessage) + assert.deepEqual(asserted, true) + assert.deepEqual(wasCalled, true) + }) }) - afterEach(() => { - robot.shutdown() + describe('Robot ES6', () => { + let robot = null + beforeEach(async () => { + robot = new Robot('MockAdapter', false, 'TestHubot') + robot.alias = 'Hubot' + await robot.loadAdapter('./test/fixtures/MockAdapter.mjs') + await robot.loadFile(path.resolve('./test/fixtures/'), 'TestScript.js') + robot.run() + }) + afterEach(() => { + robot.shutdown() + }) + it('should load an ES6 module adapter from a file', async () => { + const { MockAdapter } = await import('./fixtures/MockAdapter.mjs') + assert.ok(robot.adapter instanceof MockAdapter) + assert.equal(robot.adapter.name, 'MockAdapter') + }) + it('should respond to a message', async () => { + const sent = async (envelop, strings) => { + assert.deepEqual(strings, ['test response']) + } + robot.adapter.on('send', sent) + await robot.receive(new TextMessage('tester', 'hubot test')) + }) + }) + describe('Robot Coffeescript', () => { + let robot = null + beforeEach(async () => { + robot = new Robot('MockAdapter', false, 'TestHubot') + robot.alias = 'Hubot' + await robot.loadAdapter('./test/fixtures/MockAdapter.coffee') + await robot.loadFile(path.resolve('./test/fixtures/'), 'TestScript.coffee') + robot.run() + }) + afterEach(() => { + robot.shutdown() + }) + it('should load a CoffeeScript adapter from a file', async () => { + assert.equal(robot.adapter.name, 'MockAdapter') + }) + it('should load a coffeescript file and respond to a message', async () => { + const sent = async (envelop, strings) => { + assert.deepEqual(strings, ['test response from coffeescript']) + } + robot.adapter.on('send', sent) + await robot.receive(new TextMessage('tester', 'hubot test')) + }) }) - it('should load a CoffeeScript adapter from a file', async () => { - expect(robot.adapter.name).to.equal('MockAdapter') + describe('Robot Defaults', () => { + let robot = null + beforeEach(async () => { + robot = new Robot(null, false, 'TestHubot') + robot.alias = 'Hubot' + await robot.loadAdapter() + robot.run() + }) + afterEach(() => { + robot.shutdown() + }) + it('should load the builtin shell adapter by default', async () => { + assert.equal(robot.adapter.name, 'Shell') + }) }) - it('should load a coffeescript file and respond to a message', async () => { - const sent = (envelop, strings) => { - expect(strings).to.deep.equal(['test response from coffeescript']) - } - robot.adapter.on('send', sent) - await robot.adapter.receive(new TextMessage('tester', 'hubot test')) + describe('Robot HTTP Service', () => { + it('should start a web service', async () => { + process.env.PORT = 3000 + hook('hubot-mock-adapter', mockAdapter) + const robot = new Robot('hubot-mock-adapter', true, 'TestHubot') + await robot.loadAdapter() + robot.run() + const res = await fetch(`http://localhost:${process.env.PORT}/hubot/version`) + assert.equal(res.status, 404) + assert.match(await res.text(), /Cannot GET \/hubot\/version/ig) + robot.shutdown() + reset() + }) }) }) diff --git a/test/shell_test.js b/test/shell_test.js index f7b92d0d7..510efb793 100644 --- a/test/shell_test.js +++ b/test/shell_test.js @@ -1,73 +1,80 @@ 'use strict' -/* global describe, beforeEach, it */ - -const chai = require('chai') -const sinon = require('sinon') -chai.use(require('sinon-chai')) - -const expect = chai.expect +const { describe, it, beforeEach, afterEach } = require('node:test') +const assert = require('assert/strict') const Robot = require('../src/robot') - -describe('Shell Adapter', function () { - beforeEach(async function () { - this.robot = new Robot('shell', false, 'TestHubot') - await this.robot.loadAdapter() - this.robot.run() +const { TextMessage } = require('../src/message.js') +const User = require('../src/user.js') + +describe('Shell Adapter', () => { + let robot = null + beforeEach(async () => { + robot = new Robot('shell', false, 'TestHubot') + await robot.loadAdapter() + robot.run() }) - this.afterEach(function () { - this.robot.shutdown() + afterEach(() => { + robot.shutdown() }) - describe('Public API', function () { - beforeEach(function () { - this.adapter = this.robot.adapter + describe('Public API', () => { + let adapter = null + beforeEach(() => { + adapter = robot.adapter }) - it('assigns robot', function () { - expect(this.adapter.robot).to.equal(this.robot) + it('assigns robot', () => { + assert.deepEqual(adapter.robot, robot, 'The adapter should have a reference to the robot.') }) - it('sends a message', function () { - this.adapter.send = sinon.spy() - this.adapter.send({ room: 'general' }, 'hello') - - expect(this.adapter.send).to.have.been.calledWith({ room: 'general' }, 'hello') + it('sends a message', (t, done) => { + const old = console.log + console.log = (...args) => { + console.log = old + assert.deepEqual(args[0], '\x1b[1mhello\x1b[22m', 'Message should be outputed as bold to the console.') + done() + } + adapter.send({ room: 'general' }, 'hello') }) - it('emotes a message', function () { - this.adapter.send = sinon.spy() - this.adapter.emote({ room: 'general' }, 'hello') - - expect(this.adapter.send).to.have.been.calledWith({ room: 'general' }, '* hello') + it('emotes a message', (t, done) => { + const old = console.log + console.log = (...args) => { + console.log = old + assert.deepEqual(args[0], '\x1b[1m* hello\x1b[22m', 'Message should be bold and have an * in front.') + done() + } + adapter.emote({ room: 'general' }, 'hello') }) - it('replies to a message', function () { - this.adapter.send = sinon.spy() - this.adapter.reply({ room: 'general', user: { name: 'mocha' } }, 'hello') - - expect(this.adapter.send).to.have.been.calledWith({ room: 'general', user: { name: 'mocha' } }, 'mocha: hello') + it('replies to a message', (t, done) => { + const old = console.log + console.log = (...args) => { + console.log = old + assert.deepEqual(args[0], '\x1b[1mnode: hello\x1b[22m', 'The strings should be passed through.') + done() + } + adapter.reply({ room: 'general', user: { name: 'node' } }, 'hello') }) - it('runs the adapter and emits connected', function (done) { + it('runs the adapter and emits connected', (t, done) => { const connected = () => { - this.adapter.off('connected', connected) + adapter.off('connected', connected) done() } - this.adapter.on('connected', connected) - this.adapter.run() + adapter.on('connected', connected) + adapter.run() }) - }) - - it('dispatches received messages to the robot', function () { - this.robot.receive = sinon.spy() - this.adapter = this.robot.adapter - this.message = sinon.spy() - this.adapter.receive(this.message) - - expect(this.robot.receive).to.have.been.calledWith(this.message) + it('dispatches received messages to the robot', (t, done) => { + const message = new TextMessage(new User('node'), 'hello', 1) + robot.receive = (msg) => { + assert.deepEqual(msg, message, 'The message should be passed through.') + done() + } + adapter.receive(message) + }) }) }) diff --git a/test/user_test.js b/test/user_test.js index 6b5e0dfa4..2fb7587dc 100644 --- a/test/user_test.js +++ b/test/user_test.js @@ -1,8 +1,7 @@ 'use strict' -/* global describe, it */ - -const expect = require('chai').expect +const { describe, it } = require('node:test') +const assert = require('assert/strict') const User = require('../src/user') describe('User', () => @@ -10,20 +9,20 @@ describe('User', () => it('uses id as the default name', function () { const user = new User('hubot') - expect(user.name).to.equal('hubot') + assert.equal(user.name, 'hubot', 'User constructor should set name') }) it('sets attributes passed in', function () { const user = new User('hubot', { foo: 1, bar: 2 }) - expect(user.foo).to.equal(1) - expect(user.bar).to.equal(2) + assert.equal(user.foo, 1, 'Passing an object with attributes in the User constructor should set those attributes on the instance.') + assert.equal(user.bar, 2, 'Passing an object with attributes in the User constructor should set those attributes on the instance.') }) it('uses name attribute when passed in, not id', function () { const user = new User('hubot', { name: 'tobuh' }) - expect(user.name).to.equal('tobuh') + assert.equal(user.name, 'tobuh', 'Passing a name attribute in the User constructor should set the name attribute on the instance.') }) }) ) From a5dfd99730b1033db82718dd30f256e6fb25315e Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sun, 17 Sep 2023 18:05:59 -0500 Subject: [PATCH 2/9] BREAKING CHANGE: Bump Node support to v18 --- .github/workflows/nodejs-ubuntu.yml | 2 +- package-lock.json | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/nodejs-ubuntu.yml b/.github/workflows/nodejs-ubuntu.yml index 140baf492..9d9a042a1 100644 --- a/.github/workflows/nodejs-ubuntu.yml +++ b/.github/workflows/nodejs-ubuntu.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, latest] + node-version: [18.x, latest] steps: - uses: actions/checkout@v3 diff --git a/package-lock.json b/package-lock.json index 969d9c79f..4d39af5e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,8 @@ "standard": "^17.1.0" }, "engines": { - "node": "> 16.20.2", - "npm": "> 8.19.4" + "node": ">= 18", + "npm": ">= 9" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index afc9e3c94..081498bfa 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "standard": "^17.1.0" }, "engines": { - "node": "> 16.20.2", - "npm": "> 8.19.4" + "node": ">= 18", + "npm": ">= 9" }, "main": "./index", "bin": { From 7697dca768d18c3f43f28fda79ebf56c5f935bb2 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sun, 17 Sep 2023 22:09:17 -0500 Subject: [PATCH 3/9] Get tests working with node v18.17.1 --- package.json | 2 +- test/brain_test.js | 105 ++++++++++++++++++++++++++++------------- test/datastore_test.js | 15 +++--- test/robot_test.js | 1 + 4 files changed, 83 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 081498bfa..bc725b3bf 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "scripts": { "start": "bin/hubot", "pretest": "standard", - "test": "node --test --experimental-test-coverage", + "test": "node --test", "test:smoke": "node src/**/*.js", "test:e2e": "bin/e2e-test.sh" }, diff --git a/test/brain_test.js b/test/brain_test.js index 11d9c0b77..b057bec2b 100644 --- a/test/brain_test.js +++ b/test/brain_test.js @@ -8,6 +8,7 @@ const assert = require('assert/strict') // Hubot classes const User = require('../src/user.js') const Robot = require('../src/robot.js') +const Brain = require('../src/brain.js') const { hook, reset } = require('./fixtures/RequireMocker.js') const mockAdapter = require('./fixtures/mock-adapter.js') @@ -19,7 +20,7 @@ describe('Brain', () => { beforeEach(async () => { hook('hubot-mock-adapter', mockAdapter) mockRobot = new Robot('hubot-mock-adapter', false, 'TestHubot') - await mockRobot.loadAdapter() + await mockRobot.loadAdapter('hubot-mock-adapter') mockRobot.run() user1 = mockRobot.brain.userForId('1', { name: 'Guy One' }) user2 = mockRobot.brain.userForId('2', { name: 'Guy One Two' }) @@ -65,15 +66,17 @@ describe('Brain', () => { }) }) - describe('#save', () => it('emits a save event', (t, done) => { - const saveListener = (data) => { - assert.deepEqual(data, mockRobot.brain.data) - mockRobot.brain.off('save', saveListener) - done() - } - mockRobot.brain.on('save', saveListener) - mockRobot.brain.save() - })) + describe('#save', () => { + it('emits a save event', (t, done) => { + const saveListener = (data) => { + assert.deepEqual(data, mockRobot.brain.data) + mockRobot.brain.off('save', saveListener) + done() + } + mockRobot.brain.on('save', saveListener) + mockRobot.brain.save() + }) + }) describe('#resetSaveInterval', () => { it('updates the auto-save interval', async () => { @@ -167,21 +170,20 @@ describe('Brain', () => { describe('#get', () => { it('returns the saved value', () => { - mockRobot.brain.data._private['test-key'] = 'value' - assert.equal(mockRobot.brain.get('test-key'), 'value') + const brain = new Brain(mockRobot) + brain.set('test-key', 'value') + assert.equal(brain.get('test-key'), 'value') + brain.close() }) it('returns null if object is not found', () => { - assert.equal(mockRobot.brain.get('not a real key'), null) + const brain = new Brain(mockRobot) + assert.equal(brain.get('not a real key'), null) + brain.close() }) }) describe('#set', () => { - it('saves the value', () => { - mockRobot.brain.set('test-key', 'value') - assert.equal(mockRobot.brain.data._private['test-key'], 'value') - }) - it('sets multiple keys at once if an object is provided', () => { mockRobot.brain.data._private = { key1: 'val1', @@ -218,14 +220,17 @@ describe('Brain', () => { }) describe('#remove', () => it('removes the specified key', () => { - mockRobot.brain.data._private['test-key'] = 'value' + mockRobot.brain.set('test-key', 'value') mockRobot.brain.remove('test-key') assert.deepEqual(Object.keys(mockRobot.brain.data._private).includes('test-key'), false) })) describe('#userForId', () => { it('returns the user object', () => { - assert.deepEqual(mockRobot.brain.userForId(1), user1) + const brain = new Brain(mockRobot) + brain.userForId('1', user1) + assert.deepEqual(brain.userForId('1'), user1) + brain.close() }) it('does an exact match', () => { @@ -257,20 +262,30 @@ describe('Brain', () => { }) it('passes the provided options to the new User', () => { - const newUser = mockRobot.brain.userForId('all-new-user', { name: 'All New User', prop: 'mine' }) + const brain = new Brain(mockRobot) + const newUser = brain.userForId('all-new-user', { name: 'All New User', prop: 'mine' }) assert.equal(newUser.name, 'All New User') assert.equal(newUser.prop, 'mine') + brain.close() }) }) }) describe('#userForName', () => { it('returns the user with a matching name', () => { - assert.deepEqual(mockRobot.brain.userForName('Guy One'), user1) + const user = { id: 'user-for-name-guy-one', name: 'Guy One' } + const brain = new Brain(mockRobot) + const guy = brain.userForId('user-for-name-guy-one', user) + assert.deepEqual(brain.userForName('Guy One'), guy) + brain.close() }) it('does a case-insensitive match', () => { - assert.deepEqual(mockRobot.brain.userForName('guy one'), user1) + const user = { name: 'Guy One' } + const brain = new Brain(mockRobot) + const guy = brain.userForId('user-for-name-guy-one-case-insensitive', user) + assert.deepEqual(brain.userForName('guy one'), guy) + brain.close() }) it('returns null if no user matches', () => { @@ -280,18 +295,30 @@ describe('Brain', () => { describe('#usersForRawFuzzyName', () => { it('does a case-insensitive match', () => { - assert.ok(mockRobot.brain.usersForRawFuzzyName('guy').includes(user1) && mockRobot.brain.usersForRawFuzzyName('guy').includes(user2)) + const brain = new Brain(mockRobot) + const guy = brain.userForId('1', user1) + const guy2 = brain.userForId('2', user2) + assert.ok(brain.usersForRawFuzzyName('guy').includes(guy) && brain.usersForRawFuzzyName('guy').includes(guy2)) + brain.close() }) it('returns all matching users (prefix match) when there is not an exact match (case-insensitive)', () => { - assert.ok(mockRobot.brain.usersForRawFuzzyName('Guy').includes(user1) && mockRobot.brain.usersForRawFuzzyName('Guy').includes(user2)) + const brain = new Brain(mockRobot) + const guy = brain.userForId('1', user1) + const guy2 = brain.userForId('2', user2) + assert.ok(brain.usersForRawFuzzyName('Guy').includes(guy) && brain.usersForRawFuzzyName('Guy').includes(guy2)) + brain.close() }) it('returns all matching users (prefix match) when there is an exact match (case-insensitive)', () => { + const brain = new Brain(mockRobot) + const girl = brain.userForId('1', user1) + const girl2 = brain.userForId('2', user2) // Matched case - assert.deepEqual(mockRobot.brain.usersForRawFuzzyName('Guy One'), [user1, user2]) + assert.deepEqual(brain.usersForRawFuzzyName('Guy One'), [girl, girl2]) // Mismatched case - assert.deepEqual(mockRobot.brain.usersForRawFuzzyName('guy one'), [user1, user2]) + assert.deepEqual(brain.usersForRawFuzzyName('guy one'), [girl, girl2]) + brain.close() }) it('returns an empty array if no users match', () => { @@ -302,18 +329,30 @@ describe('Brain', () => { describe('#usersForFuzzyName', () => { it('does a case-insensitive match', () => { - assert.ok(mockRobot.brain.usersForFuzzyName('guy').includes(user1) && mockRobot.brain.usersForFuzzyName('guy').includes(user2)) + const brain = new Brain(mockRobot) + const girl = brain.userForId('1', user1) + const girl2 = brain.userForId('2', user2) + assert.ok(brain.usersForFuzzyName('guy').includes(girl) && brain.usersForFuzzyName('guy').includes(girl2)) + brain.close() }) it('returns all matching users (prefix match) when there is not an exact match', () => { - assert.ok(mockRobot.brain.usersForFuzzyName('Guy').includes(user1) && mockRobot.brain.usersForFuzzyName('Guy').includes(user2)) + const brain = new Brain(mockRobot) + const girl = brain.userForId('1', user1) + const girl2 = brain.userForId('2', user2) + assert.ok(brain.usersForFuzzyName('Guy').includes(girl) && brain.usersForFuzzyName('Guy').includes(girl2)) + brain.close() }) it('returns just the user when there is an exact match (case-insensitive)', () => { + const brain = new Brain(mockRobot) + const girl = brain.userForId('1', user1) + brain.userForId('2', user2) // Matched case - assert.deepEqual(mockRobot.brain.usersForFuzzyName('Guy One'), [user1]) + assert.deepEqual(brain.usersForFuzzyName('Guy One'), [girl]) // Mismatched case - assert.deepEqual(mockRobot.brain.usersForFuzzyName('guy one'), [user1]) + assert.deepEqual(brain.usersForFuzzyName('guy one'), [girl]) + brain.close() }) it('returns an empty array if no users match', () => { @@ -338,9 +377,9 @@ describe('Brain', () => { mockRobot.brain.setAutoSave(true) setTimeout(() => { mockRobot.brain.off('save', saveListener) - assert.ok(wasCalled) + assert.deepEqual(wasCalled, true) done() - }, 1000 * 5) + }, 1000 * 5.5) }) it('does not auto-save when turned off', (t, done) => { diff --git a/test/datastore_test.js b/test/datastore_test.js index a10edb710..497672c31 100644 --- a/test/datastore_test.js +++ b/test/datastore_test.js @@ -72,8 +72,9 @@ describe('Datastore', () => { }) it('creates an object from scratch when none exists', async () => { - await robot.datastore.setObject('object', 'key', 'value') - const value = await robot.datastore.get('object') + const datastore = new InMemoryDataStore(robot) + await datastore.setObject('object', 'key', 'value') + const value = await datastore.get('object') assert.deepEqual(value, { key: 'value' }) }) @@ -85,14 +86,16 @@ describe('Datastore', () => { }) it('creates an array from scratch when none exists', async () => { - await robot.datastore.setArray('array', 4) - const value = await robot.datastore.get('array') + const datastore = new InMemoryDataStore(robot) + await datastore.setArray('array', 4) + const value = await datastore.get('array') assert.deepEqual(value, [4]) }) it('creates an array with an array', async () => { const expected = [1, 2, 3] - await robot.datastore.setArray('array', [1, 2, 3]) - const actual = await robot.datastore.get('array') + const datastore = new InMemoryDataStore(robot) + await datastore.setArray('array', [1, 2, 3]) + const actual = await datastore.get('array') assert.deepEqual(actual, expected) }) }) diff --git a/test/robot_test.js b/test/robot_test.js index f2da62ede..92294beca 100644 --- a/test/robot_test.js +++ b/test/robot_test.js @@ -999,6 +999,7 @@ describe('Robot', () => { }) afterEach(() => { robot.shutdown() + process.removeAllListeners() }) it('should load the builtin shell adapter by default', async () => { assert.equal(robot.adapter.name, 'Shell') From 1eacd9908caa5e970757635f083983f4aef8b21a Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sun, 17 Sep 2023 22:22:55 -0500 Subject: [PATCH 4/9] It's fine if there's not history file for the shell adapter --- src/adapters/shell.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/shell.js b/src/adapters/shell.js index 21545fded..a8e892a76 100644 --- a/src/adapters/shell.js +++ b/src/adapters/shell.js @@ -41,7 +41,7 @@ class Shell extends Adapter { this.#rl = loadHistory((error, history) => { if (error) { - console.error(error) + console.log(error) } this.cli.history(history) this.cli.interact(`${this.robot.name ?? this.robot.alias}> `) From fb8507a46a836a69bf8787786040846a56acda37 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sun, 17 Sep 2023 22:32:29 -0500 Subject: [PATCH 5/9] fixe this[#rl].close is not a function in pipeline --- package.json | 2 +- src/adapters/shell.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bc725b3bf..1c54471f5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "scripts": { "start": "bin/hubot", "pretest": "standard", - "test": "node --test", + "test": "node --test test/*_test.js", "test:smoke": "node src/**/*.js", "test:e2e": "bin/e2e-test.sh" }, diff --git a/src/adapters/shell.js b/src/adapters/shell.js index a8e892a76..891168ae9 100644 --- a/src/adapters/shell.js +++ b/src/adapters/shell.js @@ -51,7 +51,10 @@ class Shell extends Adapter { close () { super.close() - this.#rl.close() + // Getting an error message on GitHubt Actions: error: 'this[#rl].close is not a function' + if (this.#rl.close) { + this.#rl.close() + } this.cli.removeAllListeners() this.cli.close() } From 79f047c978b0d209d7474b703e2700b6a0f8f9a2 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sun, 17 Sep 2023 22:40:58 -0500 Subject: [PATCH 6/9] Failing on linux node v18.17.1. Try using the loop back ip instead --- test/robot_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/robot_test.js b/test/robot_test.js index 92294beca..257d34abf 100644 --- a/test/robot_test.js +++ b/test/robot_test.js @@ -1012,7 +1012,7 @@ describe('Robot', () => { const robot = new Robot('hubot-mock-adapter', true, 'TestHubot') await robot.loadAdapter() robot.run() - const res = await fetch(`http://localhost:${process.env.PORT}/hubot/version`) + const res = await fetch(`http://127.0.0.1:${process.env.PORT}/hubot/version`) assert.equal(res.status, 404) assert.match(await res.text(), /Cannot GET \/hubot\/version/ig) robot.shutdown() From fc6cbba2f800d0141bb77b1b251f8a87f146ae29 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sun, 17 Sep 2023 22:54:06 -0500 Subject: [PATCH 7/9] remove unnecessary comments --- src/robot.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/robot.js b/src/robot.js index ce001a333..c015fb57e 100644 --- a/src/robot.js +++ b/src/robot.js @@ -224,9 +224,6 @@ class Robot { } this.listen(isCatchAllMessage, options, async msg => { - // TODO: Delete these commented out lines. - // console.log('catch all', msg.message) - // msg.message = msg.message.message await callback(msg.message) }) } From 808d708e05b7e380ad4c1f1b1411f4a75dbdcb81 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sat, 23 Sep 2023 16:16:10 -0500 Subject: [PATCH 8/9] feat: async/await robot.run BREAKING CHANGE: Making robot.run async/await is an API change. --- bin/hubot.js | 2 +- src/adapter.js | 4 +-- src/adapters/campfire.js | 2 +- src/adapters/shell.js | 73 +++++++++++++++++++--------------------- src/robot.js | 6 ++-- test/adapter_test.js | 4 +-- test/brain_test.js | 2 +- test/middleware_test.js | 2 +- test/robot_test.js | 12 +++---- test/shell_test.js | 49 +++++++++++++++------------ 10 files changed, 79 insertions(+), 77 deletions(-) diff --git a/bin/hubot.js b/bin/hubot.js index c3e4aa399..0a21299d7 100755 --- a/bin/hubot.js +++ b/bin/hubot.js @@ -153,5 +153,5 @@ function loadExternalScripts () { robot.adapter.once('connected', loadScripts) - robot.run() + await robot.run() })() diff --git a/src/adapter.js b/src/adapter.js index 4675db9f9..85746a257 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -57,8 +57,8 @@ class Adapter extends EventEmitter { // Public: Raw method for invoking the bot to run. Extend this. // - // Returns nothing. - run () {} + // Returns whatever the extended adapter returns. + async run () {} // Public: Raw method for shutting the bot down. Extend this. // diff --git a/src/adapters/campfire.js b/src/adapters/campfire.js index 6fd036156..be15effb6 100644 --- a/src/adapters/campfire.js +++ b/src/adapters/campfire.js @@ -82,7 +82,7 @@ class Campfire extends Adapter { }) } - run () { + async run () { const self = this const options = { diff --git a/src/adapters/shell.js b/src/adapters/shell.js index 891168ae9..160b7a04c 100644 --- a/src/adapters/shell.js +++ b/src/adapters/shell.js @@ -36,17 +36,17 @@ class Shell extends Adapter { this.send(envelope, ...strings) } - run () { + async run () { this.buildCli() - - this.#rl = loadHistory((error, history) => { - if (error) { - console.log(error) - } + try { + const { readlineInterface, history } = await this.#loadHistory() this.cli.history(history) this.cli.interact(`${this.robot.name ?? this.robot.alias}> `) - return this.emit('connected', this) - }) + this.#rl = readlineInterface + this.emit('connected', this) + } catch (error) { + console.log(error) + } } close () { @@ -62,7 +62,7 @@ class Shell extends Adapter { buildCli () { this.cli = cline() - this.cli.command('*', input => { + this.cli.command('*', async input => { let userId = process.env.HUBOT_SHELL_USER_ID || '1' if (userId.match(/A\d+z/)) { userId = parseInt(userId) @@ -70,7 +70,7 @@ class Shell extends Adapter { const userName = process.env.HUBOT_SHELL_USER_NAME || 'Shell' const user = this.robot.brain.userForId(userId, { name: userName, room: 'Shell' }) - this.receive(new TextMessage(user, input, 'messageId')) + await this.receive(new TextMessage(user, input, 'messageId')) }) this.cli.command('history', () => { @@ -110,37 +110,32 @@ class Shell extends Adapter { outstream.end() }) } + + async #loadHistory () { + if (!fs.existsSync(historyPath)) { + return new Error('No history available') + } + const instream = fs.createReadStream(historyPath) + const outstream = new Stream() + outstream.readable = true + outstream.writable = true + const history = [] + const readlineInterface = readline.createInterface({ input: instream, output: outstream, terminal: false }) + return new Promise((resolve, reject) => { + readlineInterface.on('line', line => { + line = line.trim() + if (line.length > 0) { + history.push(line) + } + }) + readlineInterface.on('close', () => { + resolve({ readlineInterface, history }) + }) + readlineInterface.on('error', reject) + }) + } } // Prevent output buffer "swallowing" every other character on OSX / Node version > 16.19.0. process.stdout._handle.setBlocking(false) exports.use = robot => new Shell(robot) - -// load history from .hubot_history. -// -// callback - A Function that is called with the loaded history items (or an empty array if there is no history) -function loadHistory (callback) { - if (!fs.existsSync(historyPath)) { - return callback(new Error('No history available')) - } - - const instream = fs.createReadStream(historyPath) - const outstream = new Stream() - outstream.readable = true - outstream.writable = true - - const items = [] - - const rl = readline.createInterface({ input: instream, output: outstream, terminal: false }) - .on('line', function (line) { - line = line.trim() - if (line.length > 0) { - items.push(line) - } - }) - .on('close', () => { - callback(null, items) - }) - .on('error', callback) - return rl -} diff --git a/src/robot.js b/src/robot.js index c015fb57e..f2babcb98 100644 --- a/src/robot.js +++ b/src/robot.js @@ -643,11 +643,11 @@ class Robot { // Public: Kick off the event loop for the adapter // - // Returns nothing. - run () { + // Returns whatever the adapter returns. + async run () { this.emit('running') - this.adapter.run() + return await this.adapter.run() } // Public: Gracefully shutdown the robot process diff --git a/test/adapter_test.js b/test/adapter_test.js index 6eb90b144..dd4f0eab1 100644 --- a/test/adapter_test.js +++ b/test/adapter_test.js @@ -78,8 +78,8 @@ describe('Adapter', () => { assert.ok(typeof adapter.run === 'function', 'The adapter should have a run method.') }) - it('does nothing', () => { - adapter.run() + it('does nothing', async () => { + await adapter.run() }) }) diff --git a/test/brain_test.js b/test/brain_test.js index b057bec2b..c2074223a 100644 --- a/test/brain_test.js +++ b/test/brain_test.js @@ -21,7 +21,7 @@ describe('Brain', () => { hook('hubot-mock-adapter', mockAdapter) mockRobot = new Robot('hubot-mock-adapter', false, 'TestHubot') await mockRobot.loadAdapter('hubot-mock-adapter') - mockRobot.run() + await mockRobot.run() user1 = mockRobot.brain.userForId('1', { name: 'Guy One' }) user2 = mockRobot.brain.userForId('2', { name: 'Guy One Two' }) user3 = mockRobot.brain.userForId('3', { name: 'Girl Three' }) diff --git a/test/middleware_test.js b/test/middleware_test.js index 9bcdc007a..79945031a 100644 --- a/test/middleware_test.js +++ b/test/middleware_test.js @@ -101,7 +101,7 @@ describe('Middleware', () => { hook('hubot-mock-adapter', require('./fixtures/mock-adapter.js')) robot = new Robot('hubot-mock-adapter', false, 'TestHubot') await robot.loadAdapter() - robot.run + await robot.run // Re-throw AssertionErrors for clearer test failures robot.on('error', function (err, response) { diff --git a/test/robot_test.js b/test/robot_test.js index 257d34abf..2f95e5ecd 100644 --- a/test/robot_test.js +++ b/test/robot_test.js @@ -337,7 +337,7 @@ describe('Robot', () => { hook('hubot-mock-adapter', mockAdapter) robot = new Robot('hubot-mock-adapter', false, 'TestHubot') await robot.loadAdapter() - robot.run() + await robot.run() }) afterEach(() => { robot.shutdown() @@ -846,7 +846,7 @@ describe('Robot', () => { user = new User('1', { name: 'node', room: '#test' }) robot.alias = 'Hubot' await robot.loadAdapter() - robot.run() + await robot.run() }) afterEach(() => { robot.shutdown() @@ -948,7 +948,7 @@ describe('Robot', () => { robot.alias = 'Hubot' await robot.loadAdapter('./test/fixtures/MockAdapter.mjs') await robot.loadFile(path.resolve('./test/fixtures/'), 'TestScript.js') - robot.run() + await robot.run() }) afterEach(() => { robot.shutdown() @@ -973,7 +973,7 @@ describe('Robot', () => { robot.alias = 'Hubot' await robot.loadAdapter('./test/fixtures/MockAdapter.coffee') await robot.loadFile(path.resolve('./test/fixtures/'), 'TestScript.coffee') - robot.run() + await robot.run() }) afterEach(() => { robot.shutdown() @@ -995,7 +995,7 @@ describe('Robot', () => { robot = new Robot(null, false, 'TestHubot') robot.alias = 'Hubot' await robot.loadAdapter() - robot.run() + await robot.run() }) afterEach(() => { robot.shutdown() @@ -1011,7 +1011,7 @@ describe('Robot', () => { hook('hubot-mock-adapter', mockAdapter) const robot = new Robot('hubot-mock-adapter', true, 'TestHubot') await robot.loadAdapter() - robot.run() + await robot.run() const res = await fetch(`http://127.0.0.1:${process.env.PORT}/hubot/version`) assert.equal(res.status, 404) assert.match(await res.text(), /Cannot GET \/hubot\/version/ig) diff --git a/test/shell_test.js b/test/shell_test.js index 510efb793..65fca53d8 100644 --- a/test/shell_test.js +++ b/test/shell_test.js @@ -1,6 +1,6 @@ 'use strict' -const { describe, it, beforeEach, afterEach } = require('node:test') +const { describe, it, beforeEach } = require('node:test') const assert = require('assert/strict') const Robot = require('../src/robot') @@ -12,11 +12,6 @@ describe('Shell Adapter', () => { beforeEach(async () => { robot = new Robot('shell', false, 'TestHubot') await robot.loadAdapter() - robot.run() - }) - - afterEach(() => { - robot.shutdown() }) describe('Public API', () => { @@ -29,52 +24,64 @@ describe('Shell Adapter', () => { assert.deepEqual(adapter.robot, robot, 'The adapter should have a reference to the robot.') }) - it('sends a message', (t, done) => { + it('sends a message', async () => { const old = console.log + let wasCalled = false console.log = (...args) => { console.log = old assert.deepEqual(args[0], '\x1b[1mhello\x1b[22m', 'Message should be outputed as bold to the console.') - done() + wasCalled = true } - adapter.send({ room: 'general' }, 'hello') + await adapter.send({ room: 'general' }, 'hello') + assert.deepEqual(wasCalled, true) }) - it('emotes a message', (t, done) => { + it('emotes a message', async () => { const old = console.log + let wasCalled = false console.log = (...args) => { console.log = old assert.deepEqual(args[0], '\x1b[1m* hello\x1b[22m', 'Message should be bold and have an * in front.') - done() + wasCalled = true } - adapter.emote({ room: 'general' }, 'hello') + await adapter.emote({ room: 'general' }, 'hello') + assert.deepEqual(wasCalled, true) }) - it('replies to a message', (t, done) => { + it('replies to a message', async () => { const old = console.log + let wasCalled = false console.log = (...args) => { console.log = old assert.deepEqual(args[0], '\x1b[1mnode: hello\x1b[22m', 'The strings should be passed through.') - done() + wasCalled = true } - adapter.reply({ room: 'general', user: { name: 'node' } }, 'hello') + await adapter.reply({ room: 'general', user: { name: 'node' } }, 'hello') + assert.deepEqual(wasCalled, true) }) - it('runs the adapter and emits connected', (t, done) => { + it('runs the adapter and emits connected', async () => { + let wasCalled = false const connected = () => { adapter.off('connected', connected) - done() + assert.ok(true, 'The connected event should be emitted.') + wasCalled = true } adapter.on('connected', connected) - adapter.run() + await adapter.run() + assert.deepEqual(wasCalled, true) + robot.shutdown() }) - it('dispatches received messages to the robot', (t, done) => { + it('dispatches received messages to the robot', async () => { const message = new TextMessage(new User('node'), 'hello', 1) + let wasCalled = false robot.receive = (msg) => { assert.deepEqual(msg, message, 'The message should be passed through.') - done() + wasCalled = true } - adapter.receive(message) + await adapter.receive(message) + assert.deepEqual(wasCalled, true) }) }) }) From d7b8974971056795106e3d3393e9601048aeb5d7 Mon Sep 17 00:00:00 2001 From: Joey Guerra Date: Sat, 23 Sep 2023 16:36:25 -0500 Subject: [PATCH 9/9] Possible race condition between loading the readline interface and closing happening close together --- src/adapters/shell.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/shell.js b/src/adapters/shell.js index 160b7a04c..866ed2727 100644 --- a/src/adapters/shell.js +++ b/src/adapters/shell.js @@ -52,7 +52,7 @@ class Shell extends Adapter { close () { super.close() // Getting an error message on GitHubt Actions: error: 'this[#rl].close is not a function' - if (this.#rl.close) { + if (this.#rl?.close) { this.#rl.close() } this.cli.removeAllListeners()