From f6acab0f5e46d0588f89439deacf0af05269bb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Wed, 27 Sep 2023 17:08:45 +0100 Subject: [PATCH 1/2] test(improve): improve artisan tests --- bin/artisan.ts | 43 --- bin/console.ts | 58 ---- bin/test.ts | 5 +- package-lock.json | 233 +++++++++++++- package.json | 30 +- src/annotations/Argument.ts | 6 +- src/annotations/Option.ts | 10 +- src/artisan/ArtisanImpl.ts | 79 +++-- src/artisan/BaseCommand.ts | 26 +- src/artisan/BaseConfigurer.ts | 4 +- src/commands/ConfigureCommand.ts | 6 +- src/commands/ListCommand.ts | 2 +- src/constants/MetadataKeys.ts | 11 + src/debug/index.ts | 12 + src/exceptions/RunningTaskException.ts | 2 +- src/handlers/CommanderHandler.ts | 96 ++++-- src/handlers/ConsoleExceptionHandler.ts | 6 +- src/helpers/Annotation.ts | 105 +++++++ src/helpers/Decorator.ts | 127 -------- src/helpers/command/Generator.ts | 2 +- src/index.ts | 2 +- src/kernels/ConsoleKernel.ts | 38 +-- src/testing/plugins/command/TestCommand.ts | 19 +- src/testing/plugins/command/TestOutput.ts | 16 +- src/types/CallInChildOptions.ts | 18 ++ tests/fixtures/.athennarc.json | 16 - tests/fixtures/AnnotatedCommand.ts | 60 ++++ .../fixtures/CustomConsoleExceptionHandler.ts | 3 + tests/fixtures/FixtureDatabase.ts | 15 + tests/fixtures/HelloCommand.ts | 26 ++ tests/fixtures/LICENSE.md | 1 - tests/fixtures/PluginCommand.ts | 28 ++ tests/fixtures/SimpleCommand.ts | 23 ++ tests/fixtures/Test.ts | 68 ----- tests/fixtures/commands/LoadAppCommand.ts | 22 -- tests/fixtures/commands/ThrowCommand.ts | 24 -- tests/fixtures/commands/UnknownArgsCommand.ts | 25 -- tests/fixtures/config/logging.ts | 27 -- tests/fixtures/config/view.ts | 121 -------- .../configurators/foo/configure/index.js | 17 ++ tests/fixtures/consoles/artisan.ts | 23 ++ tests/fixtures/consoles/base-command.ts | 31 ++ .../consoles/console-mock-dest-import.ts | 36 +++ .../fixtures/consoles/console-mock-library.ts | 44 +++ .../app.ts => consoles/console-mock-make.ts} | 5 +- .../fixtures/consoles/console-mock-prompt.ts | 15 + .../consoles/console-mock-test-template.ts | 26 ++ tests/fixtures/consoles/plugin-console.ts | 17 ++ tests/fixtures/exceptionHandler.ts | 33 -- tests/fixtures/handlers/Handler.ts | 7 - .../configurer/config/mongo/database.js | 49 --- .../configurer/config/mongo/database.ts | 49 --- .../configurer/config/mysql/database.js | 51 ---- .../configurer/config/mysql/database.ts | 51 ---- .../configurer/config/postgres/database.js | 51 ---- .../configurer/config/postgres/database.ts | 51 ---- .../library/configurer/docker-compose.yml | 3 - tests/fixtures/library/configurer/index.js | 160 ---------- tests/fixtures/routes/console.ts | 13 +- tests/helpers/BaseCommandTest.ts | 70 ----- tests/helpers/BaseTest.ts | 55 ++++ tests/unit/annotations/ArgumentTest.ts | 38 +++ tests/unit/annotations/OptionTest.ts | 42 +++ tests/unit/artisan/ArtisanTest.ts | 201 +++++++++--- tests/unit/commands/ConfigureCommandTest.ts | 54 ++-- tests/unit/commands/ListCommandTest.ts | 29 +- tests/unit/commands/MakeCommandTest.ts | 61 ++-- .../commands/TemplateCustomizeCommandTest.ts | 52 ++-- tests/unit/handlers/CommanderHandlerTest.ts | 217 ++++++++++--- .../handlers/ConsoleExceptionHandlerTest.ts | 120 ++++---- tests/unit/helpers/ActionTest.ts | 16 +- tests/unit/helpers/command/PromptTest.ts | 37 +-- tests/unit/kernels/ConsoleKernelTest.ts | 286 ++++++++++++++++-- .../unit/testing/plugins/CommandPluginTest.ts | 241 ++++++++++++++- tsconfig.json | 3 +- 75 files changed, 2086 insertions(+), 1583 deletions(-) delete mode 100644 bin/artisan.ts delete mode 100644 bin/console.ts create mode 100644 src/constants/MetadataKeys.ts create mode 100644 src/debug/index.ts create mode 100644 src/helpers/Annotation.ts delete mode 100644 src/helpers/Decorator.ts create mode 100644 src/types/CallInChildOptions.ts delete mode 100644 tests/fixtures/.athennarc.json create mode 100644 tests/fixtures/AnnotatedCommand.ts create mode 100644 tests/fixtures/CustomConsoleExceptionHandler.ts create mode 100644 tests/fixtures/FixtureDatabase.ts create mode 100644 tests/fixtures/HelloCommand.ts delete mode 100644 tests/fixtures/LICENSE.md create mode 100644 tests/fixtures/PluginCommand.ts create mode 100644 tests/fixtures/SimpleCommand.ts delete mode 100644 tests/fixtures/Test.ts delete mode 100644 tests/fixtures/commands/LoadAppCommand.ts delete mode 100644 tests/fixtures/commands/ThrowCommand.ts delete mode 100644 tests/fixtures/commands/UnknownArgsCommand.ts delete mode 100644 tests/fixtures/config/logging.ts delete mode 100644 tests/fixtures/config/view.ts create mode 100644 tests/fixtures/configurators/foo/configure/index.js create mode 100644 tests/fixtures/consoles/artisan.ts create mode 100644 tests/fixtures/consoles/base-command.ts create mode 100644 tests/fixtures/consoles/console-mock-dest-import.ts create mode 100644 tests/fixtures/consoles/console-mock-library.ts rename tests/fixtures/{config/app.ts => consoles/console-mock-make.ts} (78%) create mode 100644 tests/fixtures/consoles/console-mock-prompt.ts create mode 100644 tests/fixtures/consoles/console-mock-test-template.ts create mode 100644 tests/fixtures/consoles/plugin-console.ts delete mode 100644 tests/fixtures/exceptionHandler.ts delete mode 100644 tests/fixtures/handlers/Handler.ts delete mode 100644 tests/fixtures/library/configurer/config/mongo/database.js delete mode 100644 tests/fixtures/library/configurer/config/mongo/database.ts delete mode 100644 tests/fixtures/library/configurer/config/mysql/database.js delete mode 100644 tests/fixtures/library/configurer/config/mysql/database.ts delete mode 100644 tests/fixtures/library/configurer/config/postgres/database.js delete mode 100644 tests/fixtures/library/configurer/config/postgres/database.ts delete mode 100644 tests/fixtures/library/configurer/docker-compose.yml delete mode 100644 tests/fixtures/library/configurer/index.js delete mode 100644 tests/helpers/BaseCommandTest.ts create mode 100644 tests/helpers/BaseTest.ts create mode 100644 tests/unit/annotations/ArgumentTest.ts create mode 100644 tests/unit/annotations/OptionTest.ts diff --git a/bin/artisan.ts b/bin/artisan.ts deleted file mode 100644 index 415f180..0000000 --- a/bin/artisan.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ViewProvider } from '@athenna/view' -import { LoggerProvider } from '@athenna/logger' -import { Artisan, ConsoleKernel, ArtisanProvider } from '#src' - -/* -|-------------------------------------------------------------------------- -| Set IS_TS env. -|-------------------------------------------------------------------------- -| -| Set the IS_TS environment variable to true. Very useful when using the -| Path helper. -*/ - -process.env.IS_TS = 'true' - -await Config.loadAll(Path.fixtures('config')) - -Config.delete('app.version') -Config.set('rc.meta', import.meta.url) -Config.set('logging.channels.console.driver', 'console') -Config.set('logging.channels.exception.driver', 'console') - -new ViewProvider().register() -new LoggerProvider().register() -new ArtisanProvider().register() - -const kernel = new ConsoleKernel() - -await kernel.registerExceptionHandler() -await kernel.registerCommands(process.argv) -await kernel.registerRouteCommands(Path.pwd('not-found.ts')) -await kernel.registerRouteCommands(Path.pwd('bin/console.ts')) - -await Artisan.parse(process.argv, 'Artisan') diff --git a/bin/console.ts b/bin/console.ts deleted file mode 100644 index 1d117dc..0000000 --- a/bin/console.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Exec } from '@athenna/common' -import { Artisan } from '#src' - -Artisan.route('error', async function () { - throw new Error('error happened!') -}) - -Artisan.route('hello', async function (hello: string) { - console.log(hello) - console.log(Config.get('rc.commands.hello')) -}) - .argument('') - .settings({ loadApp: false, stayAlive: false, environments: ['hello'] }) - -Artisan.route('logger', async function () { - this.logger.simple('hello') - this.logger.simple('\n({bold,green} [ HELLO ])\n') - - this.logger.update('hello test') - this.logger.update('hello updated') - this.logger.rainbow('hello') - this.logger - .spinner() - .start('hello start spinner') - .succeed('hello end spinner') - await this.logger.promiseSpinner(() => Exec.sleep(10), { - successText: 'hello success', - failText: 'hello fail' - }) - await this.logger - .task() - .add('hello', async task => task.complete()) - .run() - this.logger.table().head('hello').render() - this.logger.sticker().add('').render() - this.logger.instruction().add('').render() - this.logger.column( - { - hello: 'world' - }, - { columns: ['KEY', 'VALUE'] } - ) - - const action = this.logger.action('create') - - action.succeeded('app/services/Service.ts') - action.skipped('app/services/Service.ts', 'File already exists') - action.failed('app/services/Service.ts', 'Something went wrong') -}) diff --git a/bin/test.ts b/bin/test.ts index c1b39b4..17e4a54 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,5 +1,5 @@ /** - * @athenna/common + * @athenna/artisan * * (c) João Lenon * @@ -7,12 +7,9 @@ * file that was distributed with this source code. */ -import { Config } from '@athenna/config' import { command } from '#src/testing/plugins/index' import { Runner, assert, specReporter } from '@athenna/test' -Config.set('meta', import.meta.url) - await Runner.setTsEnv() .addPlugin(assert()) .addPlugin(command()) diff --git a/package-lock.json b/package-lock.json index 56722ff..289f340 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/artisan", - "version": "4.11.0", + "version": "4.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@athenna/artisan", - "version": "4.11.0", + "version": "4.12.0", "license": "MIT", "dependencies": { "chalk-rainbow": "^1.0.0", @@ -21,7 +21,7 @@ "ora": "^6.3.1" }, "devDependencies": { - "@athenna/common": "^4.14.0", + "@athenna/common": "^4.15.4", "@athenna/config": "^4.4.0", "@athenna/ioc": "^4.4.2", "@athenna/logger": "^4.5.0", @@ -78,9 +78,9 @@ "dev": true }, "node_modules/@athenna/common": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.14.0.tgz", - "integrity": "sha512-OOe0X5rvaDyEUs5hSIs2Ggizs6koKNsmtQKN9y+ePNHITzXeFbJEAa+tGik8bgQwGFhEfQ3bpaCDEZFGFyeqCA==", + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.15.4.tgz", + "integrity": "sha512-Aj834H2KFQjZVhnGQWeR0jHOUbpCYcodyUTg19gQbmn3I2IJekhH0q3xQEKKvUGzlqIaLikKBe1DeLmsdKFVxw==", "dev": true, "dependencies": { "@fastify/formbody": "^7.4.0", @@ -89,6 +89,7 @@ "chalk": "^5.3.0", "change-case": "^4.1.2", "collect.js": "^4.36.1", + "execa": "^8.0.1", "fastify": "^4.23.2", "got": "^12.6.1", "http-status-codes": "^2.2.0", @@ -107,6 +108,140 @@ "youch-terminal": "^2.2.2" } }, + "node_modules/@athenna/common/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@athenna/common/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@athenna/common/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@athenna/common/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@athenna/config": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@athenna/config/-/config-4.4.0.tgz", @@ -10629,9 +10764,9 @@ "dev": true }, "@athenna/common": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.14.0.tgz", - "integrity": "sha512-OOe0X5rvaDyEUs5hSIs2Ggizs6koKNsmtQKN9y+ePNHITzXeFbJEAa+tGik8bgQwGFhEfQ3bpaCDEZFGFyeqCA==", + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.15.4.tgz", + "integrity": "sha512-Aj834H2KFQjZVhnGQWeR0jHOUbpCYcodyUTg19gQbmn3I2IJekhH0q3xQEKKvUGzlqIaLikKBe1DeLmsdKFVxw==", "dev": true, "requires": { "@fastify/formbody": "^7.4.0", @@ -10640,6 +10775,7 @@ "chalk": "^5.3.0", "change-case": "^4.1.2", "collect.js": "^4.36.1", + "execa": "^8.0.1", "fastify": "^4.23.2", "got": "^12.6.1", "http-status-codes": "^2.2.0", @@ -10656,6 +10792,85 @@ "validator-brazil": "^1.2.2", "youch": "^3.2.3", "youch-terminal": "^2.2.2" + }, + "dependencies": { + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } } }, "@athenna/config": { diff --git a/package.json b/package.json index 84fbee5..abb5e1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/artisan", - "version": "4.11.0", + "version": "4.12.0", "description": "The Athenna CLI application. Built on top of commander and inspired in @adonisjs/ace.", "license": "MIT", "author": "João Lenon ", @@ -51,6 +51,8 @@ "#bin": "./bin/index.js", "#src/*": "./src/*.js", "#src": "./src/index.js", + "#src/types": "./src/types/index.js", + "#src/debug": "./src/debug/index.js", "#tests/*": "./tests/*.js", "#tests": "./tests/index.js" }, @@ -67,7 +69,7 @@ "cross-env": "^7.0.3" }, "devDependencies": { - "@athenna/common": "^4.14.0", + "@athenna/common": "^4.15.4", "@athenna/config": "^4.4.0", "@athenna/ioc": "^4.4.2", "@athenna/logger": "^4.5.0", @@ -198,28 +200,6 @@ "athenna": { "templates": { "command": "./templates/command.edge" - }, - "commands": { - "test": { - "path": "./tests/fixtures/Test.js" - }, - "list": { - "path": "#src/commands/ListCommand", - "loadAllCommands": true - }, - "configure": "#src/commands/ConfigureCommand", - "loadapp": { - "path": "#tests/fixtures/commands/LoadAppCommand", - "loadApp": true, - "environments": [ - "worker", - "console" - ] - }, - "unknown": "#tests/fixtures/commands/UnknownArgsCommand", - "make:command": "#src/commands/MakeCommandCommand", - "template:customize": "#src/commands/TemplateCustomizeCommand" - }, - "providers": [] + } } } diff --git a/src/annotations/Argument.ts b/src/annotations/Argument.ts index 24c8ba5..d753565 100644 --- a/src/annotations/Argument.ts +++ b/src/annotations/Argument.ts @@ -10,7 +10,7 @@ import 'reflect-metadata' import { Options } from '@athenna/common' -import { Decorator } from '#src/helpers/Decorator' +import { Annotation } from '#src/helpers/Annotation' import type { ArgumentOptions } from '#src/types/ArgumentOptions' /** @@ -25,8 +25,10 @@ export function Argument(options?: ArgumentOptions): PropertyDecorator { if (!options.required) { options.signature = `[${options.signature}]` + } else { + options.signature = `<${options.signature}>` } - Decorator.setArgument(target, String(key), options) + Annotation.setArgument(target, String(key), options) } } diff --git a/src/annotations/Option.ts b/src/annotations/Option.ts index 0589459..21e8270 100644 --- a/src/annotations/Option.ts +++ b/src/annotations/Option.ts @@ -9,8 +9,8 @@ import 'reflect-metadata' -import { Options } from '@athenna/common' -import { Decorator } from '#src/helpers/Decorator' +import { Options, String } from '@athenna/common' +import { Annotation } from '#src/helpers/Annotation' import type { OptionOptions } from '#src/types/OptionOptions' /** @@ -18,10 +18,12 @@ import type { OptionOptions } from '#src/types/OptionOptions' */ export function Option(options?: OptionOptions): PropertyDecorator { return (target: any, key: string | symbol) => { + key = key.toString() + options = Options.create(options, { - signature: `--${String(key)}` + signature: `--${String.toDashCase(key.toString())}` }) - Decorator.setOption(target, String(key), options) + Annotation.setOption(target, key, options) } } diff --git a/src/artisan/ArtisanImpl.ts b/src/artisan/ArtisanImpl.ts index 9f9cfab..2a432df 100644 --- a/src/artisan/ArtisanImpl.ts +++ b/src/artisan/ArtisanImpl.ts @@ -10,26 +10,40 @@ import figlet from 'figlet' import chalkRainbow from 'chalk-rainbow' -import { platform } from 'node:os' import { Config } from '@athenna/config' -import { Decorator } from '#src/helpers/Decorator' import { Commander } from '#src/artisan/Commander' +import { Annotation } from '#src/helpers/Annotation' import { BaseCommand } from '#src/artisan/BaseCommand' import { CommanderHandler } from '#src/handlers/CommanderHandler' +import type { CallInChildOptions } from '#src/types/CallInChildOptions' import { Exec, Is, Options, Path, type CommandOutput } from '@athenna/common' export class ArtisanImpl { /** * Register the command if it is not registered yet. */ - public register(command: any): Commander { - const commander = Decorator.getCommander(command) - - /** - * We are not going to register the command in the Decorator - * helper because "Option" and "Argument" decorators will - * register the command without instantiating the class correctly. - */ + public register(Command: any): Commander { + const command = new Command() + + /** Set the command signature and description. */ + const commander = CommanderHandler.commander + .command(Command.signature()) + .description(Command.description()) + + /** Define custom commander options if exists */ + Command.commander(commander) + + /** Set arguments */ + Annotation.getArguments(command).forEach(arg => { + commander.argument(arg.signature, arg.description, arg.default) + }) + + /** Set options */ + Annotation.getOptions(command).forEach(option => { + commander.option(option.signature, option.description, option.default) + }) + + /** Set exception handler and handler */ return commander .action(CommanderHandler.bindHandler(command)) .showHelpAfterError() @@ -62,16 +76,13 @@ export class ArtisanImpl { protected __exec(...args: any[]) { const fn = handler.bind(this) - const commander: Commander = Reflect.getMetadata( - 'artisan:commander', - this - ) + const commander = CommanderHandler.getCommand(signature) return fn(...[...args, commander.opts()]) } } - const commander = this.register(new HandleCommand()) + const commander = this.register(HandleCommand) commander.constructor.prototype.settings = function (settings: any) { Config.set(`rc.commands.${this.name()}`, settings) @@ -89,13 +100,20 @@ export class ArtisanImpl { * Artisan.call('serve --watch') * * // Call command and handle settings. - * Artisan.call('serve --watch', false) + * Artisan.call('serve --watch', { withSettings: true }) * ``` */ - public async call(command: string, ignoreSettings = true): Promise { - const argv = ['./node', 'artisan', ...command.split(' ')] + public async call( + command: string, + options: { withSettings?: boolean } = {} + ): Promise { + options = Options.create(options, { + withSettings: false + }) + + const argv = ['node', 'artisan', ...command.split(' ')] - if (ignoreSettings) { + if (!options.withSettings) { await CommanderHandler.setVersion(Config.get('app.version')).parse(argv) return @@ -115,35 +133,30 @@ export class ArtisanImpl { * Artisan.callInChild('serve --watch') * // or * Artisan.callInChild('serve --watch', { - * executor: 'node --inspect', * path: Path.pwd('other-artisan.ts') * }) * ``` */ - public async callInChild( + public callInChild( command: string, - options?: { path?: string; executor?: string } + options?: CallInChildOptions ): Promise { options = Options.create(options, { - executor: Config.get('rc.artisan.child.executor', 'sh node'), path: Config.get( 'rc.artisan.child.path', - Path.bootstrap(`artisan.${Path.ext()}`) + Path.bootstrap(`console.${Path.ext()}`) ) }) options.path = Path.parseExt(options.path) - const separator = platform() === 'win32' ? '&' : '&&' - const executor = `cd ${Path.pwd()} ${separator} ${options.executor}` + const parsedCommand = command === '' ? [] : command.split(' ') - if (Env('NODE_ENV')) { - command = `cross-env NODE_ENV=${process.env.NODE_ENV} ${separator} ${executor} ${options.path} ${command}` - } else { - command = `${executor} ${options.path} ${command}` - } - - return Exec.command(command, { ignoreErrors: true }) + return Exec.node(options.path, parsedCommand, { + reject: false, + cwd: Path.pwd(), + localDir: Path.pwd() + }) } /** diff --git a/src/artisan/BaseCommand.ts b/src/artisan/BaseCommand.ts index 16e21e9..5f3a7a4 100644 --- a/src/artisan/BaseCommand.ts +++ b/src/artisan/BaseCommand.ts @@ -9,11 +9,12 @@ import { Rc } from '@athenna/config' import { Color } from '@athenna/common' -import { Decorator } from '#src/helpers/Decorator' import { Prompt } from '#src/helpers/command/Prompt' import { Logger } from '#src/helpers/command/Logger' +import { Annotation } from '#src/helpers/Annotation' import type { Commander } from '#src/artisan/Commander' import { Generator } from '#src/helpers/command/Generator' +import { CommanderHandler } from '#src/handlers/CommanderHandler' export abstract class BaseCommand { /** @@ -71,9 +72,9 @@ export abstract class BaseCommand { public prompt = new Prompt() /** - * The geneartor used to make files. The generator uses the + * The generator used to make files. The generator uses the * athenna/view package to generate files from templates. A - * briefly knowlodge about how to setup templates inside + * briefly knowledge about how to setup templates inside * athenna/view is very good to use this helper. */ public generator = new Generator() @@ -82,21 +83,24 @@ export abstract class BaseCommand { * Execute the command setting args and options in the class */ protected __exec(...args: any[]): Promise { - const commander = Decorator.getCommander(this) - const artisanOpts = Decorator.getOptions(this) - const artisanArgs = Decorator.getArguments(this) + const Command = this.constructor as typeof BaseCommand - const opts = commander.opts() + const optsMeta = Annotation.getOptions(this) + const argsMeta = Annotation.getArguments(this) + const optsValues = CommanderHandler.getCommandOptsValues( + Command.signature() + ) - artisanArgs.forEach(arg => (this[arg] = args.shift())) - Object.keys(opts).forEach(key => (this[artisanOpts[key]] = opts[key])) + argsMeta.forEach(arg => (this[arg.key] = args.shift())) + optsMeta.forEach(opt => (this[opt.key] = optsValues[opt.signatureName])) - return this.handle() + // TODO test if handle will receive the args properly + return this.handle(...args) } /** * The command handler. This method will be called * to execute your command. */ - public abstract handle(): Promise + public abstract handle(...args: any[]): Promise } diff --git a/src/artisan/BaseConfigurer.ts b/src/artisan/BaseConfigurer.ts index 530c7dc..9e239b0 100644 --- a/src/artisan/BaseConfigurer.ts +++ b/src/artisan/BaseConfigurer.ts @@ -59,9 +59,9 @@ export abstract class BaseConfigurer { public prompt = new Prompt() /** - * The geneartor used to make files. The generator uses the + * The generator used to make files. The generator uses the * athenna/view package to generate files from templates. A - * briefly knowlodge about how to setup templates inside + * briefly knowledge about how to setup templates inside * athenna/view is very good to use this helper. */ public generator = new Generator() diff --git a/src/commands/ConfigureCommand.ts b/src/commands/ConfigureCommand.ts index c56872f..ad944d9 100644 --- a/src/commands/ConfigureCommand.ts +++ b/src/commands/ConfigureCommand.ts @@ -14,7 +14,7 @@ import { NotFoundConfigurerException } from '#src/exceptions/NotFoundConfigurerE export class ConfigureCommand extends BaseCommand { @Argument({ - signature: '', + signature: 'libraries...', description: 'One or more libraries to be configured. (Example: artisan configure @athenna/mail @athenna/database)' }) @@ -44,13 +44,13 @@ export class ConfigureCommand extends BaseCommand { failText: `Failed to install ${Color.chalk.magenta(library)} library`, successText: `Library ${Color.chalk.magenta( library - )} succesfully installed` + )} successfully installed` }) } const path = isFile ? resolve(library) - : Path.nodeModules(`${library}/configurer/index.js`) + : Path.nodeModules(`${library}/configure/index.js`) if (!(await File.exists(path))) { throw new NotFoundConfigurerException(path, library) diff --git a/src/commands/ListCommand.ts b/src/commands/ListCommand.ts index 73cbadf..67022e9 100644 --- a/src/commands/ListCommand.ts +++ b/src/commands/ListCommand.ts @@ -26,7 +26,7 @@ export class ListCommand extends BaseCommand { `({bold,green} [ LISTING ${this.alias.toUpperCase()} ])\n` ) - const commands = CommanderHandler.getCommands(this.alias) + const commands = CommanderHandler.getCommandsInfo(this.alias) this.logger.column(commands, { columns: ['COMMAND', 'DESCRIPTION'] diff --git a/src/constants/MetadataKeys.ts b/src/constants/MetadataKeys.ts new file mode 100644 index 0000000..ffd18f8 --- /dev/null +++ b/src/constants/MetadataKeys.ts @@ -0,0 +1,11 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export const OPTIONS_KEY = 'artisan:options' +export const ARGUMENTS_KEY = 'artisan:arguments' diff --git a/src/debug/index.ts b/src/debug/index.ts new file mode 100644 index 0000000..e43c361 --- /dev/null +++ b/src/debug/index.ts @@ -0,0 +1,12 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { debuglog } from 'node:util' + +export const debug = debuglog('athenna:artisan') diff --git a/src/exceptions/RunningTaskException.ts b/src/exceptions/RunningTaskException.ts index 48754ab..36f2d21 100644 --- a/src/exceptions/RunningTaskException.ts +++ b/src/exceptions/RunningTaskException.ts @@ -15,7 +15,7 @@ export class RunningTaskException extends Exception { status: 500, code: 'E_RUNNING_TASK', message: `Task "${title}" with "running" status.`, - help: `The task "${title}" has runned and returned a "running" status, this means that "task.fail" or "task.complete" method havent been called inside the task callback. A task must end with one of this two status to be completed and interpreted by Artisan.` + help: `The task "${title}" has ran and returned a "running" status, this means that "task.fail" or "task.complete" method havent been called inside the task callback. A task must end with one of this two status to be completed and interpreted by Artisan.` }) } } diff --git a/src/handlers/CommanderHandler.ts b/src/handlers/CommanderHandler.ts index 48023fd..3fa8927 100644 --- a/src/handlers/CommanderHandler.ts +++ b/src/handlers/CommanderHandler.ts @@ -7,52 +7,46 @@ * file that was distributed with this source code. */ -import { Command as Commander } from 'commander' +import { Commander } from '#src/artisan/Commander' +import type { Argument, Option, OptionValues } from 'commander' export class CommanderHandler { /** * Holds the commander error handler. */ - private static exceptionHandler: any + public static exceptionHandler: any /** * The commander instance. */ - private static commander = new Commander() + public static commander = new Commander() /** - * Get the commander instance. + * Simple helper to reconstruct the commander instance. */ - public static getCommander(): T { - return this.commander as Commander as T + public static reconstruct(): void { + CommanderHandler.commander = new Commander() } /** * Parse the command called in the console and execute. */ public static async parse(argv: string[]): Promise { - return this.commander.parseAsync(argv) - } - - /** - * Set the exception handler for commander action method. - */ - public static setExceptionHandler(handler: any): void { - this.exceptionHandler = handler + return CommanderHandler.commander.parseAsync(argv) } /** * Bind the exception handler if exists inside the action. */ public static bindHandler(target: any): any { - if (!this.exceptionHandler) { - return target.__exec.bind(target) + if (!CommanderHandler.exceptionHandler) { + return (...args: any[]) => target.__exec.bind(target)(...args) } return (...args: any[]) => target.__exec .bind(target)(...args) - .catch(this.exceptionHandler) + .catch(CommanderHandler.exceptionHandler) } /** @@ -61,28 +55,84 @@ export class CommanderHandler { public static setVersion(version?: string): typeof CommanderHandler { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - if (this.commander._events['option:version']) { + if (CommanderHandler.commander._events['option:version']) { return this } if (version) { - this.commander.version(version, '-v, --version') + CommanderHandler.commander.version(version, '-v, --version') return this } - this.commander.version('Athenna Framework v3.0.0', '-v, --version') + CommanderHandler.commander.version( + 'Athenna Framework v3.0.0', + '-v, --version' + ) return this } /** - * Get all commands registered inside commander or by alias. + * Get the arguments of a command. + */ + public static getCommandArgs(name: string): Argument[] { + const commander: any = this.getCommand(name) + + return commander._args + } + + /** + * Get the options of a command. + */ + public static getCommandOpts(name: string): Option[] { + const commander: any = this.getCommand(name) + + return commander.options + } + + /** + * Get the option values of a command. + */ + public static getCommandOptsValues(name: string): OptionValues { + const commander: any = this.getCommand(name) + + return commander.opts() + } + + /** + * Get specific command my signature. + */ + public static getCommand(signature: string): Commander { + return CommanderHandler.getCommands().find( + (command: any) => command._name === signature + ) + } + + /** + * Get all commands registered inside commander. + */ + public static getCommands(): Commander[] { + return CommanderHandler.commander.commands + } + + /** + * Verify if the actual commander instance has a command + * with the given signature. + */ + public static hasCommand(signature: string): boolean { + return !!CommanderHandler.commander.commands.find( + (command: any) => command._name === signature + ) + } + + /** + * Get commands with arguments and descriptions registered inside commander. */ - public static getCommands(alias?: string): Record { + public static getCommandsInfo(alias?: string): Record { const commands = {} - this.commander.commands.forEach((command: any) => { + CommanderHandler.getCommands().forEach((command: any) => { if (alias && !command._name.startsWith(`${alias}:`)) { return } diff --git a/src/handlers/ConsoleExceptionHandler.ts b/src/handlers/ConsoleExceptionHandler.ts index 0576f72..a76d366 100644 --- a/src/handlers/ConsoleExceptionHandler.ts +++ b/src/handlers/ConsoleExceptionHandler.ts @@ -27,9 +27,9 @@ export class ConsoleExceptionHandler { } if (isInternalServerError && !isDebugMode) { - error.name = 'Internal server error' - error.code = 'E_INTERNAL_SERVER_ERROR' - error.message = 'An internal server exception has occurred.' + error.name = 'Internal error' + error.code = 'E_INTERNAL_ERROR' + error.message = 'An internal error has occurred.' delete error.stack } diff --git a/src/helpers/Annotation.ts b/src/helpers/Annotation.ts new file mode 100644 index 0000000..224bbdd --- /dev/null +++ b/src/helpers/Annotation.ts @@ -0,0 +1,105 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'reflect-metadata' + +import { debug } from '#src/debug' +import { String } from '@athenna/common' +import type { ArgumentOptions, OptionOptions } from '#src/types' +import { OPTIONS_KEY, ARGUMENTS_KEY } from '#src/constants/MetadataKeys' + +export type OptionsMeta = { key: string; signatureName: string } & OptionOptions +export type ArgumentMeta = { + key: string + signatureName: string +} & ArgumentOptions + +export class Annotation { + public static setOption( + target: any, + key: string, + options: OptionOptions + ): typeof Annotation { + const optionMeta = { + key, + signatureName: options.signature, + ...options + } + + if (optionMeta.signatureName.includes('--')) { + optionMeta.signatureName = optionMeta.signatureName.split('--')[1] + } + + if (optionMeta.signatureName.startsWith('no-')) { + optionMeta.signatureName = optionMeta.signatureName.split('no-')[1] + } + + optionMeta.signatureName = String.toCamelCase( + optionMeta.signatureName + .replace(/<([^)]+)>/g, '') + .replace(/\[([^)]+)]/g, '') + .replace(/ /g, '') + ) + + const opts = this.getOptions(target) + + debug('Registering option %o', { + target, + ...optionMeta + }) + + opts.push(optionMeta) + + Reflect.defineMetadata(OPTIONS_KEY, opts, target) + + return this + } + + public static setArgument( + target: any, + key: string, + argument: ArgumentOptions + ): typeof Annotation { + const args = this.getArguments(target) + const argumentMeta = { + key, + signatureName: argument.signature + .replace(/[[\]']+/g, '') + .replace(/[<>']+/g, ''), + ...argument + } + + debug('Registering argument %o', { + target, + ...argumentMeta + }) + + args.push(argumentMeta) + + Reflect.defineMetadata(ARGUMENTS_KEY, args, target) + + return this + } + + public static getOptions(target: any): OptionsMeta[] { + if (!Reflect.hasMetadata(OPTIONS_KEY, target)) { + Reflect.defineMetadata(OPTIONS_KEY, [], target) + } + + return Reflect.getMetadata(OPTIONS_KEY, target) + } + + public static getArguments(target: any): ArgumentMeta[] { + if (!Reflect.hasMetadata(ARGUMENTS_KEY, target)) { + Reflect.defineMetadata(ARGUMENTS_KEY, [], target) + } + + return Reflect.getMetadata(ARGUMENTS_KEY, target) + } +} diff --git a/src/helpers/Decorator.ts b/src/helpers/Decorator.ts deleted file mode 100644 index beca7cf..0000000 --- a/src/helpers/Decorator.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import 'reflect-metadata' - -import { String } from '@athenna/common' -import { Commander } from '#src/artisan/Commander' -import type { ArgumentOptions, OptionOptions } from '#src/types' -import { CommanderHandler } from '#src/handlers/CommanderHandler' - -export class Decorator { - public static readonly OPTIONS_KEY = 'artisan:options' - public static readonly ARGUMENTS_KEY = 'artisan:arguments' - public static readonly COMMANDER_KEY = 'artisan:commander' - - public static getCommander(target: any): Commander { - if (Reflect.hasMetadata(this.COMMANDER_KEY, target)) { - return Reflect.getMetadata(this.COMMANDER_KEY, target) - } - - const Target = target.constructor - const commander = CommanderHandler.getCommander() - - const command = commander - .command(Target.signature()) - .description(Target.description()) - - Reflect.defineMetadata( - this.COMMANDER_KEY, - Target.commander(command), - target - ) - - return Reflect.getMetadata(this.COMMANDER_KEY, target) - } - - public static setOption( - target: any, - key: string, - options: OptionOptions - ): typeof Decorator { - let signatureOption = '' - - if (options.signature.includes('--')) { - signatureOption = options.signature.split('--')[1] - } - - if (signatureOption.startsWith('no-')) { - signatureOption = signatureOption.split('no-')[1] - } - - signatureOption = signatureOption - .replace(/<([^)]+)>/g, '') - .replace(/\[([^)]+)]/g, '') - .replace(/ /g, '') - - signatureOption = String.toCamelCase(signatureOption) - - if (!Reflect.hasMetadata(this.OPTIONS_KEY, target)) { - Reflect.defineMetadata( - this.OPTIONS_KEY, - { [signatureOption]: key }, - target - ) - } - - const metaOptions = Reflect.getMetadata(this.OPTIONS_KEY, target) - - if (metaOptions.signature === signatureOption) { - return this - } - - metaOptions[signatureOption] = key - - Reflect.defineMetadata(key, metaOptions, target) - - this.getCommander(target).option( - options.signature, - options.description, - options.default - ) - - return this - } - - public static setArgument( - target: any, - argument: string, - options: ArgumentOptions - ): typeof Decorator { - if (!Reflect.hasMetadata(this.ARGUMENTS_KEY, target)) { - Reflect.defineMetadata(this.ARGUMENTS_KEY, [], target) - } - - const args = Reflect.getMetadata(this.ARGUMENTS_KEY, target) - - if (args.includes(argument)) { - return this - } - - args.push(argument) - - Reflect.defineMetadata(this.ARGUMENTS_KEY, args, target) - - this.getCommander(target).argument( - options.signature, - options.description, - options.default - ) - - return this - } - - public static getOptions(target: any): Record { - return Reflect.getMetadata(this.OPTIONS_KEY, target) || {} - } - - public static getArguments(target: any): string[] { - return Reflect.getMetadata(this.ARGUMENTS_KEY, target) || [] - } -} diff --git a/src/helpers/command/Generator.ts b/src/helpers/command/Generator.ts index 2b00292..c36f6dd 100644 --- a/src/helpers/command/Generator.ts +++ b/src/helpers/command/Generator.ts @@ -20,7 +20,7 @@ export class Generator { /** * Set the file path where the file will be generated. - * Rememeber that the file name in the path will be used + * Remember that the file name in the path will be used * to define the name properties. */ public path(path: string): Generator { diff --git a/src/index.ts b/src/index.ts index 72d6210..c3220a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ export * from '#src/helpers/Task' export * from '#src/helpers/Table' export * from '#src/helpers/Action' export * from '#src/helpers/Sticker' -export * from '#src/helpers/Decorator' +export * from '#src/helpers/Annotation' export * from '#src/helpers/Instruction' export * from '#src/helpers/command/Prompt' export * from '#src/helpers/command/Logger' diff --git a/src/kernels/ConsoleKernel.ts b/src/kernels/ConsoleKernel.ts index c38e47c..af55da4 100644 --- a/src/kernels/ConsoleKernel.ts +++ b/src/kernels/ConsoleKernel.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { isAbsolute, resolve } from 'node:path' +import { sep, isAbsolute, resolve } from 'node:path' import { Exec, File, Is, Module } from '@athenna/common' import { Artisan, CommanderHandler, ConsoleExceptionHandler } from '#src' @@ -56,7 +56,7 @@ export class ConsoleKernel { */ public async registerRouteCommands(path: string) { if (path.startsWith('#')) { - await this.resolvePath(path) + await Module.resolve(path, this.getMeta()) return } @@ -65,11 +65,14 @@ export class ConsoleKernel { path = resolve(path) } - if (!(await File.exists(path))) { + const [pathWithoutQueries, pathQueries] = path.split('?') + const parsedExtPath = Path.parseExt(pathWithoutQueries) + + if (!(await File.exists(parsedExtPath))) { return } - await this.resolvePath(path) + await Module.resolve(parsedExtPath + '?' + pathQueries, this.getMeta()) } /** @@ -79,15 +82,15 @@ export class ConsoleKernel { if (!path) { const handler = new ConsoleExceptionHandler() - CommanderHandler.setExceptionHandler(handler.handle.bind(handler)) + CommanderHandler.exceptionHandler = handler.handle.bind(handler) return } - const Handler = await this.resolvePath(path) + const Handler = await Module.resolve(path, this.getMeta()) const handler = new Handler() - CommanderHandler.setExceptionHandler(handler.handle.bind(handler)) + CommanderHandler.exceptionHandler = handler.handle.bind(handler) } /** @@ -95,19 +98,9 @@ export class ConsoleKernel { * command inside the service provider and also in Artisan. */ public async registerCommandByPath(path: string): Promise { - const Command = await this.resolvePath(path) - - Artisan.register(new Command()) - } + const Command = await Module.resolve(path, this.getMeta()) - /** - * Resolve the import path by meta URL and import it. - */ - private async resolvePath(path: string) { - return Module.resolve( - `${path}?version=${Math.random()}`, - Config.get('rc.meta') - ) + Artisan.register(Command) } /** @@ -126,4 +119,11 @@ export class ConsoleKernel { return this.registerCommandByPath(path.path) }) } + + /** + * Get the meta URL of the project. + */ + private getMeta() { + return Config.get('rc.meta', Path.toHref(Path.pwd() + sep)) + } } diff --git a/src/testing/plugins/command/TestCommand.ts b/src/testing/plugins/command/TestCommand.ts index 13a0fcc..ff976df 100644 --- a/src/testing/plugins/command/TestCommand.ts +++ b/src/testing/plugins/command/TestCommand.ts @@ -11,21 +11,15 @@ import { Artisan } from '#src' import { Assert } from '@japa/assert' import type { CommandOutput } from '@athenna/common' import { TestOutput } from '#src/testing/plugins/command/TestOutput' +import type { CallInChildOptions } from '#src/types/CallInChildOptions' export class TestCommand { /** * The Artisan file path that will be used to run commands. * - * @default 'Path.bootstrap(`artisan.${Path.ext()}`)' + * @default 'Path.bootstrap(`console.${Path.ext()}`)' */ - public static artisanPath = Path.bootstrap(`artisan.${Path.ext()}`) - - /** - * The Artisan executor that will be used to run commands. - * - * @default 'sh node' - */ - public static artisanExecutor = 'sh node' + public static artisanPath = Path.bootstrap(`console.${Path.ext()}`) /** * Set the artisan file path. @@ -52,10 +46,13 @@ export class TestCommand { * Run the command and return the TestOutput instance * to make assertions. */ - public async run(command: string): Promise { + public async run( + command: string, + options?: CallInChildOptions + ): Promise { return Artisan.callInChild(command, { path: TestCommand.artisanPath, - executor: TestCommand.artisanExecutor + ...options }).then(output => this.createOutput(output)) } } diff --git a/src/testing/plugins/command/TestOutput.ts b/src/testing/plugins/command/TestOutput.ts index 2e06855..239d563 100644 --- a/src/testing/plugins/command/TestOutput.ts +++ b/src/testing/plugins/command/TestOutput.ts @@ -70,9 +70,13 @@ export class TestOutput { */ public assertLogged(message: string, stream?: 'stdout' | 'stderr') { const existsInStdout = this.output.stdout.includes(message) - const existsInStderr = this.output.stdout.includes(message) + const existsInStderr = this.output.stderr.includes(message) - this.assert.isTrue(existsInStdout || existsInStderr) + if (!existsInStdout && !existsInStderr) { + return this.assert.fail( + `Expected message "${message}" to be logged in "stdout" or "stderr" but it was not logged.` + ) + } if (stream === 'stdout' && existsInStderr) { return this.assert.fail( @@ -92,7 +96,7 @@ export class TestOutput { */ public assertNotLogged(message: string, stream?: 'stdout' | 'stderr') { const existsInStdout = this.output.stdout.includes(message) - const existsInStderr = this.output.stdout.includes(message) + const existsInStderr = this.output.stderr.includes(message) switch (stream) { case 'stdout': @@ -116,7 +120,11 @@ export class TestOutput { const existsInStdout = regex.test(this.output.stdout) const existsInStderr = regex.test(this.output.stderr) - this.assert.isTrue(existsInStdout || existsInStderr) + if (!existsInStdout && !existsInStderr) { + return this.assert.fail( + `Expected regex to match some message in "stdout" or "stderr" but none matched.` + ) + } if (stream === 'stdout' && existsInStderr) { return this.assert.fail( diff --git a/src/types/CallInChildOptions.ts b/src/types/CallInChildOptions.ts new file mode 100644 index 0000000..08e3fcd --- /dev/null +++ b/src/types/CallInChildOptions.ts @@ -0,0 +1,18 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export type CallInChildOptions = { + /** + * The artisan file path that will be used to + * invoke the child process. + * + * @default Path.bootstrap(`console.${Path.ext()}`) + */ + path?: string +} diff --git a/tests/fixtures/.athennarc.json b/tests/fixtures/.athennarc.json deleted file mode 100644 index 1b94f5c..0000000 --- a/tests/fixtures/.athennarc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "view": { - "disks": {}, - "templates": { - "command": "./templates/command.edge" - } - }, - "commands": { - "test": "./tests/Stubs/Test.js", - "configure": "#src/Commands/ConfigureCommand", - "make:command": "#src/Commands/MakeCommandCommand", - "template:customize": "#src/Commands/TemplateCustomize" - }, - "providers": [], - "isAthennaRc": true -} diff --git a/tests/fixtures/AnnotatedCommand.ts b/tests/fixtures/AnnotatedCommand.ts new file mode 100644 index 0000000..7f7a886 --- /dev/null +++ b/tests/fixtures/AnnotatedCommand.ts @@ -0,0 +1,60 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Option, Argument, BaseCommand } from '#src' +import { FixtureDatabase } from './FixtureDatabase.js' + +export class AnnotatedCommand extends BaseCommand { + @Argument() + public name: string + + @Argument({ + required: true, + description: 'The age of the person' + }) + public age: string + + @Argument({ + required: false, + signature: 'your-email', + default: 'lenon@athenna.io', + description: 'The email of the person' + }) + public email: string + + @Option() + public withName: boolean + + @Option({ description: 'Add the age of the person' }) + public withAge: boolean + + @Option({ default: true, signature: '-am, --add-email', description: 'Add the email of the person' }) + public withEmail: boolean + + @Option({ default: false, signature: '--no-foo' }) + public withFoo: boolean + + public static signature() { + return 'annotated' + } + + public async handle() { + FixtureDatabase.set('annotated:command', { + name: this.name, + age: this.age, + email: this.email, + withName: this.withName, + withAge: this.withAge, + withEmail: this.withEmail, + withFoo: this.withFoo + }) + } +} + +export const annotatedCommand = new AnnotatedCommand() diff --git a/tests/fixtures/CustomConsoleExceptionHandler.ts b/tests/fixtures/CustomConsoleExceptionHandler.ts new file mode 100644 index 0000000..d48a809 --- /dev/null +++ b/tests/fixtures/CustomConsoleExceptionHandler.ts @@ -0,0 +1,3 @@ +import { ConsoleExceptionHandler } from '#src' + +export class CustomConsoleExceptionHandler extends ConsoleExceptionHandler {} diff --git a/tests/fixtures/FixtureDatabase.ts b/tests/fixtures/FixtureDatabase.ts new file mode 100644 index 0000000..1457c95 --- /dev/null +++ b/tests/fixtures/FixtureDatabase.ts @@ -0,0 +1,15 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * This is a fixture database that will be used to store + * all the data created by commands during the tests. This + * data will be used to assert the commands' behavior. + */ +export const FixtureDatabase = new Map() diff --git a/tests/fixtures/HelloCommand.ts b/tests/fixtures/HelloCommand.ts new file mode 100644 index 0000000..a03fe56 --- /dev/null +++ b/tests/fixtures/HelloCommand.ts @@ -0,0 +1,26 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Argument, BaseCommand } from '#src' +import { FixtureDatabase } from '#tests/fixtures/FixtureDatabase' + +export class HelloCommand extends BaseCommand { + @Argument({ signature: 'personName', description: 'The name of the person to greet' }) + public name: string + + public static signature() { + return 'hello' + } + + public async handle() { + FixtureDatabase.set('hello:command', this.name) + } +} + +export const helloCommand = new HelloCommand() diff --git a/tests/fixtures/LICENSE.md b/tests/fixtures/LICENSE.md deleted file mode 100644 index ce01362..0000000 --- a/tests/fixtures/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ -hello diff --git a/tests/fixtures/PluginCommand.ts b/tests/fixtures/PluginCommand.ts new file mode 100644 index 0000000..1645140 --- /dev/null +++ b/tests/fixtures/PluginCommand.ts @@ -0,0 +1,28 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand, Option } from '#src' + +export class PluginCommand extends BaseCommand { + public static signature() { + return 'plugin' + } + + @Option({ signature: '--throw' }) + public throw: boolean + + public async handle() { + if (this.throw) { + throw new Error('This is a test error') + } + + console.log('Hello stdout world!') + console.error('Hello stderr world!') + } +} diff --git a/tests/fixtures/SimpleCommand.ts b/tests/fixtures/SimpleCommand.ts new file mode 100644 index 0000000..5baaa7d --- /dev/null +++ b/tests/fixtures/SimpleCommand.ts @@ -0,0 +1,23 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand } from '#src' +import { FixtureDatabase } from './FixtureDatabase.js' + +export class SimpleCommand extends BaseCommand { + public static signature() { + return 'simple' + } + + public async handle() { + FixtureDatabase.set('simple:command', true) + } +} + +export const simpleCommand = new SimpleCommand() diff --git a/tests/fixtures/Test.ts b/tests/fixtures/Test.ts deleted file mode 100644 index 2be87c9..0000000 --- a/tests/fixtures/Test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Argument, Option } from '#src' -import { BaseCommand } from '#src/artisan/BaseCommand' - -export class Test extends BaseCommand { - @Argument() - public requiredArg: string - - @Option({ - signature: '-o, --other', - description: 'Option.' - }) - public option: string - - @Option({ - signature: '-oo, --otherOption', - default: 'notRequiredOption', - description: 'Other option.' - }) - public notRequiredOption: string - - @Argument({ - required: false, - default: 'notRequiredArg', - description: 'Not required arg.' - }) - public notRequiredArg: string - - @Option({ - signature: '--no-clean' - }) - public any___VALLLUE: boolean - - @Option({ - signature: '--ignore-on-clean [folders]', - description: 'Ignore the given folders when cleaning the application.', - default: 'tests|node_modules' - }) - public ignoreOnClean: string - - @Option({ - signature: '--ignore-on-clean-array [folders...]', - description: 'Ignore the given folders when cleaning the application.', - default: ['tests', 'node_modules'] - }) - public ignoreOnCleanArray: string[] - - public static signature(): string { - return 'test' - } - - public static description(): string { - return 'The description of test command.' - } - - public async handle(): Promise { - console.log(this.requiredArg, this.notRequiredArg, this.option, this.notRequiredOption) - console.log(this.any___VALLLUE, this.ignoreOnClean, this.ignoreOnCleanArray) - } -} diff --git a/tests/fixtures/commands/LoadAppCommand.ts b/tests/fixtures/commands/LoadAppCommand.ts deleted file mode 100644 index 2cad158..0000000 --- a/tests/fixtures/commands/LoadAppCommand.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand } from '#src' - -export class LoadAppCommand extends BaseCommand { - public static signature(): string { - return 'loadapp' - } - - public static description(): string { - return 'Command with load app setting as true.' - } - - public async handle(): Promise {} -} diff --git a/tests/fixtures/commands/ThrowCommand.ts b/tests/fixtures/commands/ThrowCommand.ts deleted file mode 100644 index 83333df..0000000 --- a/tests/fixtures/commands/ThrowCommand.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand } from '#src' - -export class ThrowCommand extends BaseCommand { - public static signature(): string { - return 'throw' - } - - public static description(): string { - return 'Throw some exception in handle.' - } - - public async handle(): Promise { - throw new Error('hey') - } -} diff --git a/tests/fixtures/commands/UnknownArgsCommand.ts b/tests/fixtures/commands/UnknownArgsCommand.ts deleted file mode 100644 index 0465f80..0000000 --- a/tests/fixtures/commands/UnknownArgsCommand.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { BaseCommand } from '#src' -import type { Commander } from '#src/artisan/Commander' - -export class UnknownArgsCommand extends BaseCommand { - public static signature(): string { - return 'unknown' - } - - public static commander(commander: Commander) { - return commander.allowUnknownOption() - } - - public async handle(): Promise { - console.log(process.argv) - } -} diff --git a/tests/fixtures/config/logging.ts b/tests/fixtures/config/logging.ts deleted file mode 100644 index 05d7af8..0000000 --- a/tests/fixtures/config/logging.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export default { - default: 'console', - - channels: { - console: { - driver: 'null', - level: 'trace', - formatter: 'cli' - }, - - exception: { - driver: 'null', - level: 'trace', - streamType: 'stderr', - formatter: 'none' - } - } -} diff --git a/tests/fixtures/config/view.ts b/tests/fixtures/config/view.ts deleted file mode 100644 index 9378ca1..0000000 --- a/tests/fixtures/config/view.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Path } from '@athenna/common' - -export default { - /* - |-------------------------------------------------------------------------- - | Disks - |-------------------------------------------------------------------------- - | - | Most templating systems load templates from disk. Here you may specify - | a key value map that will have the disk name pointing to the respective - | path that should be checked to load all the views inside. - | - */ - - disks: {}, - - /* - |-------------------------------------------------------------------------- - | Templates - |-------------------------------------------------------------------------- - | - | Templates are used by Artisan "make:..." commands to create your files. - | all the options defined here will be loaded by View Facade on bootstrap. - | - */ - - templates: { - /* - |-------------------------------------------------------------------------- - | Register templates - |-------------------------------------------------------------------------- - | - | Set if View Facade should register templates automatically. Setting this - | option to "false" will let the bootstrap of the application more performatic, - | but you are not going to be able to run Artisan "make:..." commands. - | - */ - - register: true, - - /* - |-------------------------------------------------------------------------- - | Paths - |-------------------------------------------------------------------------- - | - | Here you may specify a key value map that will have the template name - | pointing to the respective path that should be checked to load the view. - | - */ - - paths: { - command: Path.pwd('templates/command.edge') - }, - - /* - |-------------------------------------------------------------------------- - | Use custom templates - |-------------------------------------------------------------------------- - | - | Set if View Facade should load custom template files registered in the - | "view.templates.customTemplatesPath" options. Athenna default templates - | could be customized by running "template:customize" command. Setting this - | option as "false" will let the bootstrap of the application more performatic, - | but Athenna will not be able to auto register the custom templates. But you - | can manually register the templates in the "view.templates.paths" object above. - | - */ - - useCustom: true, - - /* - |-------------------------------------------------------------------------- - | Custom templates path - |-------------------------------------------------------------------------- - | - | Set the custom templates paths that "template:customize" command and View - | facade will use to store and load your custom templates. By default, the - | path is set as "resources/templates". All the ".edge" files found inside - | will be loaded by their name, example: 'artisan::command'. - | - */ - - customTemplatesPath: Path.resources('templates') - }, - - /* - |-------------------------------------------------------------------------- - | Edge options - |-------------------------------------------------------------------------- - | - | Athenna uses the Edge.js template engine to render templates. You can set - | all Edge supported options here. - | - */ - - edge: { - /* - |-------------------------------------------------------------------------- - | Cache - |-------------------------------------------------------------------------- - | - | Compiling a template to a JavaScript function is a time-consuming process, - | and hence it is recommended to cache the compiled templates in production. - | - | You can control the template caching using this options. Just make sure - | to set the value to true in the production environment. - | - */ - - cache: false - } -} diff --git a/tests/fixtures/configurators/foo/configure/index.js b/tests/fixtures/configurators/foo/configure/index.js new file mode 100644 index 0000000..86a11be --- /dev/null +++ b/tests/fixtures/configurators/foo/configure/index.js @@ -0,0 +1,17 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseConfigurer } from '../../../../../src/artisan/BaseConfigurer.js' + +export default class FooConfigurer extends BaseConfigurer { + async configure() { + const db = await this.prompt.list('Select your database', ['PostgreSQL', 'MySQL / MariaDB', 'MongoDB']) + this.logger.info(`You selected ${db}`) + } +} diff --git a/tests/fixtures/consoles/artisan.ts b/tests/fixtures/consoles/artisan.ts new file mode 100644 index 0000000..873e791 --- /dev/null +++ b/tests/fixtures/consoles/artisan.ts @@ -0,0 +1,23 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Artisan, ArtisanProvider } from '#src' +import { HelloCommand } from '#tests/fixtures/HelloCommand' +import { SimpleCommand } from '#tests/fixtures/SimpleCommand' +import { AnnotatedCommand } from '#tests/fixtures/AnnotatedCommand' + +process.env.IS_TS = 'true' + +new ArtisanProvider().register() + +Artisan.register(HelloCommand) +Artisan.register(SimpleCommand) +Artisan.register(AnnotatedCommand) + +await Artisan.parse(process.argv, 'Artisan') diff --git a/tests/fixtures/consoles/base-command.ts b/tests/fixtures/consoles/base-command.ts new file mode 100644 index 0000000..44da5ae --- /dev/null +++ b/tests/fixtures/consoles/base-command.ts @@ -0,0 +1,31 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Config, Rc } from '@athenna/config' +import { ViewProvider } from '@athenna/view' +import { Artisan, ArtisanProvider } from '#src' +import { ListCommand } from '#src/commands/ListCommand' +import { ConfigureCommand } from '#src/commands/ConfigureCommand' +import { MakeCommandCommand } from '#src/commands/MakeCommandCommand' +import { TemplateCustomizeCommand } from '#src/commands/TemplateCustomizeCommand' + +process.env.IS_TS = 'true' + +await Config.loadAll(Path.fixtures('config')) +await Rc.setFile(Path.pwd('package.json')) + +new ViewProvider().register() +new ArtisanProvider().register() + +Artisan.register(ListCommand) +Artisan.register(ConfigureCommand) +Artisan.register(MakeCommandCommand) +Artisan.register(TemplateCustomizeCommand) + +await Artisan.parse(process.argv, 'Artisan') diff --git a/tests/fixtures/consoles/console-mock-dest-import.ts b/tests/fixtures/consoles/console-mock-dest-import.ts new file mode 100644 index 0000000..e60a50c --- /dev/null +++ b/tests/fixtures/consoles/console-mock-dest-import.ts @@ -0,0 +1,36 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Artisan, ArtisanProvider } from '#src' + +import { Config, Rc } from '@athenna/config' +import { ViewProvider } from '@athenna/view' +import { ListCommand } from '#src/commands/ListCommand' +import { ConfigureCommand } from '#src/commands/ConfigureCommand' +import { MakeCommandCommand } from '#src/commands/MakeCommandCommand' +import { TemplateCustomizeCommand } from '#src/commands/TemplateCustomizeCommand' + +process.env.IS_TS = 'true' + +await Config.loadAll(Path.fixtures('config')) + +Config.set('rc.commands.make:command.path', '#src/commands/MakeCommandCommand') +Config.set('rc.commands.make:command.destination', './tests/fixtures/storage/commands') + +await Rc.setFile(Path.pwd('package.json')) + +new ViewProvider().register() +new ArtisanProvider().register() + +Artisan.register(ListCommand) +Artisan.register(ConfigureCommand) +Artisan.register(MakeCommandCommand) +Artisan.register(TemplateCustomizeCommand) + +await Artisan.parse(process.argv, 'Artisan') diff --git a/tests/fixtures/consoles/console-mock-library.ts b/tests/fixtures/consoles/console-mock-library.ts new file mode 100644 index 0000000..7337123 --- /dev/null +++ b/tests/fixtures/consoles/console-mock-library.ts @@ -0,0 +1,44 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Mock } from '@athenna/test' +import { BaseConfigurer, Artisan, ArtisanProvider } from '#src' +import { Exec, File, Module } from '@athenna/common' + +import { Config, Rc } from '@athenna/config' +import { ViewProvider } from '@athenna/view' +import { ListCommand } from '#src/commands/ListCommand' +import { ConfigureCommand } from '#src/commands/ConfigureCommand' +import { MakeCommandCommand } from '#src/commands/MakeCommandCommand' +import { TemplateCustomizeCommand } from '#src/commands/TemplateCustomizeCommand' + +class Configurer extends BaseConfigurer { + public async configure() { + this.logger.info('Configuring some-lib-name') + } +} + +process.env.IS_TS = 'true' + +await Config.loadAll(Path.fixtures('config')) +await Rc.setFile(Path.pwd('package.json')) + +new ViewProvider().register() +new ArtisanProvider().register() + +Artisan.register(ListCommand) +Artisan.register(ConfigureCommand) +Artisan.register(MakeCommandCommand) +Artisan.register(TemplateCustomizeCommand) + +Mock.when(Exec, 'command').resolve(undefined) +Mock.when(File, 'exists').resolve(true) +Mock.when(Module, 'getFrom').resolve(Configurer) + +await Artisan.parse(process.argv, 'Artisan') diff --git a/tests/fixtures/config/app.ts b/tests/fixtures/consoles/console-mock-make.ts similarity index 78% rename from tests/fixtures/config/app.ts rename to tests/fixtures/consoles/console-mock-make.ts index c392162..0b7fdb4 100644 --- a/tests/fixtures/config/app.ts +++ b/tests/fixtures/consoles/console-mock-make.ts @@ -7,7 +7,4 @@ * file that was distributed with this source code. */ -export default { - debug: true, - version: '1.0.0' -} +await import('#tests/fixtures/consoles/base-command') diff --git a/tests/fixtures/consoles/console-mock-prompt.ts b/tests/fixtures/consoles/console-mock-prompt.ts new file mode 100644 index 0000000..cab6cbd --- /dev/null +++ b/tests/fixtures/consoles/console-mock-prompt.ts @@ -0,0 +1,15 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Mock } from '@athenna/test' +import { Prompt } from '#src/helpers/command/Prompt' + +Mock.when(Prompt.prototype, 'list').return('something') + +await import('#tests/fixtures/consoles/base-command') diff --git a/tests/fixtures/consoles/console-mock-test-template.ts b/tests/fixtures/consoles/console-mock-test-template.ts new file mode 100644 index 0000000..8b10818 --- /dev/null +++ b/tests/fixtures/consoles/console-mock-test-template.ts @@ -0,0 +1,26 @@ +import { File, Path } from '@athenna/common' +import { Artisan, ArtisanProvider } from '#src' + +import { Config, Rc } from '@athenna/config' +import { ViewProvider } from '@athenna/view' +import { ListCommand } from '#src/commands/ListCommand' +import { ConfigureCommand } from '#src/commands/ConfigureCommand' +import { MakeCommandCommand } from '#src/commands/MakeCommandCommand' +import { TemplateCustomizeCommand } from '#src/commands/TemplateCustomizeCommand' + +await Config.loadAll(Path.fixtures('config')) + +await new File(Path.resources('templates/test.edge'), '').load() +Config.set('rc.templates.test', Path.resources('templates/test.edge')) + +await Rc.setFile(Path.pwd('package.json')) + +new ViewProvider().register() +new ArtisanProvider().register() + +Artisan.register(ListCommand) +Artisan.register(ConfigureCommand) +Artisan.register(MakeCommandCommand) +Artisan.register(TemplateCustomizeCommand) + +await Artisan.parse(process.argv, 'Artisan') diff --git a/tests/fixtures/consoles/plugin-console.ts b/tests/fixtures/consoles/plugin-console.ts new file mode 100644 index 0000000..2da6d93 --- /dev/null +++ b/tests/fixtures/consoles/plugin-console.ts @@ -0,0 +1,17 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Artisan, ArtisanProvider } from '#src' +import { PluginCommand } from '#tests/fixtures/PluginCommand' + +new ArtisanProvider().register() + +Artisan.register(PluginCommand) + +await Artisan.parse(process.argv, 'Artisan') diff --git a/tests/fixtures/exceptionHandler.ts b/tests/fixtures/exceptionHandler.ts deleted file mode 100644 index 56b29f7..0000000 --- a/tests/fixtures/exceptionHandler.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Exception } from '@athenna/common' -import { ConsoleExceptionHandler } from '#src' -import { LoggerProvider } from '@athenna/logger' - -new LoggerProvider().register() - -let error = null -const type = process.argv[2] -const noDebug = process.argv[3] - -if (noDebug === '--no-debug') { - Config.set('app.debug', false) -} - -switch (type) { - case 'error': - error = new Error('hello') - break - case 'exception': - error = new Exception({ message: 'hello', help: 'world' }) - break -} - -await new ConsoleExceptionHandler().handle(error) diff --git a/tests/fixtures/handlers/Handler.ts b/tests/fixtures/handlers/Handler.ts deleted file mode 100644 index bbbfd14..0000000 --- a/tests/fixtures/handlers/Handler.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ConsoleExceptionHandler } from '#src' - -export class Handler extends ConsoleExceptionHandler { - public async handle(error: any): Promise { - return super.handle(error) - } -} diff --git a/tests/fixtures/library/configurer/config/mongo/database.js b/tests/fixtures/library/configurer/config/mongo/database.js deleted file mode 100644 index 4bcf126..0000000 --- a/tests/fixtures/library/configurer/config/mongo/database.js +++ /dev/null @@ -1,49 +0,0 @@ -export default { - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - default: Env('DB_CONNECTION', 'mongo'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Athenna is shown below to make development simple. - | - | Supported: "mysql", "postgres" and "mongo". - | - */ - - connections: { - mongo: { - driver: 'mongo', - url: Env('DB_URL', 'mongodb://localhost:27017/database'), - retryWrites: true, - useNewUrlParser: true, - useUnifiedTopology: true, - }, - }, - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - migrations: Env('DB_MIGRATIONS', 'migrations'), -} diff --git a/tests/fixtures/library/configurer/config/mongo/database.ts b/tests/fixtures/library/configurer/config/mongo/database.ts deleted file mode 100644 index b02f7ab..0000000 --- a/tests/fixtures/library/configurer/config/mongo/database.ts +++ /dev/null @@ -1,49 +0,0 @@ -export default { - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - default: Env('DB_CONNECTION', 'mongo'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Athenna is shown below to make development simple. - | - | Supported: "mysql", "postgres" and "mongo". - | - */ - - connections: { - mongo: { - driver: 'mongo', - url: Env('DB_URL', 'mongodb://localhost:27017/database'), - retryWrites: true, - useNewUrlParser: true, - useUnifiedTopology: true - } - }, - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - migrations: Env('DB_MIGRATIONS', 'migrations') -} diff --git a/tests/fixtures/library/configurer/config/mysql/database.js b/tests/fixtures/library/configurer/config/mysql/database.js deleted file mode 100644 index 144e3d3..0000000 --- a/tests/fixtures/library/configurer/config/mysql/database.js +++ /dev/null @@ -1,51 +0,0 @@ -export default { - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - default: Env('DB_CONNECTION', 'mysql'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Athenna is shown below to make development simple. - | - | Supported: "mysql", "postgres" and "mongo". - | - */ - - connections: { - mysql: { - driver: 'mysql', - host: Env('DB_HOST', '127.0.0.1'), - port: Env('DB_PORT', 3306), - debug: Env('DB_DEBUG', false), - user: Env('DB_USERNAME', 'root'), - password: Env('DB_PASSWORD', 'root'), - database: Env('DB_DATABASE', 'database'), - }, - }, - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - migrations: Env('DB_MIGRATIONS', 'migrations'), -} diff --git a/tests/fixtures/library/configurer/config/mysql/database.ts b/tests/fixtures/library/configurer/config/mysql/database.ts deleted file mode 100644 index 501a6cc..0000000 --- a/tests/fixtures/library/configurer/config/mysql/database.ts +++ /dev/null @@ -1,51 +0,0 @@ -export default { - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - default: Env('DB_CONNECTION', 'mysql'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Athenna is shown below to make development simple. - | - | Supported: "mysql", "postgres" and "mongo". - | - */ - - connections: { - mysql: { - driver: 'mysql', - host: Env('DB_HOST', '127.0.0.1'), - port: Env('DB_PORT', 3306), - debug: Env('DB_DEBUG', false), - user: Env('DB_USERNAME', 'root'), - password: Env('DB_PASSWORD', 'root'), - database: Env('DB_DATABASE', 'database') - } - }, - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - migrations: Env('DB_MIGRATIONS', 'migrations') -} diff --git a/tests/fixtures/library/configurer/config/postgres/database.js b/tests/fixtures/library/configurer/config/postgres/database.js deleted file mode 100644 index ec24b65..0000000 --- a/tests/fixtures/library/configurer/config/postgres/database.js +++ /dev/null @@ -1,51 +0,0 @@ -export default { - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - default: Env('DB_CONNECTION', 'postgres'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Athenna is shown below to make development simple. - | - | Supported: "mysql", "postgres" and "mongo". - | - */ - - connections: { - postgres: { - driver: 'postgres', - host: Env('DB_HOST', '127.0.0.1'), - port: Env('DB_PORT', 5432), - debug: Env('DB_DEBUG', false), - user: Env('DB_USERNAME', 'root'), - password: Env('DB_PASSWORD', 'root'), - database: Env('DB_DATABASE', 'database'), - }, - }, - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - migrations: Env('DB_MIGRATIONS', 'migrations'), -} diff --git a/tests/fixtures/library/configurer/config/postgres/database.ts b/tests/fixtures/library/configurer/config/postgres/database.ts deleted file mode 100644 index 498f0d6..0000000 --- a/tests/fixtures/library/configurer/config/postgres/database.ts +++ /dev/null @@ -1,51 +0,0 @@ -export default { - /* - |-------------------------------------------------------------------------- - | Default Database Connection Name - |-------------------------------------------------------------------------- - | - | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once using the Database library. - | - */ - default: Env('DB_CONNECTION', 'postgres'), - - /* - |-------------------------------------------------------------------------- - | Database Connections - |-------------------------------------------------------------------------- - | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Athenna is shown below to make development simple. - | - | Supported: "mysql", "postgres" and "mongo". - | - */ - - connections: { - postgres: { - driver: 'postgres', - host: Env('DB_HOST', '127.0.0.1'), - port: Env('DB_PORT', 5432), - debug: Env('DB_DEBUG', false), - user: Env('DB_USERNAME', 'root'), - password: Env('DB_PASSWORD', 'root'), - database: Env('DB_DATABASE', 'database') - } - }, - - /* - |-------------------------------------------------------------------------- - | Migration Repository Table - |-------------------------------------------------------------------------- - | - | This table keeps track of all the migrations that have already run for - | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. - | - */ - - migrations: Env('DB_MIGRATIONS', 'migrations') -} diff --git a/tests/fixtures/library/configurer/docker-compose.yml b/tests/fixtures/library/configurer/docker-compose.yml deleted file mode 100644 index ef13c76..0000000 --- a/tests/fixtures/library/configurer/docker-compose.yml +++ /dev/null @@ -1,3 +0,0 @@ -version: "3.1" - -services: diff --git a/tests/fixtures/library/configurer/index.js b/tests/fixtures/library/configurer/index.js deleted file mode 100644 index f995a41..0000000 --- a/tests/fixtures/library/configurer/index.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import yaml from 'js-yaml' - -import { Env } from '@athenna/config' -import { File, Exec, Path } from '@athenna/common' -import { BaseConfigurer } from '../../../../src/artisan/BaseConfigurer.js' - -export default class LibraryConfigurer extends BaseConfigurer { - prompt = Env('ARTISAN_TESTING', false) - ? { - list: (_, __) => Promise.resolve('PostgreSQL') - } - : super.prompt - - async configure() { - const db = await this.prompt.list('Select the database driver you want to use', [ - 'PostgreSQL', - 'MySQL / MariaDB', - 'MongoDB' - ]) - - const libTitles = { - MongoDB: 'Install mongoose library', - PostgreSQL: 'Install knex and pg libraries', - 'MySQL / MariaDB': 'Install knex and mysql2 libraries' - } - - await this.logger - .task() - .add(libTitles[db], async t => this.setTask(t, () => this.taskOne(db))) - .add(`Create config/database.${Path.ext()} file`, async t => this.setTask(t, () => this.taskTwo(db))) - .add('Update commands and providers of .athennarc.json', async t => this.setTask(t, () => this.taskThree())) - .add('Update .env, .env.test and .env.example', async t => this.setTask(t, () => this.taskFour(db))) - .add('Update docker-compose.yml file', async t => this.setTask(t, () => this.taskFive(db))) - .run() - } - - async setTask(task, callback) { - try { - await callback() - await task.complete() - } catch (err) { - await task.fail(err) - } - } - - async taskOne() { - await Exec.sleep(100) - // const libraries = { - // MongoDB: 'mongoose', - // PostgreSQL: 'knex pg', - // 'MySQL / MariaDB': 'knex mysql2', - // } - // const npmInstallCommand = `cd ${Path.pwd()} && npm install ${libraries[db]} --production=false` - // return Exec.command(npmInstallCommand) - } - - async taskTwo(db) { - const paths = { - MongoDB: this.paths.lib.concat(`/configurer/config/mongo/database.${Path.ext()}`), - PostgreSQL: this.paths.lib.concat(`/configurer/config/postgres/database.${Path.ext()}`), - 'MySQL / MariaDB': this.paths.lib.concat(`/configurer/config/mysql/database.${Path.ext()}`) - } - - return new File(paths[db]).copy(Path.config(`database.${Path.ext()}`)) - } - - async taskThree() { - return this.rc - .pushTo('providers', './tests/fixtures/library/providers/DatabaseProvider.js') - .setTo('commands', 'make:model', './tests/fixtures/library/commands/MakeModelCommand.js') - .save() - } - - async taskFour(db) { - const envVars = { - MongoDB: '\nDB_CONNECTION=mongo\n' + 'DB_URL=mongodb://localhost:27017/database\n', - PostgreSQL: - '\nDB_CONNECTION=postgres\n' + - 'DB_HOST=127.0.0.1\n' + - 'DB_PORT=5432\n' + - 'DB_DATABASE=database\n' + - 'DB_DEBUG=false\n' + - 'DB_USERNAME=root\n' + - 'DB_PASSWORD=root\n', - 'MySQL / MariaDB': - '\nDB_CONNECTION=mysql\n' + - 'DB_HOST=127.0.0.1\n' + - 'DB_PORT=3306\n' + - 'DB_DATABASE=database\n' + - 'DB_DEBUG=false\n' + - 'DB_USERNAME=root\n' + - 'DB_PASSWORD=root\n' - } - - return new File(Path.pwd('.env'), Buffer.from('')) - .append(envVars[db]) - .then(() => new File(Path.pwd('.env.test'), Buffer.from('')).append(envVars[db])) - .then(() => new File(Path.pwd('.env.example'), Buffer.from('')).append(envVars[db])) - } - - async taskFive(db) { - const services = { - MongoDB: { - mongo: { - container_name: 'athenna_mongo', - image: 'mongo', - ports: ['27017:27017'] - } - }, - PostgreSQL: { - postgres: { - container_name: 'athenna_postgres', - image: 'postgres', - ports: ['5433:5432'], - environment: { - POSTGRES_DB: 'postgres', - POSTGRES_USER: 'postgres', - POSTGRES_PASSWORD: 12345, - POSTGRES_ROOT_PASSWORD: 12345 - } - } - }, - 'MySQL / MariaDB': { - mysql: { - container_name: 'athenna_mysql', - image: 'mysql', - ports: ['5433:5432'], - environment: { - MYSQL_DATABASE: 'athenna', - MYSQL_ROOT_PASSWORD: '12345', - MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' - } - } - } - } - - const baseDockerCompose = await new File(this.paths.lib.concat('/configurer/docker-compose.yml')).getContent() - const file = await new File(Path.pwd('docker-compose.yml'), baseDockerCompose).load() - const dockerCompose = yaml.load(file.content) - - const key = Object.keys(services[db])[0] - - if (!dockerCompose.services) { - dockerCompose.services = {} - } - - dockerCompose.services[key] = services[db][key] - - return file.setContent(yaml.dump(dockerCompose).concat('\n')) - } -} diff --git a/tests/fixtures/routes/console.ts b/tests/fixtures/routes/console.ts index 2a260d6..24f59da 100644 --- a/tests/fixtures/routes/console.ts +++ b/tests/fixtures/routes/console.ts @@ -1,12 +1,5 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - import { Artisan } from '#src' -Artisan.route('importalias', async function () {}) +Artisan.route('test:one', async function () {}) +Artisan.route('test:two', async function () {}) +Artisan.route('test:three', async function () {}) diff --git a/tests/helpers/BaseCommandTest.ts b/tests/helpers/BaseCommandTest.ts deleted file mode 100644 index 65afd7d..0000000 --- a/tests/helpers/BaseCommandTest.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @athenna/artisan - * - * (c) João Lenon - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Config, Rc } from '@athenna/config' -import { ViewProvider } from '@athenna/view' -import { File, Folder } from '@athenna/common' -import { LoggerProvider } from '@athenna/logger' -import { Mock, AfterEach, BeforeEach, type Stub } from '@athenna/test' -import { ConsoleKernel, ArtisanProvider, CommanderHandler } from '#src' -import { TestCommand } from '#src/testing/plugins/index' - -export class BaseCommandTest { - public artisan = Path.pwd('bin/artisan.ts') - public processExit: Stub - public originalPJson = new File(Path.pwd('package.json')).getContentAsStringSync() - - @BeforeEach() - public async beforeEach() { - this.processExit = Mock.when(process, 'exit').return(undefined) - - TestCommand.setArtisanPath(this.artisan) - - process.env.ARTISAN_TESTING = 'true' - - await Config.loadAll(Path.fixtures('config')) - - new ViewProvider().register() - new LoggerProvider().register() - new ArtisanProvider().register() - - const kernel = new ConsoleKernel() - - await Rc.setFile(Path.pwd('package.json')) - - await kernel.registerExceptionHandler() - await kernel.registerCommands() - } - - @AfterEach() - public async afterEach() { - Config.clear() - ioc.reconstruct() - Mock.restoreAll() - - delete process.env.ARTISAN_TESTING - - CommanderHandler.getCommander()._events = {} - CommanderHandler.getCommander().commands = [] - CommanderHandler.getCommander()._version = undefined - - await Folder.safeRemove(Path.app()) - await Folder.safeRemove(Path.config()) - await Folder.safeRemove(Path.resources()) - await Folder.safeRemove(Path.fixtures('storage')) - await Folder.safeRemove(Path.fixtures('build')) - - await File.safeRemove(Path.pwd('.env')) - await File.safeRemove(Path.pwd('.env.test')) - await File.safeRemove(Path.pwd('.env.example')) - await File.safeRemove(Path.pwd('docker-compose.yml')) - - await new File(Path.pwd('package.json')).setContent(this.originalPJson) - } -} diff --git a/tests/helpers/BaseTest.ts b/tests/helpers/BaseTest.ts new file mode 100644 index 0000000..c91424f --- /dev/null +++ b/tests/helpers/BaseTest.ts @@ -0,0 +1,55 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { File, Folder, Path } from '@athenna/common' +import { TestCommand } from '#src/testing/plugins/index' +import { ArtisanProvider, CommanderHandler } from '#src' +import { FixtureDatabase } from '#tests/fixtures/FixtureDatabase' +import { AfterEach, BeforeEach, Mock, type Stub } from '@athenna/test' + +export class BaseTest { + public processExitMock: Stub + public originalPJson = new File(Path.pwd('package.json')).getContentAsStringSync() + + @BeforeEach() + public async baseBeforeEach() { + new ArtisanProvider().register() + + TestCommand.setArtisanPath(Path.fixtures('consoles/base-command.ts')) + + this.processExitMock = Mock.when(process, 'exit').return(undefined) + } + + @AfterEach() + public async baseAfterEach() { + Mock.restoreAll() + ioc.reconstruct() + CommanderHandler.reconstruct() + FixtureDatabase.clear() + + await Folder.safeRemove(Path.app()) + await Folder.safeRemove(Path.resources()) + await Folder.safeRemove(Path.fixtures('storage')) + + await new File(Path.pwd('package.json')).setContent(this.originalPJson) + } + + /** + * Safe import a module, avoiding cache and if + * the module is not found, return null. + */ + public async import(path: string): Promise { + try { + return await import(`${path}.js?version=${Math.random()}`) + } catch (error) { + console.log(error) + return null + } + } +} diff --git a/tests/unit/annotations/ArgumentTest.ts b/tests/unit/annotations/ArgumentTest.ts new file mode 100644 index 0000000..4a300b4 --- /dev/null +++ b/tests/unit/annotations/ArgumentTest.ts @@ -0,0 +1,38 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Test, type Context } from '@athenna/test' +import { BaseTest } from '#tests/helpers/BaseTest' +import { ARGUMENTS_KEY } from '#src/constants/MetadataKeys' + +export default class ArgumentTest extends BaseTest { + @Test() + public async shouldBeAbleToPreregisterArgumentsInACommandUsingArgumentAnnotation({ assert }: Context) { + const { annotatedCommand } = await this.import('#tests/fixtures/AnnotatedCommand') + + const args = Reflect.getMetadata(ARGUMENTS_KEY, annotatedCommand) + + assert.deepEqual(args[0], { key: 'name', required: true, signature: '', signatureName: 'name' }) + assert.deepEqual(args[1], { + key: 'age', + required: true, + signature: '', + signatureName: 'age', + description: 'The age of the person' + }) + assert.deepEqual(args[2], { + key: 'email', + required: false, + signature: '[your-email]', + signatureName: 'your-email', + default: 'lenon@athenna.io', + description: 'The email of the person' + }) + } +} diff --git a/tests/unit/annotations/OptionTest.ts b/tests/unit/annotations/OptionTest.ts new file mode 100644 index 0000000..b4697dc --- /dev/null +++ b/tests/unit/annotations/OptionTest.ts @@ -0,0 +1,42 @@ +/** + * @athenna/artisan + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Test, type Context } from '@athenna/test' +import { BaseTest } from '#tests/helpers/BaseTest' +import { OPTIONS_KEY } from '#src/constants/MetadataKeys' + +export default class OptionTest extends BaseTest { + @Test() + public async shouldBeAbleToPreregisterOptionsInACommandUsingOptionAnnotation({ assert }: Context) { + const { annotatedCommand } = await this.import('#tests/fixtures/AnnotatedCommand') + + const opts = Reflect.getMetadata(OPTIONS_KEY, annotatedCommand) + + assert.deepEqual(opts[0], { key: 'withName', signature: '--with-name', signatureName: 'withName' }) + assert.deepEqual(opts[1], { + key: 'withAge', + signature: '--with-age', + signatureName: 'withAge', + description: 'Add the age of the person' + }) + assert.deepEqual(opts[2], { + default: true, + key: 'withEmail', + signatureName: 'addEmail', + signature: '-am, --add-email', + description: 'Add the email of the person' + }) + assert.deepEqual(opts[3], { + key: 'withFoo', + default: false, + signature: '--no-foo', + signatureName: 'foo' + }) + } +} diff --git a/tests/unit/artisan/ArtisanTest.ts b/tests/unit/artisan/ArtisanTest.ts index 0006e5e..897bbcc 100644 --- a/tests/unit/artisan/ArtisanTest.ts +++ b/tests/unit/artisan/ArtisanTest.ts @@ -8,76 +8,205 @@ */ import figlet from 'figlet' +import chalkRainbow from 'chalk-rainbow' -import { Artisan } from '#src' +import { BaseTest } from '#tests/helpers/BaseTest' import { Test, type Context, Mock } from '@athenna/test' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' +import { Annotation, Artisan, CommanderHandler } from '#src' +import { FixtureDatabase } from '#tests/fixtures/FixtureDatabase' -export default class ArtisanTest extends BaseCommandTest { +export default class ArtisanTest extends BaseTest { @Test() - public async shouldThrowAnErrorWhenTryingToExecuteACommandThatDoesNotExist({ assert }: Context) { - const { stderr } = await Artisan.callInChild('not-found', { - path: this.artisan + public async shouldBeAbleToRegisterAnArtisanCommand({ assert }: Context) { + const { SimpleCommand } = await this.import('#tests/fixtures/SimpleCommand') + + Artisan.register(SimpleCommand) + + assert.isTrue(CommanderHandler.hasCommand(SimpleCommand.signature())) + } + + @Test() + public async shouldBeAbleToRegisterAnArtisanCommandWithArguments({ assert }: Context) { + const { AnnotatedCommand, annotatedCommand } = await this.import('#tests/fixtures/AnnotatedCommand') + + Artisan.register(AnnotatedCommand) + + const args = CommanderHandler.getCommandArgs(AnnotatedCommand.signature()) + + assert.deepEqual( + args.map(arg => arg.name()), + Annotation.getArguments(annotatedCommand).map(arg => arg.signatureName) + ) + } + + @Test() + public async shouldBeAbleToRegisterAnArtisanCommandWithOptions({ assert }: Context) { + const { AnnotatedCommand, annotatedCommand } = await this.import('#tests/fixtures/AnnotatedCommand') + + Artisan.register(AnnotatedCommand) + + const opts = CommanderHandler.getCommandOpts(AnnotatedCommand.signature()) + + assert.deepEqual( + opts.map(opt => opt.flags), + Annotation.getOptions(annotatedCommand).map(arg => arg.signature) + ) + } + + @Test() + public async shouldBeAbleToRegisterAnArtisanCommandAsARoute({ assert }: Context) { + assert.plan(2) + + Artisan.route('hello', async function (hello: string, options: { hello: string }) { + assert.equal(hello, 'world') + assert.equal(options.hello, 'world') + }) + .argument('', 'Description for hello arg.') + .option('--hello [hello]', 'Description for hello option.') + + await CommanderHandler.parse(['node', 'artisan', 'hello', 'world', '--hello=world']) + } + + @Test() + public async shouldBeAbleToRegisterAnArtisanCommandAsARouteWithSettings({ assert }: Context) { + const fakeFire = Mock.fake() + ioc.singleton('Athenna/Core/Ignite', { fire: fakeFire }) + + assert.plan(4) + + Artisan.route('hello', async function (hello: string, options: { hello: string }) { + assert.equal(hello, 'world') + assert.equal(options.hello, 'world') }) + .argument('', 'Description for hello arg.') + .option('--hello [hello]', 'Description for hello option.') + .settings({ loadApp: true, stayAlive: false, environments: ['hello', 'world'] }) + + await Artisan.parse(['node', 'artisan', 'hello', 'world', '--hello=world']) + + assert.calledWith(fakeFire, ['hello', 'world']) + assert.calledWith(this.processExitMock, 0) + } + + @Test() + public async shouldBeAbleToCallArtisanCommandsInRuntime({ assert }: Context) { + const { SimpleCommand } = await this.import('#tests/fixtures/SimpleCommand') + + Artisan.register(SimpleCommand) - assert.equal(stderr, "error: unknown command 'not-found'\n") + await Artisan.call('simple') + + assert.isTrue(FixtureDatabase.has('simple:command')) } @Test() - public async shouldBeAbleToLogTheApplicationNameInEntrypointCommands({ assert }: Context) { - const { stdout } = await Artisan.callInChild('', { - path: this.artisan + public async shouldBeAbleToCallArtisanCommandsWithArgumentsInRuntime({ assert }: Context) { + const { HelloCommand } = await this.import('#tests/fixtures/HelloCommand') + + Artisan.register(HelloCommand) + + await Artisan.call('hello Lenon') + + assert.isTrue(FixtureDatabase.has('hello:command')) + assert.equal(FixtureDatabase.get('hello:command'), 'Lenon') + } + + @Test() + public async shouldBeAbleToCallArtisanCommandsWithOptionsInRuntime({ assert }: Context) { + const { AnnotatedCommand } = await this.import('#tests/fixtures/AnnotatedCommand') + + Artisan.register(AnnotatedCommand) + + await Artisan.call('annotated Lenon 22 --with-name --with-age --add-email --no-foo') + + assert.isTrue(FixtureDatabase.has('annotated:command')) + assert.deepEqual(FixtureDatabase.get('annotated:command'), { + age: '22', + email: 'lenon@athenna.io', + name: 'Lenon', + withName: true, + withAge: true, + withEmail: true, + withFoo: false }) + } + + @Test() + public async shouldBeAbleToCallArtisanCommandsInRuntimeWithSettings({ assert }: Context) { + const { SimpleCommand } = await this.import('#tests/fixtures/SimpleCommand') + + Artisan.register(SimpleCommand) - const appNameFiglet = figlet.textSync('Artisan') + await Artisan.call('simple', { withSettings: true }) - assert.equal(stdout, appNameFiglet.concat('\n')) + assert.isTrue(FixtureDatabase.has('simple:command')) + assert.calledWith(this.processExitMock, 0) } @Test() - public async shouldBeAbleToRegisterCommandsAsRoutes({ assert }: Context) { - process.env.NODE_ENV = 'test' + public async shouldBeAbleToCallArtisanCommandsInChildProcess({ assert }: Context) { + const { stdout, stderr, exitCode } = await Artisan.callInChild('simple', { + path: Path.fixtures('consoles/artisan.ts') + }) + + assert.equal(stdout, '') + assert.equal(stderr, '') + assert.equal(exitCode, 0) + } - const { stderr, stdout } = await Artisan.callInChild('hello world', { - path: this.artisan + @Test() + public async shouldBeAbleToCallArtisanCommandsWithArgumentsInChildProcess({ assert }: Context) { + const { stdout, stderr, exitCode } = await Artisan.callInChild('hello Lenon', { + path: Path.fixtures('consoles/artisan.ts') }) + assert.equal(stdout, '') assert.equal(stderr, '') - assert.equal(stdout, "world\n{ loadApp: false, stayAlive: false, environments: [ 'hello' ] }\n") + assert.equal(exitCode, 0) + } + + @Test() + public async shouldBeAbleToCallArtisanCommandsWithOptionsInChildProcess({ assert }: Context) { + const { stdout, stderr, exitCode } = await Artisan.callInChild( + 'annotated Lenon 22 --with-name --with-age --add-email --no-foo', + { + path: Path.fixtures('consoles/artisan.ts') + } + ) - process.env.NODE_ENV = undefined + assert.equal(stdout, '') + assert.equal(stderr, '') + assert.equal(exitCode, 0) } @Test() - public async shouldBeAbleToSetArgumentsAndOptionsUsingArgumentAndOptionDecorators({ assert }: Context) { - const { stderr, stdout } = await Artisan.callInChild('test test --other', { - path: this.artisan + public async shouldBeAbleToAutomaticallyParseTheArtisanExtPathInCallInChildMethod({ assert }: Context) { + const { stdout, stderr, exitCode } = await Artisan.callInChild('simple', { + path: Path.fixtures('consoles/artisan.js') }) + assert.equal(stdout, '') assert.equal(stderr, '') - assert.equal( - stdout, - "test notRequiredArg true notRequiredOption\ntrue tests|node_modules [ 'tests', 'node_modules' ]\n" - ) + assert.equal(exitCode, 0) } @Test() - public async shouldBeAbleToLoadTheApplicationIfTheLoadAppOptionsIsTrue({ assert }: Context) { - const fakeIgniteFire = Mock.sandbox.fake() - ioc.instance('Athenna/Core/Ignite', { fire: fakeIgniteFire }) + public async shouldBeAbleToParseTheArgvToPickTheRightCommandToExecute({ assert }: Context) { + const { SimpleCommand } = await this.import('#tests/fixtures/SimpleCommand') - await Artisan.call('loadapp', false) + Artisan.register(SimpleCommand) - assert.isTrue(fakeIgniteFire.calledWith(['worker', 'console'])) + await Artisan.parse(['node', 'artisan', 'simple']) + + assert.isTrue(FixtureDatabase.has('simple:command')) } @Test() - public async shouldBeAbleToSetCustomCommanderOptionsInCommands({ assert }: Context) { - const { stderr, stdout } = await Artisan.callInChild('unknown --unk', { - path: this.artisan - }) + public async shouldBeAbleToLogTheAppNameWithFigletAndChalkRainbowWhenParsingArgvInCliEntrypoint({ assert }: Context) { + const stdoutWriteMock = Mock.when(process.stdout, 'write').return(undefined) + + await Artisan.parse(['node', 'artisan'], 'Artisan') - assert.isDefined(stdout) - assert.isEmpty(stderr) + assert.calledWith(stdoutWriteMock, chalkRainbow(figlet.textSync('Artisan')) + '\n') } } diff --git a/tests/unit/commands/ConfigureCommandTest.ts b/tests/unit/commands/ConfigureCommandTest.ts index af80b22..605a15a 100644 --- a/tests/unit/commands/ConfigureCommandTest.ts +++ b/tests/unit/commands/ConfigureCommandTest.ts @@ -7,44 +7,38 @@ * file that was distributed with this source code. */ -import { Artisan } from '#src' -import { Config } from '@athenna/config' -import { Exec, File } from '@athenna/common' -import { Test, type Context, Mock } from '@athenna/test' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' +import { BaseTest } from '#tests/helpers/BaseTest' +import { Test, type Context } from '@athenna/test' -export default class ConfigureCommandTest extends BaseCommandTest { +export default class ConfigureCommandTest extends BaseTest { @Test() - public async shouldBeAbleToConfigurePathsInsideTheApplication({ assert }: Context) { - await Artisan.call('configure ./tests/fixtures/library/configurer/index.js') - - const { athenna } = await new File(Path.pwd('package.json')).getContentAsJson() - - assert.containsSubset(Config.get('rc.providers'), ['./tests/fixtures/library/providers/DatabaseProvider.js']) - assert.containsSubset(athenna.providers, ['./tests/fixtures/library/providers/DatabaseProvider.js']) - - assert.containsSubset(athenna.commands, { - 'make:model': './tests/fixtures/library/commands/MakeModelCommand.js' - }) - assert.containsSubset(Config.get('rc.commands'), { - 'make:model': './tests/fixtures/library/commands/MakeModelCommand.js' + public async shouldBeAbleToInstallLibrariesBeforeLookingUpTheConfigurer({ command }: Context) { + const output = await command.run('configure some-lib-name', { + path: Path.fixtures('consoles/console-mock-library.ts') }) + + output.assertSucceeded() + output.assertLogged('[ info ] Configuring some-lib-name') + output.assertLogged('✔ Library some-lib-name successfully installed') } @Test() - public async shouldBeAbleToConfigureLibrariesInsideTheApplicationAndThrowErrorWhenConfigureDoesNotExist({ - assert - }: Context) { - const originalCommand = Exec.command - const commandFake = Mock.sandbox.fake() - - Exec.command = (...args: any[]) => Promise.resolve(commandFake(...args)) + public async shouldBeAbleToRunAConfiguratorFilePathDirectlyInsteadOfInstallingLibraries({ command }: Context) { + const output = await command.run('configure ./tests/fixtures/configurators/foo/configure/index.js', { + path: Path.fixtures('consoles/console-mock-prompt.ts') + }) - await Artisan.call('configure some-lib-name', false) + output.assertSucceeded() + output.assertLogged('[ info ] You selected something') + } - Exec.command = originalCommand + @Test() + public async shouldThrowIfTheConfigurerFilePathCannotBeFound({ command }: Context) { + const output = await command.run('configure not-found.js', { + path: Path.fixtures('consoles/console-mock-prompt.ts') + }) - assert.isTrue(commandFake.calledOnceWith('npm install some-lib-name')) - assert.isTrue(this.processExit.calledWith(1)) // <- Means that some error happened + output.assertFailed() + output.assertLogged('NotFoundConfigurerException') } } diff --git a/tests/unit/commands/ListCommandTest.ts b/tests/unit/commands/ListCommandTest.ts index 28539f3..96d2652 100644 --- a/tests/unit/commands/ListCommandTest.ts +++ b/tests/unit/commands/ListCommandTest.ts @@ -7,19 +7,28 @@ * file that was distributed with this source code. */ -import { Artisan } from '#src' +import { BaseTest } from '#tests/helpers/BaseTest' import { Test, type Context } from '@athenna/test' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' -export default class ListCommandTest extends BaseCommandTest { +export default class ListCommandTest extends BaseTest { @Test() - public async shouldBeAbleToListOtherCommandsByAlias({ assert }: Context) { - const { stderr, stdout } = await Artisan.callInChild('list make', { - path: this.artisan - }) + public async shouldBeAbleToListArtisanCommandsByAlias({ command }: Context) { + const output = await command.run('list make') - assert.equal(stderr, '') - assert.isTrue(stdout.includes('[ LISTING MAKE ]')) - assert.isTrue(stdout.includes('make:command Make a new command file.')) + output.assertSucceeded() + output.assertLogged('[ LISTING MAKE ]') + output.assertLogged('COMMAND') + output.assertLogged('DESCRIPTION') + output.assertLogged('make:command Make a new command file.') + } + + @Test() + public async shouldLogEmptyWhenTheThereAreAnyCommandWithTheProvidedAlias({ command }: Context) { + const output = await command.run('list not-found') + + output.assertSucceeded() + output.assertLogged('[ LISTING NOT-FOUND ]') + output.assertLogged('COMMAND') + output.assertLogged('DESCRIPTION') } } diff --git a/tests/unit/commands/MakeCommandTest.ts b/tests/unit/commands/MakeCommandTest.ts index dcf3801..58c61a5 100644 --- a/tests/unit/commands/MakeCommandTest.ts +++ b/tests/unit/commands/MakeCommandTest.ts @@ -7,59 +7,60 @@ * file that was distributed with this source code. */ -import { Artisan } from '#src' -import { File } from '@athenna/common' -import { Config } from '@athenna/config' +import { File, Path } from '@athenna/common' +import { BaseTest } from '#tests/helpers/BaseTest' import { Test, type Context } from '@athenna/test' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' -export default class MakeCommandTest extends BaseCommandTest { +export default class MakeCommandTest extends BaseTest { @Test() - public async shouldBeAbleToCreateACommandFile({ assert }: Context) { - await Artisan.call('make:command TestCommand', false) + public async shouldBeAbleToCreateACommandFile({ assert, command }: Context) { + const output = await command.run('make:command TestCommand') - const path = Path.console('commands/TestCommand.ts') - - assert.isTrue(await File.exists(path)) - assert.isTrue(this.processExit.calledOnceWith(0)) + output.assertSucceeded() + output.assertLogged('[ MAKING COMMAND ]') + output.assertLogged('[ success ] Command "TestCommand" successfully created.') + output.assertLogged( + '[ success ] Athenna RC updated: { commands += "testCommand": "#app/console/commands/TestCommand" }' + ) const { athenna } = await new File(Path.pwd('package.json')).getContentAsJson() - assert.containsSubset(Config.get('rc.commands'), { - testCommand: '#app/console/commands/TestCommand' - }) + assert.isTrue(await File.exists(Path.commands('TestCommand.ts'))) assert.containsSubset(athenna.commands, { testCommand: '#app/console/commands/TestCommand' }) } @Test() - public async shouldBeAbleToCreateACommandFileWithADifferentDestPathAndImportPath({ assert }: Context) { - Config.set('rc.commands.make:command.path', Config.get('rc.commands.make:command')) - Config.set('rc.commands.make:command.destination', './tests/fixtures/storage/commands') - - await Artisan.call('make:command TestCommand', false) - - const path = Path.fixtures('storage/commands/TestCommand.ts') + public async shouldBeAbleToCreateACommandFileWithADifferentDestPathAndImportPath({ assert, command }: Context) { + const output = await command.run('make:command TestCommand', { + path: Path.fixtures('consoles/console-mock-dest-import.ts') + }) - assert.isTrue(await File.exists(path)) - assert.isTrue(this.processExit.calledOnceWith(0)) + output.assertSucceeded() + output.assertLogged('[ MAKING COMMAND ]') + output.assertLogged('[ success ] Command "TestCommand" successfully created.') + output.assertLogged( + '[ success ] Athenna RC updated: { commands += "testCommand": "#tests/fixtures/storage/commands/TestCommand" }' + ) const { athenna } = await new File(Path.pwd('package.json')).getContentAsJson() - assert.containsSubset(Config.get('rc.commands'), { - testCommand: '#tests/fixtures/storage/commands/TestCommand' - }) + assert.isTrue(await File.exists(Path.fixtures('storage/commands/TestCommand.ts'))) assert.containsSubset(athenna.commands, { testCommand: '#tests/fixtures/storage/commands/TestCommand' }) } @Test() - public async shouldThrowAnExceptionWhenTheFileAlreadyExists({ assert }: Context) { - await Artisan.call('make:command TestCommand') - await Artisan.call('make:command TestCommand') + public async shouldThrowAnExceptionWhenTheFileAlreadyExists({ command }: Context) { + await command.run('make:command TestCommand') + const output = await command.run('make:command TestCommand') - assert.isTrue(this.processExit.calledWith(1)) + output.assertFailed() + output.assertLogged('[ MAKING COMMAND ]') + output.assertLogged( + 'The file ({yellow} "/Users/jlenon7/Development/Athenna/Artisan/app/console/commands/TestCommand.ts") already exists' + ) } } diff --git a/tests/unit/commands/TemplateCustomizeCommandTest.ts b/tests/unit/commands/TemplateCustomizeCommandTest.ts index e41bb8d..73f46cc 100644 --- a/tests/unit/commands/TemplateCustomizeCommandTest.ts +++ b/tests/unit/commands/TemplateCustomizeCommandTest.ts @@ -7,41 +7,47 @@ * file that was distributed with this source code. */ -import { Artisan } from '#src' -import { Config } from '@athenna/config' import { File, Folder } from '@athenna/common' +import { BaseTest } from '#tests/helpers/BaseTest' import { Test, type Context } from '@athenna/test' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' -export default class TemplateCustomizeCommandTest extends BaseCommandTest { +export default class TemplateCustomizeCommandTest extends BaseTest { @Test() - public async shouldBeAbleToPublishTheAthennaTemplatesToDoCustomCustomizations({ assert }: Context) { - await Artisan.call('template:customize', false) - - const path = Path.resources() + public async shouldBeAbleToPublishTheAthennaTemplatesToMakeCustomCustomizations({ assert, command }: Context) { + const output = await command.run('template:customize') const { athenna } = await new File(Path.pwd('package.json')).getContentAsJson() - assert.isTrue(await Folder.exists(path)) - assert.isTrue(this.processExit.calledOnceWith(0)) - assert.isTrue(await File.exists(path.concat('/templates/command.edge'))) + output.assertSucceeded() + output.assertLogged('[ MOVING TEMPLATES ]') + output.assertLogged('Athenna RC updated:') + output.assertLogged('"command": "./resources/templates/command.edge"') + output.assertLogged('[ success ] Template files successfully moved to resources/templates folder.') + assert.isTrue(await Folder.exists(Path.resources())) + assert.isTrue(await File.exists(Path.resources('templates/command.edge'))) assert.equal(athenna.view.templates.command, './resources/templates/command.edge') } @Test() - public async shouldNotThrowErrorsIfTheTemplatePathAlreadyExistsInsideResourcesTemplates({ assert }: Context) { - const templatePath = Path.resources('templates/test.edge') - await new File(templatePath, '').load() - - Config.set('rc.templates.test', templatePath) - - await Artisan.call('template:customize', false) + public async shouldNotThrowErrorsIfTheTemplatePathAlreadyExistsInsideResourcesTemplates({ + assert, + command + }: Context) { + const output = await command.run('template:customize', { + path: Path.fixtures('consoles/console-mock-test-template.ts') + }) - const path = Path.resources() + const { athenna } = await new File(Path.pwd('package.json')).getContentAsJson() - assert.isTrue(await Folder.exists(path)) - assert.isTrue(await File.exists(templatePath)) - assert.isTrue(this.processExit.calledOnceWith(0)) - assert.isTrue(await File.exists(path.concat('/templates/command.edge'))) + output.assertSucceeded() + output.assertLogged('[ MOVING TEMPLATES ]') + output.assertLogged('Athenna RC updated:') + output.assertLogged('"command": "./resources/templates/command.edge"') + output.assertLogged('"test": "./resources/templates/test.edge"') + output.assertLogged('[ success ] Template files successfully moved to resources/templates folder.') + assert.isTrue(await Folder.exists(Path.resources())) + assert.isTrue(await File.exists(Path.resources('templates/test.edge'))) + assert.isTrue(await File.exists(Path.resources('templates/command.edge'))) + assert.equal(athenna.view.templates.command, './resources/templates/command.edge') } } diff --git a/tests/unit/handlers/CommanderHandlerTest.ts b/tests/unit/handlers/CommanderHandlerTest.ts index 6979402..2233f61 100644 --- a/tests/unit/handlers/CommanderHandlerTest.ts +++ b/tests/unit/handlers/CommanderHandlerTest.ts @@ -8,65 +8,212 @@ */ import { CommanderHandler } from '#src' -import { Test, type Context } from '@athenna/test' -import { ListCommand } from '#src/commands/ListCommand' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' +import { Test, type Context, BeforeEach } from '@athenna/test' + +export default class CommanderHandlerTest { + @BeforeEach() + public beforeEach() { + CommanderHandler.reconstruct() + } -export default class CommanderHandlerTest extends BaseCommandTest { @Test() - public async shouldBeAbleToGetAllTheCommandsRegisteredInsideCommander({ assert }: Context) { - CommanderHandler.getCommander().commands = [] + public async shouldBeAbleToParseArgumentsToExecuteACommand({ assert }: Context) { + let NAME = null - CommanderHandler.getCommander() - .command('configure ') - .description('Configure one or more libraries inside your application.') + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .action((name: string) => { + NAME = name + }) - const commands = CommanderHandler.getCommands() + await CommanderHandler.parse(['node', 'artisan', 'test', 'João Lenon']) - assert.deepEqual(commands, { - 'configure ': 'Configure one or more libraries inside your application.' - }) + assert.equal(NAME, 'João Lenon') } @Test() - public async shouldBeAbleToGetAllTheCommandsRegisteredInsideCommanderBySomeAlias({ assert }: Context) { - // CommanderHandler.getCommander().commands = [] + public async shouldBeAbleToVerifyThatACommandIsRegisteredAndExists({ assert }: Context) { + assert.isFalse(CommanderHandler.hasCommand('test')) - CommanderHandler.getCommander() - .command('configure ') - .description('Configure one or more libraries inside your application.') + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .action((name: string) => { + console.log(name) + }) + + assert.isTrue(CommanderHandler.hasCommand('test')) + } + + @Test() + public async shouldBeAbleToGetAllCommanderInstancesOfTheRegisteredCommands({ assert }: Context) { + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .action((name: string) => { + console.log(name) + }) + + const commanders = CommanderHandler.getCommands() + + assert.lengthOf(commanders, 1) + assert.equal(commanders[0].name(), 'test') + } - CommanderHandler.getCommander().command('make:command ').description('Make a new command file.') - CommanderHandler.getCommander().command('make:controller ').description('Make a new controller file.') + @Test() + public async shouldBeAbleToGetACommanderInstanceOfTheCommandByItName({ assert }: Context) { + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .action((name: string) => { + console.log(name) + }) - const commands = CommanderHandler.getCommands('make') + const commander = CommanderHandler.getCommand('test') - assert.deepEqual(commands, { - 'make:command ': 'Make a new command file.', - 'make:controller ': 'Make a new controller file.' - }) + assert.equal(commander.name(), 'test') } @Test() - public async shouldBeAbleToSetTheCliVersion({ assert }: Context) { - CommanderHandler.setVersion('v3.0.0') + public async shouldBeAbleToReconstructTheCommanderInstance({ assert }: Context) { + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .action((name: string) => { + console.log(name) + }) + + assert.isTrue(CommanderHandler.hasCommand('test')) - assert.equal(CommanderHandler.getCommander()._version, 'v3.0.0') + CommanderHandler.reconstruct() + + assert.isFalse(CommanderHandler.hasCommand('test')) } @Test() - public async shouldBeAbleToSetTheOficialAthennaVersionIfNoneIsVersionIsSed({ assert }: Context) { - CommanderHandler.setVersion() + public async shouldBeAbleToGetTheArgumentsOfACommandByItName({ assert }: Context) { + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .action((name: string) => { + console.log(name) + }) + + const args = CommanderHandler.getCommandArgs('test') + + assert.lengthOf(args, 1) + assert.equal(args[0].name(), 'name') + } - assert.equal(CommanderHandler.getCommander()._version, 'Athenna Framework v3.0.0') + @Test() + public async shouldBeAbleToGetTheOptionsOfACommandByItName({ assert }: Context) { + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .option('-ag, --age', 'The person age') + .action((name: string) => { + console.log(name) + }) + + const opts = CommanderHandler.getCommandOpts('test') + + assert.lengthOf(opts, 1) + assert.equal(opts[0].flags, '-ag, --age') } @Test() - public async shouldBeAbleToGetTheExecMethodBindedWithoutExceptionHandler({ assert }: Context) { - CommanderHandler.setExceptionHandler(undefined) + public async shouldBeAbleToGetTheOptionsValuesOfACommandByItNameDuringItExecution({ assert }: Context) { + assert.plan(1) + + CommanderHandler.commander + .command('test') + .argument('', 'The person name') + .option('-ag, --age', 'The person age') + .action(() => { + const opts = CommanderHandler.getCommandOptsValues('test') - const exec = CommanderHandler.bindHandler(new ListCommand()) + assert.deepEqual(opts, { age: true }) + }) - assert.isDefined(exec) + await CommanderHandler.parse(['node', 'artisan', 'test', 'João Lenon', '--age']) + } + + @Test() + public async shouldBeAbleToGetTheCommandsInfo({ assert }: Context) { + CommanderHandler.commander + .command('test') + .description('The test command description') + .argument('', 'The person name') + .option('-ag, --age', 'The person age') + .action(() => {}) + + const infos = CommanderHandler.getCommandsInfo() + + assert.deepEqual(infos, { 'test [options] ': 'The test command description' }) + } + + @Test() + public async shouldBeAbleToGetTheCommandsInfoByAlias({ assert }: Context) { + CommanderHandler.commander + .command('test') + .description('The test command description') + .argument('', 'The person name') + .option('-ag, --age', 'The person age') + .action(() => {}) + + CommanderHandler.commander + .command('make:test') + .description('The make:test command description') + .argument('', 'The person name') + .option('-ag, --age', 'The person age') + .action(() => {}) + + const infos = CommanderHandler.getCommandsInfo('make') + + assert.deepEqual(infos, { 'make:test [options] ': 'The make:test command description' }) + } + + @Test() + public async shouldBeAbleToBindTheExecMethodWithTheTargetToBeRegisteredAsTheCommandAction({ assert }: Context) { + class Command { + async __exec(...args: any[]) { + assert.equal(args[0], 'João Lenon') + assert.deepEqual(args[1], { age: true }) + } + } + + CommanderHandler.commander + .command('test') + .description('The test command description') + .argument('', 'The person name') + .option('-ag, --age', 'The person age') + .action(CommanderHandler.bindHandler(new Command())) + + await CommanderHandler.parse(['node', 'artisan', 'test', 'João Lenon', '--age']) + } + + @Test() + public async shouldBeAbleToRegisterAnExceptionHandlerToBeRegisteredWithTheTargetExecMethod({ assert }: Context) { + assert.plan(1) + + class Command { + async __exec() { + throw new Error('error') + } + } + + CommanderHandler.exceptionHandler = (error: Error) => { + assert.equal(error.message, 'error') + } + + CommanderHandler.commander + .command('test') + .description('The test command description') + .argument('', 'The person name') + .option('-ag, --age', 'The person age') + .action(CommanderHandler.bindHandler(new Command())) + + await CommanderHandler.parse(['node', 'artisan', 'test', 'João Lenon', '--age']) } } diff --git a/tests/unit/handlers/ConsoleExceptionHandlerTest.ts b/tests/unit/handlers/ConsoleExceptionHandlerTest.ts index 6bdc568..1c0b4ac 100644 --- a/tests/unit/handlers/ConsoleExceptionHandlerTest.ts +++ b/tests/unit/handlers/ConsoleExceptionHandlerTest.ts @@ -7,106 +7,90 @@ * file that was distributed with this source code. */ -import { Log } from '@athenna/logger' import { Exception } from '@athenna/common' import { ConsoleExceptionHandler } from '#src' -import { Test, type Context, Mock } from '@athenna/test' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' +import { Log, LoggerProvider } from '@athenna/logger' +import { Test, Mock, AfterEach, BeforeEach, type Context, type Stub } from '@athenna/test' -export default class ConsoleExceptionHandlerTest extends BaseCommandTest { - @Test() - public async shouldBeAbleToLogThePrettyExceptionFromErrorInstances({ assert }: Context) { - const stub = Log.when('channelOrVanilla').return({ error: () => {} }) - - await new ConsoleExceptionHandler().handle(new Error()) +export default class ConsoleExceptionHandlerTest { + public processExitMock: Stub - assert.calledWith(stub, 'exception') - assert.isTrue(this.processExit.calledOnceWith(1)) + @BeforeEach() + public beforeEach() { + new LoggerProvider().register() + this.processExitMock = Mock.when(process, 'exit').return(1) } - @Test() - public async shouldBeAbleToLogThePrettyExceptionFromExceptionInstances({ assert }: Context) { - const stub = Log.when('channelOrVanilla').return({ error: () => {} }) - - await new ConsoleExceptionHandler().handle(new Exception()) - - assert.calledWith(stub, 'exception') - assert.isTrue(this.processExit.calledOnceWith(1)) + @AfterEach() + public afterEach() { + ioc.reconstruct() + Mock.restoreAll() + Config.clear() } @Test() - public async shouldBeAbleToLogInTheConsoleChannelIfTheExceptionCodeIsE_SIMPLE_CLI({ assert }: Context) { - const stub = Log.when('channelOrVanilla').return({ error: () => {} }) + public async shouldBeAbleToHandleAVanillaError({ assert }: Context) { + const error = new Error('Test error') + const errorFake = Mock.fake() + Log.when('channelOrVanilla').return({ + error: errorFake + }) - await new ConsoleExceptionHandler().handle(new Exception({ code: 'E_SIMPLE_CLI' })) + await new ConsoleExceptionHandler().handle(error) - assert.calledWith(stub, 'console') - assert.isTrue(this.processExit.calledOnceWith(1)) + assert.calledWith(this.processExitMock, 1) + assert.calledWith(errorFake, await error.toAthennaException().prettify()) } @Test() - public async shouldBeAbleToHideErrorsIfDebugModeIsNotActivatedAndIsInternalError({ assert }: Context) { - Config.set('app.debug', false) - - const fake = Mock.sandbox.fake() - const stub = Log.when('channelOrVanilla').return({ error: fake }) - - await new ConsoleExceptionHandler().handle(new Error('hello')) - - const exception = new Exception({ - message: 'An internal server exception has occurred.', - code: 'E_INTERNAL_SERVER_ERROR', - status: 500 + public async shouldBeAbleToHandleAnAthennaException({ assert }: Context) { + const exception = new Exception({ message: 'Test error' }) + const errorFake = Mock.fake() + Log.when('channelOrVanilla').return({ + error: errorFake }) - delete exception.stack + await new ConsoleExceptionHandler().handle(exception) - assert.calledWith(stub, 'exception') - assert.isTrue(fake.calledWith(await exception.prettify())) - assert.isTrue(this.processExit.calledOnceWith(1)) + assert.calledWith(this.processExitMock, 1) + assert.calledWith(errorFake, await exception.prettify()) } @Test() - public async shouldBeAbleToHideErrorsIfDebugModeIsNotActivatedAndIsInternalTypeError({ assert }: Context) { - Config.set('app.debug', false) - - const fake = Mock.sandbox.fake() - const stub = Log.when('channelOrVanilla').return({ error: fake }) - - await new ConsoleExceptionHandler().handle(new TypeError('hello')) - - const exception = new Exception({ - message: 'An internal server exception has occurred.', - code: 'E_INTERNAL_SERVER_ERROR', - status: 500 + public async shouldLogOnlyTheErrorMessageIfTheErrorCodeIsSimpleCLI({ assert }: Context) { + const exception = new Exception({ code: 'E_SIMPLE_CLI', message: 'Test error' }) + const errorFake = Mock.fake() + Log.when('channelOrVanilla').return({ + error: errorFake }) - delete exception.stack + await new ConsoleExceptionHandler().handle(exception) - assert.calledWith(stub, 'exception') - assert.isTrue(fake.calledWith(await exception.prettify())) - assert.isTrue(this.processExit.calledOnceWith(1)) + assert.calledWith(this.processExitMock, 1) + assert.calledWith(errorFake, 'Test error') } @Test() - public async shouldBeAbleToHideErrorsIfDebugModeIsNotActivatedAndIsInternalSyntaxError({ assert }: Context) { + public async shouldBeAbleToHideErrorMessageCodeAndStackIfIsInternalErrorAndAppIsNotInDebugMode({ assert }: Context) { Config.set('app.debug', false) - const fake = Mock.sandbox.fake() - const stub = Log.when('channelOrVanilla').return({ error: fake }) + const error = new Error('Test error') + const errorFake = Mock.fake() + Log.when('channelOrVanilla').return({ + error: errorFake + }) + + await new ConsoleExceptionHandler().handle(error) - await new ConsoleExceptionHandler().handle(new SyntaxError('hello')) + const exception = error.toAthennaException() - const exception = new Exception({ - message: 'An internal server exception has occurred.', - code: 'E_INTERNAL_SERVER_ERROR', - status: 500 - }) + exception.name = 'Internal error' + exception.code = 'E_INTERNAL_ERROR' + exception.message = 'An internal error has occurred.' delete exception.stack - assert.calledWith(stub, 'exception') - assert.isTrue(fake.calledWith(await exception.prettify())) - assert.isTrue(this.processExit.calledOnceWith(1)) + assert.calledWith(this.processExitMock, 1) + assert.calledWith(errorFake, await exception.prettify()) } } diff --git a/tests/unit/helpers/ActionTest.ts b/tests/unit/helpers/ActionTest.ts index 224cbf1..15b0ec5 100644 --- a/tests/unit/helpers/ActionTest.ts +++ b/tests/unit/helpers/ActionTest.ts @@ -26,7 +26,7 @@ export default class ActionTest { @Test() public async shouldBeAbleToLogASucceededActionInTheStdout({ assert }: Context) { - const successFake = Mock.sandbox.fake() + const successFake = Mock.fake() const stub = Log.when('standalone').return({ success: _args => successFake(Color.removeColors(_args)) }) this.action.succeeded('app/Services/Service.ts') @@ -36,8 +36,8 @@ export default class ActionTest { } @Test() - public async shouldBeAbleToLogASkipedActionInTheStdout({ assert }: Context) { - const successFake = Mock.sandbox.fake() + public async shouldBeAbleToLogASkippedActionInTheStdout({ assert }: Context) { + const successFake = Mock.fake() const stub = Log.when('standalone').return({ success: _args => successFake(Color.removeColors(_args)) }) this.action.skipped('app/Services/Service.ts') @@ -47,8 +47,8 @@ export default class ActionTest { } @Test() - public async shouldBeAbleToLogASkipedActionWithReasonInTheStdout({ assert }: Context) { - const successFake = Mock.sandbox.fake() + public async shouldBeAbleToLogASkippedActionWithReasonInTheStdout({ assert }: Context) { + const successFake = Mock.fake() const stub = Log.when('standalone').return({ success: _args => successFake(Color.removeColors(_args)) }) this.action.skipped('app/Services/Service.ts', 'Some reason') @@ -59,7 +59,7 @@ export default class ActionTest { @Test() public async shouldBeAbleToLogAFailedActionInTheStdout({ assert }: Context) { - const successFake = Mock.sandbox.fake() + const successFake = Mock.fake() const stub = Log.when('standalone').return({ success: _args => successFake(Color.removeColors(_args)) }) this.action.failed('app/Services/Service.ts') @@ -70,7 +70,7 @@ export default class ActionTest { @Test() public async shouldBeAbleToLogAFailedActionWithReasonInTheStdout({ assert }: Context) { - const successFake = Mock.sandbox.fake() + const successFake = Mock.fake() const stub = Log.when('standalone').return({ success: _args => successFake(Color.removeColors(_args)) }) this.action.failed('app/Services/Service.ts', 'Some reason') @@ -82,7 +82,7 @@ export default class ActionTest { @Test() public async shouldBeAbleToCreateAnActionInstanceWhereTheErrorActionIsTheBiggest({ assert }: Context) { const action = new Action('OK') - const successFake = Mock.sandbox.fake() + const successFake = Mock.fake() const stub = Log.when('standalone').return({ success: _args => successFake(Color.removeColors(_args)) }) action.succeeded('app/Services/Service.ts') diff --git a/tests/unit/helpers/command/PromptTest.ts b/tests/unit/helpers/command/PromptTest.ts index 586f536..1faf910 100644 --- a/tests/unit/helpers/command/PromptTest.ts +++ b/tests/unit/helpers/command/PromptTest.ts @@ -7,31 +7,22 @@ * file that was distributed with this source code. */ -import inquirer from 'inquirer' - import { Prompt } from '#src/helpers/command/Prompt' -import { Test, AfterAll, BeforeEach, type Context } from '@athenna/test' +import { Test, type Context, Mock, AfterEach } from '@athenna/test' import { InquirerPromptException } from '#src/exceptions/InquirerPromptException' export default class PromptTest { private prompt = new Prompt() - @BeforeEach() - public async beforeEach() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.prompt.inquirer = _ => Promise.resolve({ raw: 'value' }) - } - - @AfterAll() + @AfterEach() public async afterAll() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.prompt.inquirer = inquirer.createPromptModule() + Mock.restoreAll() } @Test() public async shouldBeAbleToPromptInputs({ assert }: Context) { + Mock.when(this.prompt, 'inquirer').resolve({ raw: 'value' }) + const value = await this.prompt.input('What is the value?') assert.equal(value, 'value') @@ -39,6 +30,8 @@ export default class PromptTest { @Test() public async shouldBeAbleToPromptSecrets({ assert }: Context) { + Mock.when(this.prompt, 'inquirer').resolve({ raw: 'value' }) + const value = await this.prompt.secret('What is the value?') assert.equal(value, 'value') @@ -46,6 +39,8 @@ export default class PromptTest { @Test() public async shouldBeAbleToPromptConfirm({ assert }: Context) { + Mock.when(this.prompt, 'inquirer').resolve({ raw: 'value' }) + const value = await this.prompt.confirm('What is the value?') assert.equal(value, 'value') @@ -53,6 +48,8 @@ export default class PromptTest { @Test() public async shouldBeAbleToPromptEditor({ assert }: Context) { + Mock.when(this.prompt, 'inquirer').resolve({ raw: 'value' }) + const value = await this.prompt.editor('What is the value?') assert.equal(value, 'value') @@ -60,6 +57,8 @@ export default class PromptTest { @Test() public async shouldBeAbleToPromptList({ assert }: Context) { + Mock.when(this.prompt, 'inquirer').resolve({ raw: 'value' }) + const value = await this.prompt.list('What is the value?', ['value', 'other value']) assert.equal(value, 'value') @@ -67,6 +66,8 @@ export default class PromptTest { @Test() public async shouldBeAbleToPromptExpand({ assert }: Context) { + Mock.when(this.prompt, 'inquirer').resolve({ raw: 'value' }) + const value = await this.prompt.expand('What is the value?', [{ name: 'value', key: 'value' }]) assert.equal(value, 'value') @@ -74,6 +75,8 @@ export default class PromptTest { @Test() public async shouldBeAbleToPromptCheckbox({ assert }: Context) { + Mock.when(this.prompt, 'inquirer').resolve({ raw: 'value' }) + const value = await this.prompt.checkbox('What is the value?', ['value', 'other value']) assert.equal(value, ['value']) @@ -81,9 +84,9 @@ export default class PromptTest { @Test() public async shouldThrowInquirerPromptExceptionWhenPromptFails({ assert }: Context) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.prompt.inquirer = _ => Promise.reject(new Error('value')) + assert.plan(1) + + Mock.when(this.prompt, 'inquirer').reject(new Error('value')) try { await this.prompt.input('What is the value?') diff --git a/tests/unit/kernels/ConsoleKernelTest.ts b/tests/unit/kernels/ConsoleKernelTest.ts index e177eac..6bb13f2 100644 --- a/tests/unit/kernels/ConsoleKernelTest.ts +++ b/tests/unit/kernels/ConsoleKernelTest.ts @@ -7,65 +7,287 @@ * file that was distributed with this source code. */ -import 'reflect-metadata' +import { ArtisanProvider, CommanderHandler, ConsoleKernel } from '#src' +import { Test, BeforeEach, AfterEach, type Context } from '@athenna/test' -import { Test, type Context } from '@athenna/test' -import { CommanderHandler, ConsoleKernel } from '#src' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' -import { ThrowCommand } from '#tests/fixtures/commands/ThrowCommand' +export default class ConsoleKernelTest { + @BeforeEach() + public beforeEach() { + new ArtisanProvider().register() + } + + @AfterEach() + public afterEach() { + Config.clear() + ioc.reconstruct() + CommanderHandler.reconstruct() + CommanderHandler.exceptionHandler = undefined + } -export default class ConsoleKernelTest extends BaseCommandTest { @Test() - public async shouldBeAbleToRegisterCommandsUsingConsoleKernel({ assert }: Context) { - CommanderHandler.getCommander().commands = [] + public async shouldBeAbleToRegisterCommandsUsingRelativeJSPath({ assert }: Context) { + Config.set('rc.commands', { + 'template:customize': './src/commands/TemplateCustomizeCommand.js' + }) - await new ConsoleKernel().registerCommands() + const kernel = new ConsoleKernel() - assert.isDefined(CommanderHandler.getCommands()['make:command ']) + await kernel.registerCommands() + + assert.isTrue(CommanderHandler.hasCommand('template:customize')) } @Test() - public async shouldBeAbleToRegisterCommandsByArgvUsingConsoleKernel({ assert }: Context) { - CommanderHandler.getCommander().commands = [] + public async shouldBeAbleToRegisterCommandsUsingRelativeTSPath({ assert }: Context) { + Config.set('rc.commands', { + 'template:customize': './src/commands/TemplateCustomizeCommand.ts' + }) + + const kernel = new ConsoleKernel() - await new ConsoleKernel().registerCommands(['node', 'artisan', 'make:command']) + await kernel.registerCommands() - assert.isDefined(CommanderHandler.getCommands()['make:command ']) + assert.isTrue(CommanderHandler.hasCommand('template:customize')) } @Test() - public async shouldBeAbleToRegisterRouteFilesUsingConsoleKernel({ assert }: Context) { - CommanderHandler.getCommander().commands = [] + public async shouldBeAbleToRegisterCommandsUsingFullJSPath({ assert }: Context) { + Config.set('rc.commands', { + 'template:customize': Path.src('commands/TemplateCustomizeCommand.js') + }) + + const kernel = new ConsoleKernel() - await new ConsoleKernel().registerRouteCommands('./bin/console.ts') + await kernel.registerCommands() + + assert.isTrue(CommanderHandler.hasCommand('template:customize')) + } + + @Test() + public async shouldBeAbleToRegisterCommandsUsingFullTSPath({ assert }: Context) { + Config.set('rc.commands', { + 'template:customize': Path.src('commands/TemplateCustomizeCommand.ts') + }) + + const kernel = new ConsoleKernel() + + await kernel.registerCommands() + + assert.isTrue(CommanderHandler.hasCommand('template:customize')) + } + + @Test() + public async shouldBeAbleToRegisterCommandsUsingImportAliasPath({ assert }: Context) { + Config.set('rc.commands', { + 'template:customize': '#src/commands/TemplateCustomizeCommand' + }) + + const kernel = new ConsoleKernel() + + await kernel.registerCommands() + + assert.isTrue(CommanderHandler.hasCommand('template:customize')) + } + + @Test() + public async shouldBeAbleToRegisterMultipleCommandsConcurrently({ assert }: Context) { + Config.set('rc.commands', { + 'make:command': '#src/commands/MakeCommandCommand', + 'template:customize': '#src/commands/TemplateCustomizeCommand' + }) + + const kernel = new ConsoleKernel() + + await kernel.registerCommands() + + assert.isTrue(CommanderHandler.hasCommand('make:command')) + assert.isTrue(CommanderHandler.hasCommand('template:customize')) + } + + @Test() + public async shouldRegisterAllCommandsWhenArgvIsTheCLIEntrypoint({ assert }: Context) { + Config.set('rc.commands', { + 'make:command': '#src/commands/MakeCommandCommand', + 'template:customize': '#src/commands/TemplateCustomizeCommand' + }) + + const kernel = new ConsoleKernel() + + await kernel.registerCommands(['node', 'artisan']) + + assert.isTrue(CommanderHandler.hasCommand('make:command')) + assert.isTrue(CommanderHandler.hasCommand('template:customize')) + } + + @Test() + public async shouldRegisterOnlyTheCommandExecutedInCLIPresentInTheArgv({ assert }: Context) { + Config.set('rc.commands', { + 'make:command': '#src/commands/MakeCommandCommand', + 'template:customize': '#src/commands/TemplateCustomizeCommand' + }) + + const kernel = new ConsoleKernel() + + await kernel.registerCommands(['node', 'artisan', 'make:command']) + + assert.isTrue(CommanderHandler.hasCommand('make:command')) + assert.isFalse(CommanderHandler.hasCommand('template:customize')) + } + + @Test() + public async shouldRegisterAllCommandsIfCommandNameInRcIsNotTheSameOfItsSignature({ assert }: Context) { + Config.set('rc.commands', { + makeCommand: { + path: '#src/commands/MakeCommandCommand', + loadAllCommands: true + }, + 'template:customize': '#src/commands/TemplateCustomizeCommand' + }) + + const kernel = new ConsoleKernel() + + await kernel.registerCommands(['node', 'artisan', 'make:command']) + + assert.isTrue(CommanderHandler.hasCommand('make:command')) + assert.isTrue(CommanderHandler.hasCommand('template:customize')) + } - assert.deepEqual(Config.get('rc.commands.hello'), { - loadApp: false, - stayAlive: false, - environments: ['hello'] + @Test() + public async shouldRegisterAllCommandsIfCommandHasTheSettingLoadAllCommands({ assert }: Context) { + Config.set('rc.commands', { + 'make:command': { + path: '#src/commands/MakeCommandCommand', + loadAllCommands: true + }, + 'template:customize': '#src/commands/TemplateCustomizeCommand' }) - assert.isDefined(CommanderHandler.getCommands()['hello ']) + + const kernel = new ConsoleKernel() + + await kernel.registerCommands(['node', 'artisan', 'make:command']) + + assert.isTrue(CommanderHandler.hasCommand('make:command')) + assert.isTrue(CommanderHandler.hasCommand('template:customize')) + } + + @Test() + public async shouldBeAbleToRegisterRouteCommandsWithRelativeTSFilePath({ assert }: Context) { + const kernel = new ConsoleKernel() + + await kernel.registerRouteCommands(`./tests/fixtures/routes/console.ts?version=${Math.random()}`) + + assert.isTrue(CommanderHandler.hasCommand('test:one')) + assert.isTrue(CommanderHandler.hasCommand('test:two')) + assert.isTrue(CommanderHandler.hasCommand('test:three')) + } + + @Test() + public async shouldBeAbleToRegisterRouteCommandsWithRelativeJSFilePath({ assert }: Context) { + const kernel = new ConsoleKernel() + + await kernel.registerRouteCommands(`./tests/fixtures/routes/console.js?version=${Math.random()}`) + + assert.isTrue(CommanderHandler.hasCommand('test:one')) + assert.isTrue(CommanderHandler.hasCommand('test:two')) + assert.isTrue(CommanderHandler.hasCommand('test:three')) } @Test() - public async shouldBeAbleToRegisterRouteFilesWithImportAliasUsingConsoleKernel({ assert }: Context) { - CommanderHandler.getCommander().commands = [] + public async shouldBeAbleToRegisterRouteCommandsWithFullTSFilePath({ assert }: Context) { + const kernel = new ConsoleKernel() - await new ConsoleKernel().registerRouteCommands('#tests/fixtures/routes/console') + await kernel.registerRouteCommands(Path.fixtures(`routes/console.ts?version=${Math.random()}`)) - assert.isDefined(CommanderHandler.getCommands()['importalias']) + assert.isTrue(CommanderHandler.hasCommand('test:one')) + assert.isTrue(CommanderHandler.hasCommand('test:two')) + assert.isTrue(CommanderHandler.hasCommand('test:three')) } @Test() - public async shouldBeAbleToSetCustomExceptionHandlerUsingConsoleKernel({ assert }: Context) { - CommanderHandler.setExceptionHandler(null) + public async shouldBeAbleToRegisterRouteCommandsWithFullJSFilePath({ assert }: Context) { + const kernel = new ConsoleKernel() - await new ConsoleKernel().registerExceptionHandler('#tests/fixtures/handlers/Handler') + await kernel.registerRouteCommands(Path.fixtures(`routes/console.js?version=${Math.random()}`)) + + assert.isTrue(CommanderHandler.hasCommand('test:one')) + assert.isTrue(CommanderHandler.hasCommand('test:two')) + assert.isTrue(CommanderHandler.hasCommand('test:three')) + } + + @Test() + public async shouldBeAbleToRegisterRouteCommandsWithImportAliasPath({ assert }: Context) { + const kernel = new ConsoleKernel() + + await kernel.registerRouteCommands('#tests/fixtures/routes/console') + + assert.isTrue(CommanderHandler.hasCommand('test:one')) + assert.isTrue(CommanderHandler.hasCommand('test:two')) + assert.isTrue(CommanderHandler.hasCommand('test:three')) + } + + @Test() + public async shouldBeAbleToRegisterTheDefaultAthennaConsoleExceptionHandler({ assert }: Context) { + const kernel = new ConsoleKernel() + + assert.isUndefined(CommanderHandler.exceptionHandler) + + await kernel.registerExceptionHandler() + + assert.isDefined(CommanderHandler.exceptionHandler) + } + + @Test() + public async shouldBeAbleToRegisterTheCustomConsoleExceptionHandlerRelativeTSPath({ assert }: Context) { + const kernel = new ConsoleKernel() + + assert.isUndefined(CommanderHandler.exceptionHandler) + + await kernel.registerExceptionHandler('./tests/fixtures/CustomConsoleExceptionHandler.ts') + + assert.isDefined(CommanderHandler.exceptionHandler) + } + + @Test() + public async shouldBeAbleToRegisterTheCustomConsoleExceptionHandlerRelativeJSPath({ assert }: Context) { + const kernel = new ConsoleKernel() + + assert.isUndefined(CommanderHandler.exceptionHandler) + + await kernel.registerExceptionHandler('./tests/fixtures/CustomConsoleExceptionHandler.js') + + assert.isDefined(CommanderHandler.exceptionHandler) + } + + @Test() + public async shouldBeAbleToRegisterTheCustomConsoleExceptionHandlerFullTSPath({ assert }: Context) { + const kernel = new ConsoleKernel() + + assert.isUndefined(CommanderHandler.exceptionHandler) + + await kernel.registerExceptionHandler(Path.fixtures('CustomConsoleExceptionHandler.ts')) + + assert.isDefined(CommanderHandler.exceptionHandler) + } + + @Test() + public async shouldBeAbleToRegisterTheCustomConsoleExceptionHandlerFullJSPath({ assert }: Context) { + const kernel = new ConsoleKernel() + + assert.isUndefined(CommanderHandler.exceptionHandler) + + await kernel.registerExceptionHandler(Path.fixtures('CustomConsoleExceptionHandler.js')) + + assert.isDefined(CommanderHandler.exceptionHandler) + } + + @Test() + public async shouldBeAbleToRegisterTheCustomConsoleExceptionHandlerImportAliasPath({ assert }: Context) { + const kernel = new ConsoleKernel() - const exec = CommanderHandler.bindHandler(new ThrowCommand()) + assert.isUndefined(CommanderHandler.exceptionHandler) - await exec() + await kernel.registerExceptionHandler('#tests/fixtures/CustomConsoleExceptionHandler') - assert.isTrue(this.processExit.calledWith(1)) + assert.isDefined(CommanderHandler.exceptionHandler) } } diff --git a/tests/unit/testing/plugins/CommandPluginTest.ts b/tests/unit/testing/plugins/CommandPluginTest.ts index 56a1a4f..796c323 100644 --- a/tests/unit/testing/plugins/CommandPluginTest.ts +++ b/tests/unit/testing/plugins/CommandPluginTest.ts @@ -7,43 +7,254 @@ * file that was distributed with this source code. */ -import { Test, type Context } from '@athenna/test' -import { BaseCommandTest } from '#tests/helpers/BaseCommandTest' +import { ArtisanProvider } from '#src/index' +import { TestCommand } from '#src/testing/plugins/index' +import { Test, type Context, BeforeEach, AfterEach, Fails } from '@athenna/test' + +export default class CommandPluginTest { + @BeforeEach() + public beforeEach() { + new ArtisanProvider().register() + TestCommand.setArtisanPath(Path.fixtures('consoles/plugin-console.ts')) + } + + @AfterEach() + public afterEach() { + ioc.reconstruct() + } -export default class CommandPluginTest extends BaseCommandTest { @Test() public async shouldBeAbleToExecuteCommandsUsingCommandPlugin({ command }: Context) { - const output = await command.run('test hello') + const output = await command.run('plugin') output.assertSucceeded() - output.assertLogged('hello notRequiredArg undefined notRequiredOption') - output.assertLogged("true tests|node_modules [ 'tests', 'node_modules' ]") } @Test() - public async shouldBeAbleToAssertThatTheExitCodeIsNotSomeValue({ command }: Context) { - const output = await command.run('test hello') + public async shouldBeAbleToAssertThatCommandHasLoggedAMessage({ command }: Context) { + const output = await command.run('plugin') - output.assertIsNotExitCode(1) + output.assertSucceeded() + output.assertLogged('Hello stdout world!') + output.assertLogged('Hello stderr world!') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasLoggedAMessageSpecificallyInStdout({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogged('Hello stdout world!', 'stdout') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasLoggedAMessageSpecificallyInStderr({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogged('Hello stderr world!', 'stderr') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasNotLoggedAMessage({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertNotLogged('Not logged message') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasNotLoggedAMessageSpecificallyInStdout({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertNotLogged('Hello stderr world!', 'stdout') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasNotLoggedAMessageSpecificallyInStderr({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertNotLogged('Hello stdout world!', 'stderr') + } + + @Test() + @Fails() + public async shouldFailIfLoggedAssertionFail({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogged('Not logged message') + } + + @Test() + @Fails() + public async shouldFailIfLoggedAssertionFailInStdout({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogged('Hello stderr world!', 'stdout') + } + + @Test() + @Fails() + public async shouldFailIfLoggedAssertionFailInStderr({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogged('Hello stdout world!', 'stderr') + } + + @Test() + @Fails() + public async shouldFailIfNotLoggedAssertionFailInStdout({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertNotLogged('Hello stdout world!', 'stdout') } @Test() - public async shouldBeAbleToAssertThatTheLogMessageMatchesARegex({ command }: Context) { - const output = await command.run('test hello') + @Fails() + public async shouldFailIfNotLoggedAssertionFailInStderr({ command }: Context) { + const output = await command.run('plugin') - output.assertLogMatches(/hello notRequiredArg undefined notRequiredOption/) + output.assertSucceeded() + output.assertNotLogged('Hello stderr world!', 'stderr') } @Test() - public async shouldBeAbleToAssertThatTheCommandFailed({ command }: Context) { - const output = await command.run('test') + public async shouldBeAbleToAssertThatCommandHasFailed({ command }: Context) { + const output = await command.run('plugin --throw') output.assertFailed() } + @Test() + public async shouldBeAbleToAssertTheCommandMatchesExitCodeZero({ command }: Context) { + const output = await command.run('plugin') + + output.assertExitCode(0) + } + + @Test() + public async shouldBeAbleToAssertTheCommandMatchesExitCodeOne({ command }: Context) { + const output = await command.run('plugin --throw') + + output.assertExitCode(1) + } + + @Test() + public async shouldBeAbleToAssertTheCommandDoesNotMatchExitCodeZero({ command }: Context) { + const output = await command.run('plugin --throw') + + output.assertIsNotExitCode(0) + } + + @Test() + public async shouldBeAbleToAssertTheCommandDoesNotMatchExitCodeOne({ command }: Context) { + const output = await command.run('plugin') + + output.assertIsNotExitCode(1) + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasLoggedAMessageThatMatchesTheRegexp({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogMatches(/Hello stdout world!/) + output.assertLogMatches(/Hello stderr world!/) + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasLoggedAMessageThatMatchesTheRegexpSpecificallyInStdout({ + command + }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogMatches(/Hello stdout world!/, 'stdout') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasLoggedAMessageThatMatchesTheRegexpSpecificallyInStderr({ + command + }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogMatches(/Hello stderr world!/, 'stderr') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasNotLoggedAMessageThatMatchesTheRegexp({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogNotMatches(/Not logged message/) + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasNotLoggedAMessageThatMatchesTheRegexpSpecificallyInStdout({ + command + }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogNotMatches(/Hello stderr world!/, 'stdout') + } + + @Test() + public async shouldBeAbleToAssertThatCommandHasNotLoggedAMessageThatMatchesTheRegexpSpecificallyInStderr({ + command + }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogNotMatches(/Hello stdout world!/, 'stderr') + } + + @Test() + @Fails() + public async shouldFailIfLogMatchesAssertionFailInStdout({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogMatches(/Hello stderr world!/, 'stdout') + } + + @Test() + @Fails() + public async shouldFailIfLogMatchesAssertionFailInStderr({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogMatches(/Hello stdout world!/, 'stderr') + } + + @Test() + @Fails() + public async shouldFailIfLogNotMatchesAssertionFailInStdout({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogNotMatches(/Hello stdout world!/, 'stdout') + } + + @Test() + @Fails() + public async shouldFailIfLogNotMatchesAssertionFailInStderr({ command }: Context) { + const output = await command.run('plugin') + + output.assertSucceeded() + output.assertLogNotMatches(/Hello stderr world!/, 'stderr') + } + @Test() public async shouldBeAbleToCheckAndManipulateTheCommandOutput({ assert, command }: Context) { - const output = await command.run('test hello') + const output = await command.run('plugin') assert.isDefined(output.output.stdout) assert.isDefined(output.output.stderr) diff --git a/tsconfig.json b/tsconfig.json index 5a20cac..1999c40 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,8 @@ "#bin/*": ["./bin/*.ts"], "#src/*": ["./src/*.ts"], "#tests/*": ["./tests/*.ts"], - "#src/types": ["./src/types/index.ts"] + "#src/types": ["./src/types/index.ts"], + "#src/debug": ["./src/debug/index.ts"] } }, "include": ["./**/*"], From 134398137648dea5b9a5f677d1ba2e1f4c69bfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lenon?= Date: Wed, 27 Sep 2023 19:51:20 +0100 Subject: [PATCH 2/2] test(fix): better assertion --- tests/unit/commands/MakeCommandTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/commands/MakeCommandTest.ts b/tests/unit/commands/MakeCommandTest.ts index 58c61a5..46c59f6 100644 --- a/tests/unit/commands/MakeCommandTest.ts +++ b/tests/unit/commands/MakeCommandTest.ts @@ -59,8 +59,8 @@ export default class MakeCommandTest extends BaseTest { output.assertFailed() output.assertLogged('[ MAKING COMMAND ]') - output.assertLogged( - 'The file ({yellow} "/Users/jlenon7/Development/Athenna/Artisan/app/console/commands/TestCommand.ts") already exists' - ) + output.assertLogged('The file') + output.assertLogged('TestCommand.ts') + output.assertLogged('already exists') } }