From 09bc2ad298a1e451a10a5bae72f9c3181a092598 Mon Sep 17 00:00:00 2001 From: nosleepman1 Date: Sat, 11 Jul 2026 03:25:15 +0000 Subject: [PATCH] test: add PHPUnit tests, GitHub Actions workflow, and configuration --- .github/workflows/tests.yml | 47 +++++++++ .gitignore | 3 + composer.json | 19 +++- phpunit.xml.dist | 11 ++ tests/GenerateModuleCommandTest.php | 157 ++++++++++++++++++++++++++++ 5 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 phpunit.xml.dist create mode 100644 tests/GenerateModuleCommandTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..068c6f8 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: Run Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [8.1, 8.2, 8.3, 8.4] + laravel: [10.*, 11.*, 12.*, 13.*] + exclude: + # Laravel 11+ requires PHP 8.2+ + - php: 8.1 + laravel: 11.* + - php: 8.1 + laravel: 12.* + - php: 8.1 + laravel: 13.* + # Laravel 12+ requires PHP 8.2+ + # Laravel 13+ requires PHP 8.2+ (or 8.3+ depending on final specs, but excluding 8.1 is safe) + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:^8.0 || ^9.0 || ^10.0 || ^11.0" --no-interaction --no-update + composer update --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e33e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ +.phpunit.result.cache +composer.lock \ No newline at end of file diff --git a/composer.json b/composer.json index ec92e39..53660f1 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,20 @@ "php": "^8.1", "laravel/framework": "^10.0 || ^11.0 || ^12.0 || ^13.0" }, - "require-dev": { - "phpunit/phpunit": "^10.0" + "autoload-dev": { + "psr-4": { + "Nosleepman\\ArchCLI\\Tests\\": "tests/" + } }, "minimum-stability": "stable", - "prefer-stable": true -} \ No newline at end of file + "prefer-stable": true, + "require-dev": { + "phpunit/phpunit": "^10.0", + "orchestra/testbench": "^8.0 || ^9.0 || ^10.0 || ^11.0" + }, + "config": { + "audit": { + "block-insecure": false + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..99c017f --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,11 @@ + + + + + ./tests + + + diff --git a/tests/GenerateModuleCommandTest.php b/tests/GenerateModuleCommandTest.php new file mode 100644 index 0000000..6a4ac44 --- /dev/null +++ b/tests/GenerateModuleCommandTest.php @@ -0,0 +1,157 @@ +cleanUp(); + } + + protected function tearDown(): void + { + $this->cleanUp(); + parent::tearDown(); + } + + private function cleanUp() + { + File::deleteDirectory(app_path('Models')); + File::deleteDirectory(app_path('Http')); + File::deleteDirectory(app_path('Services')); + File::deleteDirectory(app_path('Repositories')); + File::deleteDirectory(app_path('Events')); + File::deleteDirectory(app_path('Listeners')); + File::deleteDirectory(app_path('Notifications')); + File::deleteDirectory(app_path('Policies')); + File::deleteDirectory(database_path('migrations')); + File::delete(app_path('Product.php')); + File::delete(app_path('Category.php')); + } + + /** + * Test full module generation with all components, including Service and Repository. + */ + public function test_it_generates_complete_module_with_service_and_repository() + { + $this->artisan('make:module', ['name' => 'Product']) + ->expectsQuestion('Field (or empty to finish)', 'title:string:unique') + ->expectsQuestion('Field (or empty to finish)', 'price:integer:nullable') + ->expectsQuestion('Field (or empty to finish)', '') + ->expectsChoice('Controller version', 'v1', ['v1', 'v2', 'v3']) + ->expectsConfirmation('Include policies?', 'yes') + ->expectsConfirmation('Include service layer?', 'yes') + ->expectsConfirmation('Include repository layer?', 'yes') + ->expectsConfirmation('Include events and listeners?', 'yes') + ->assertExitCode(0); + + // Assert Model and fillable fields + $modelPath = app_path('Models/Product.php'); + if (!File::exists($modelPath)) { + $modelPath = app_path('Product.php'); + } + $this->assertTrue(File::exists($modelPath)); + $modelContent = File::get($modelPath); + $this->assertStringContainsString("protected \$fillable = ['title', 'price'];", $modelContent); + + // Assert Migration files + $migrations = File::files(database_path('migrations')); + $this->assertCount(1, $migrations); + $migrationContent = File::get($migrations[0]); + $this->assertStringContainsString("\$table->string('title')->unique();", $migrationContent); + $this->assertStringContainsString("\$table->integer('price')->nullable();", $migrationContent); + + // Assert Repository + $repositoryPath = app_path('Repositories/ProductRepository.php'); + $this->assertTrue(File::exists($repositoryPath)); + $repositoryContent = File::get($repositoryPath); + $this->assertStringContainsString('class ProductRepository', $repositoryContent); + $this->assertStringContainsString('Product::create($data)', $repositoryContent); + + // Assert Service (uses Repository) + $servicePath = app_path('Services/ProductService.php'); + $this->assertTrue(File::exists($servicePath)); + $serviceContent = File::get($servicePath); + $this->assertStringContainsString('protected ProductRepository $repository', $serviceContent); + $this->assertStringContainsString('$this->repository->create($data)', $serviceContent); + $this->assertStringContainsString('use App\Events\ProductCreated;', $serviceContent); + $this->assertStringContainsString('event(new ProductCreated($model));', $serviceContent); + + // Assert Controller + $controllerPath = app_path('Http/Controllers/Api/V1/ProductController.php'); + $this->assertTrue(File::exists($controllerPath)); + $controllerContent = File::get($controllerPath); + $this->assertStringContainsString('protected ProductService $service', $controllerContent); + + // Assert Form Requests and dynamic validation rules + $storeRequestPath = app_path('Http/Requests/Product/StoreProductRequest.php'); + $this->assertTrue(File::exists($storeRequestPath)); + $storeRequestContent = File::get($storeRequestPath); + $this->assertStringContainsString("'title' => 'required|string|max:255|unique:titles'", $storeRequestContent); + $this->assertStringContainsString("'price' => 'required|integer'", $storeRequestContent); + + $updateRequestPath = app_path('Http/Requests/Product/UpdateProductRequest.php'); + $this->assertTrue(File::exists($updateRequestPath)); + $updateRequestContent = File::get($updateRequestPath); + $this->assertStringContainsString("'title' => 'required|string|max:255|unique:titles'", $updateRequestContent); + + // Assert API Resource + $resourcePath = app_path('Http/Resources/ProductResource.php'); + $this->assertTrue(File::exists($resourcePath)); + $resourceContent = File::get($resourcePath); + $this->assertStringContainsString("'title' => \$this->title", $resourceContent); + $this->assertStringContainsString("'price' => \$this->price", $resourceContent); + + // Assert Event, Listener, Notification, Policy + $this->assertTrue(File::exists(app_path('Events/ProductCreated.php'))); + $this->assertTrue(File::exists(app_path('Listeners/ProductCreatedListener.php'))); + $this->assertTrue(File::exists(app_path('Notifications/ProductCreatedNotification.php'))); + $this->assertTrue(File::exists(app_path('Policies/ProductPolicy.php'))); + } + + /** + * Test module generation without Service Layer and without Repository. + */ + public function test_it_generates_module_without_service_and_repository() + { + $this->artisan('make:module', ['name' => 'Category']) + ->expectsQuestion('Field (or empty to finish)', 'name:string') + ->expectsQuestion('Field (or empty to finish)', '') + ->expectsChoice('Controller version', 'v2', ['v1', 'v2', 'v3']) + ->expectsConfirmation('Include policies?', 'no') + ->expectsConfirmation('Include service layer?', 'no') + ->expectsConfirmation('Include repository layer?', 'no') + ->expectsConfirmation('Include events and listeners?', 'no') + ->assertExitCode(0); + + // Assert Category Model exists + $modelPath = app_path('Models/Category.php'); + if (!File::exists($modelPath)) { + $modelPath = app_path('Category.php'); + } + $this->assertTrue(File::exists($modelPath)); + + // Assert Category Controller exists and uses Model directly + $controllerPath = app_path('Http/Controllers/Api/V2/CategoryController.php'); + $this->assertTrue(File::exists($controllerPath)); + $controllerContent = File::get($controllerPath); + $this->assertStringNotContainsString('protected CategoryService $service', $controllerContent); + $this->assertStringContainsString('Category::all()', $controllerContent); + + // Assert Service & Repository do NOT exist + $this->assertFalse(File::exists(app_path('Services/CategoryService.php'))); + $this->assertFalse(File::exists(app_path('Repositories/CategoryRepository.php'))); + } +}