diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..b08a2af
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,64 @@
+name: Tests
+
+on:
+ pull_request:
+ branches:
+ - main
+ types: [opened, synchronize, reopened]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php-version: ['8.3', '8.4']
+ fail-fast: false
+
+ services:
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: test_openapi_theme
+ ports:
+ - 3306:3306
+ options: >-
+ --health-cmd="mysqladmin ping"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=3
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-version }}
+ extensions: intl, zip, pdo_mysql
+ coverage: none
+
+ - name: Get composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Wait for MySQL
+ run: |
+ while ! mysqladmin ping -h"127.0.0.1" -P"3306" --silent; do
+ sleep 1
+ done
+
+ - name: Run tests
+ run: vendor/bin/phpunit
+ env:
+ DATABASE_URL: mysql://root:secret@127.0.0.1/test_openapi_theme?encoding=utf8mb4&timezone=UTC&cacheMetadata=true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2b0d4df..aa76ed4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
/vendor/
/.idea/
.DS_Store
+/tests/test_app/src/Controller
diff --git a/Dockerfile.test b/Dockerfile.test
new file mode 100644
index 0000000..1576836
--- /dev/null
+++ b/Dockerfile.test
@@ -0,0 +1,18 @@
+ARG PHP_VERSION
+FROM php:${PHP_VERSION}-cli
+
+RUN apt-get update && apt-get install -y \
+ libicu-dev \
+ libzip-dev \
+ unzip \
+ git \
+ && docker-php-ext-install \
+ intl \
+ zip \
+ pdo_mysql
+
+COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
+
+WORKDIR /app
+
+CMD ["vendor/bin/phpunit"]
\ No newline at end of file
diff --git a/composer.json b/composer.json
index b1502bf..5388d17 100644
--- a/composer.json
+++ b/composer.json
@@ -7,10 +7,14 @@
"php": ">=8.3",
"cakephp/cakephp": "^5.0 || ^5.1",
"cakephp/bake": "^3.1",
- "zircote/swagger-php": "^4.9"
+ "zircote/swagger-php": "^4.9",
+ "cakephp/twig-view": "^2.0.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.1.0"
+ "phpunit/phpunit": "^10.1.0",
+ "cakephp/chronos": "^3.0",
+ "cakephp/migrations": "^4.0",
+ "cakephp/plugin-installer": "^2.0"
},
"autoload": {
"psr-4": {
@@ -20,6 +24,7 @@
"autoload-dev": {
"psr-4": {
"OpenApiTheme\\Test\\": "tests/",
+ "TestApp\\": "tests/test_app/src/",
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
}
},
@@ -27,7 +32,8 @@
"bin/build-swagger-json"
],
"scripts": {
- "build:swagger": "build-swagger-json"
+ "build:swagger": "build-swagger-json",
+ "test": "phpunit"
},
"config": {
"allow-plugins": {
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
new file mode 100644
index 0000000..e558e32
--- /dev/null
+++ b/docker-compose.test.yml
@@ -0,0 +1,37 @@
+version: '3.8'
+
+services:
+ test-php84:
+ build:
+ context: .
+ dockerfile: Dockerfile.test
+ args:
+ PHP_VERSION: "8.4"
+ volumes:
+ - .:/app
+ depends_on:
+ - db-test
+ environment:
+ - DATABASE_URL=mysql://root:secret@db-test/test_openapi_theme?encoding=utf8mb4&timezone=UTC&cacheMetadata=true
+
+ test-php83:
+ build:
+ context: .
+ dockerfile: Dockerfile.test
+ args:
+ PHP_VERSION: "8.3"
+ volumes:
+ - .:/app
+ depends_on:
+ - db-test
+ environment:
+ - DATABASE_URL=mysql://root:secret@db-test/test_openapi_theme?encoding=utf8mb4&timezone=UTC&cacheMetadata=true
+
+ db-test:
+ image: mysql:8.0
+ environment:
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: test_openapi_theme
+ command: --default-authentication-plugin=mysql_native_password
+ ports:
+ - "3306"
\ No newline at end of file
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 0000000..7ed096d
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# コンテナをビルド
+docker-compose -f docker-compose.test.yml build
+
+# PHP 8.4でテスト実行
+echo "Running tests with PHP 8.4..."
+docker-compose -f docker-compose.test.yml run --rm test-php84 composer install
+docker-compose -f docker-compose.test.yml run --rm test-php84
+
+# PHP 8.3でテスト実行
+echo "Running tests with PHP 8.3..."
+docker-compose -f docker-compose.test.yml run --rm test-php83 composer install
+docker-compose -f docker-compose.test.yml run --rm test-php83
+
+# コンテナを停止・削除
+docker-compose -f docker-compose.test.yml down
\ No newline at end of file
diff --git a/tests/Fixture/TestTablesFixture.php b/tests/Fixture/TestTablesFixture.php
new file mode 100644
index 0000000..8be1d3d
--- /dev/null
+++ b/tests/Fixture/TestTablesFixture.php
@@ -0,0 +1,46 @@
+
+ */
+ public array $fields = [
+ 'id' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => 'Primary key', 'autoIncrement' => true],
+ 'name' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => 'Name field', 'collate' => 'utf8mb4_general_ci'],
+ 'created' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => false, 'default' => null, 'comment' => ''],
+ 'modified' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => false, 'default' => null, 'comment' => ''],
+ '_constraints' => [
+ 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
+ ],
+ '_options' => [
+ 'engine' => 'InnoDB',
+ 'collation' => 'utf8mb4_general_ci'
+ ],
+ ];
+
+ /**
+ * Init method
+ *
+ * @return void
+ */
+ public function init(): void
+ {
+ $this->records = [
+ [
+ 'id' => 1,
+ 'name' => 'Test Record',
+ 'created' => '2024-01-01 00:00:00',
+ 'modified' => '2024-01-01 00:00:00'
+ ],
+ ];
+ parent::init();
+ }
+}
\ No newline at end of file
diff --git a/tests/TestCase/Command/OpenApiControllerCommandTest.php b/tests/TestCase/Command/OpenApiControllerCommandTest.php
index 026edcb..517794d 100644
--- a/tests/TestCase/Command/OpenApiControllerCommandTest.php
+++ b/tests/TestCase/Command/OpenApiControllerCommandTest.php
@@ -3,9 +3,18 @@
namespace OpenApiTheme\Test\TestCase\Command;
-use Cake\TestSuite\ConsoleIntegrationTestTrait;
+use Cake\Console\TestSuite\ConsoleIntegrationTestTrait;
use Cake\TestSuite\TestCase;
-use OpenApiTheme\Command\OpenApiControllerCommand;
+use Cake\Core\Configure;
+use Cake\Core\Plugin;
+use OpenApi\Attributes\Get;
+use OpenApi\Attributes\Post;
+use OpenApi\Attributes\Put;
+use OpenApi\Attributes\Delete;
+use OpenApi\Attributes\Tag;
+use OpenApi\Attributes\Response;
+use ReflectionClass;
+use ReflectionMethod;
/**
* OpenApiTheme\Command\OpenApiControllerCommand Test Case
@@ -16,6 +25,11 @@ class OpenApiControllerCommandTest extends TestCase
{
use ConsoleIntegrationTestTrait;
+ /**
+ * @var array
+ */
+ private $loadedClasses = [];
+
/**
* setUp method
*
@@ -24,25 +38,272 @@ class OpenApiControllerCommandTest extends TestCase
public function setUp(): void
{
parent::setUp();
- $this->useCommandRunner();
+ $this->setAppNamespace('TestApp');
+
+ Configure::write('App.namespace', 'TestApp');
+ Plugin::getCollection()->add(new \OpenApiTheme\Plugin());
+
+ // TwigViewの設定
+ if (!defined('Cake\TwigView\View\CACHE')) {
+ define('Cake\TwigView\View\CACHE', TMP . 'twig' . DS);
+ }
+
+ $this->cleanupTestFiles();
+ }
+
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ Configure::delete('App.namespace');
+ Plugin::getCollection()->remove('OpenApiTheme');
+ $this->cleanupTestFiles();
+ }
+
+ /**
+ * テストファイルをクリーンアップ
+ */
+ private function cleanupTestFiles(): void
+ {
+ $testFiles = glob(TEST_APP . 'Controller' . DS . '*Controller.php');
+ $testAdminFiles = glob(TEST_APP . 'Controller' . DS . 'Admin' . DS . '*Controller.php');
+
+ foreach (array_merge($testFiles ?? [], $testAdminFiles ?? []) as $file) {
+ // AppController.phpは削除しない
+ if (basename($file) === 'AppController.php') {
+ continue;
+ }
+
+ if (file_exists($file)) {
+ unlink($file);
+ }
+ }
+
+ $adminDir = TEST_APP . 'Controller' . DS . 'Admin';
+ if (is_dir($adminDir) && count(glob($adminDir . '/*')) === 0) {
+ rmdir($adminDir);
+ }
+ }
+
+ /**
+ * クラスをアンロードする
+ *
+ * @param string $className
+ * @return void
+ */
+ private function removeClass(string $className): void
+ {
+ $classExists = class_exists($className, false);
+ if ($classExists) {
+ $class = new ReflectionClass($className);
+ $fileName = $class->getFileName();
+
+ if ($fileName && is_file($fileName)) {
+ opcache_invalidate($fileName, true);
+ opcache_reset();
+ }
+ }
+ }
+
+ /**
+ * クラスをロードする
+ *
+ * @param string $controllerPath
+ * @param string $className
+ * @return ReflectionClass
+ */
+ private function loadControllerClass(string $controllerPath, string $className): ReflectionClass
+ {
+ if (class_exists($className, false)) {
+ $this->removeClass($className);
+ }
+
+ require $controllerPath;
+ $this->loadedClasses[] = $className;
+
+ return new ReflectionClass($className);
+ }
+
+ /**
+ * Test basic controller baking
+ *
+ * @return void
+ */
+ public function testBasicBaking(): void
+ {
+ $this->exec('bake controller BasicArticles --connection default --theme OpenApiTheme --no-test');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('Baking controller class for BasicArticles');
+ $this->assertOutputContains('Wrote');
+
+ $controllerPath = TEST_APP . 'Controller' . DS . 'BasicArticlesController.php';
+ $this->assertFileExists($controllerPath);
+
+ $className = 'TestApp\\Controller\\BasicArticlesController';
+ $reflClass = $this->loadControllerClass($controllerPath, $className);
+
+ $indexMethod = $reflClass->getMethod('index');
+ $this->assertNotEmpty(
+ $indexMethod->getAttributes(Get::class),
+ 'indexメソッドにGetアトリビュートが存在しません'
+ );
+
+ $viewMethod = $reflClass->getMethod('view');
+ $this->assertNotEmpty(
+ $viewMethod->getAttributes(Get::class),
+ 'viewメソッドにGetアトリビュートが存在しません'
+ );
+
+ $addMethod = $reflClass->getMethod('add');
+ $this->assertNotEmpty(
+ $addMethod->getAttributes(Post::class),
+ 'addメソッドにPostアトリビュートが存在しません'
+ );
+
+ $editMethod = $reflClass->getMethod('edit');
+ $this->assertNotEmpty(
+ $editMethod->getAttributes(Put::class),
+ 'editメソッドにPutアトリビュートが存在しません'
+ );
+
+ $deleteMethod = $reflClass->getMethod('delete');
+ $this->assertNotEmpty(
+ $deleteMethod->getAttributes(Delete::class),
+ 'deleteメソッドにDeleteアトリビュートが存在しません'
+ );
}
+
/**
- * Test buildOptionParser method
+ * Test baking with prefix
*
* @return void
*/
- public function testBuildOptionParser(): void
+ public function testBakeWithPrefix(): void
{
- $this->markTestIncomplete('Not implemented yet.');
+ $this->exec('bake controller PrefixArticles --prefix Admin --connection default --theme OpenApiTheme --no-test');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('Baking controller class for PrefixArticles');
+ $this->assertOutputContains('Wrote');
+
+ $controllerPath = TEST_APP . 'Controller' . DS . 'Admin' . DS . 'PrefixArticlesController.php';
+ $this->assertFileExists($controllerPath);
+
+ $className = 'TestApp\\Controller\\Admin\\PrefixArticlesController';
+ $reflClass = $this->loadControllerClass($controllerPath, $className);
+
+ $indexMethod = $reflClass->getMethod('index');
+ $this->assertNotEmpty(
+ $indexMethod->getAttributes(Get::class),
+ 'Admin/indexメソッドにGetアトリビュートが存在しません'
+ );
+
+ $viewMethod = $reflClass->getMethod('view');
+ $this->assertNotEmpty(
+ $viewMethod->getAttributes(Get::class),
+ 'Admin/viewメソッドにGetアトリビュートが存在しません'
+ );
+
+ $addMethod = $reflClass->getMethod('add');
+ $this->assertNotEmpty(
+ $addMethod->getAttributes(Post::class),
+ 'Admin/addメソッドにPostアトリビュートが存在しません'
+ );
+
+ $editMethod = $reflClass->getMethod('edit');
+ $this->assertNotEmpty(
+ $editMethod->getAttributes(Put::class),
+ 'Admin/editメソッドにPutアトリビュートが存在しません'
+ );
+
+ $deleteMethod = $reflClass->getMethod('delete');
+ $this->assertNotEmpty(
+ $deleteMethod->getAttributes(Delete::class),
+ 'Admin/deleteメソッドにDeleteアトリビュートが存在しません'
+ );
+ }
+
+ /**
+ * Test baking with actions
+ *
+ * @return void
+ */
+ public function testBakeWithActions(): void
+ {
+ $this->exec('bake controller ActionArticles --actions index,view,add --connection default --theme OpenApiTheme --no-test');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('Baking controller class for ActionArticles');
+ $this->assertOutputContains('Wrote');
+
+ $controllerPath = TEST_APP . 'Controller' . DS . 'ActionArticlesController.php';
+ $this->assertFileExists($controllerPath);
+
+ $className = 'TestApp\\Controller\\ActionArticlesController';
+ $reflClass = $this->loadControllerClass($controllerPath, $className);
+
+ $indexMethod = $reflClass->getMethod('index');
+ $this->assertNotEmpty(
+ $indexMethod->getAttributes(Get::class),
+ 'indexメソッドにGetアトリビュートが存在しません'
+ );
+
+ $viewMethod = $reflClass->getMethod('view');
+ $this->assertNotEmpty(
+ $viewMethod->getAttributes(Get::class),
+ 'viewメソッドにGetアトリビュートが存在しません'
+ );
+
+ $addMethod = $reflClass->getMethod('add');
+ $this->assertNotEmpty(
+ $addMethod->getAttributes(Post::class),
+ 'addメソッドにPostアトリビュートが存在しません'
+ );
+
+ $this->assertFalse(
+ method_exists($className, 'edit'),
+ 'editメソッドが存在してはいけません'
+ );
+ $this->assertFalse(
+ method_exists($className, 'delete'),
+ 'deleteメソッドが存在してはいけません'
+ );
}
/**
- * Test execute method
+ * Test baking with no actions
*
* @return void
*/
- public function testExecute(): void
+ public function testBakeWithNoActions(): void
{
- $this->markTestIncomplete('Not implemented yet.');
+ $this->exec('bake controller NoActionArticles --no-actions --connection default --theme OpenApiTheme --no-test');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('Baking controller class for NoActionArticles');
+ $this->assertOutputContains('Wrote');
+
+ $controllerPath = TEST_APP . 'Controller' . DS . 'NoActionArticlesController.php';
+ $this->assertFileExists($controllerPath);
+
+ $className = 'TestApp\\Controller\\NoActionArticlesController';
+ $reflClass = $this->loadControllerClass($controllerPath, $className);
+
+ $this->assertFalse(
+ method_exists($className, 'index'),
+ 'indexメソッドが存在してはいけません'
+ );
+ $this->assertFalse(
+ method_exists($className, 'view'),
+ 'viewメソッドが存在してはいけません'
+ );
+ $this->assertFalse(
+ method_exists($className, 'add'),
+ 'addメソッドが存在してはいけません'
+ );
+ $this->assertFalse(
+ method_exists($className, 'edit'),
+ 'editメソッドが存在してはいけません'
+ );
+ $this->assertFalse(
+ method_exists($className, 'delete'),
+ 'deleteメソッドが存在してはいけません'
+ );
}
}
diff --git a/tests/TestCase/Command/OpenApiModelCommandTest.php b/tests/TestCase/Command/OpenApiModelCommandTest.php
new file mode 100644
index 0000000..bd09fe0
--- /dev/null
+++ b/tests/TestCase/Command/OpenApiModelCommandTest.php
@@ -0,0 +1,161 @@
+command = new OpenApiModelCommand();
+ }
+
+ /**
+ * tearDown method
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ unset($this->command);
+ parent::tearDown();
+ }
+
+ /**
+ * Test getEntityPropertySchema method
+ *
+ * @return void
+ */
+ public function testGetEntityPropertySchema(): void
+ {
+ /** @var Table&MockObject $table */
+ $table = $this->getMockBuilder(Table::class)
+ ->onlyMethods(['getSchema', 'associations'])
+ ->getMock();
+
+ $schema = new TableSchema('test_table', [
+ 'id' => ['type' => 'integer', 'null' => false, 'comment' => 'Primary key'],
+ 'name' => ['type' => 'string', 'null' => false, 'comment' => 'Name field'],
+ ]);
+
+ $associationCollection = new AssociationCollection();
+
+ $table->expects($this->once())
+ ->method('getSchema')
+ ->willReturn($schema);
+
+ $table->expects($this->once())
+ ->method('associations')
+ ->willReturn($associationCollection);
+
+ $result = $this->command->getEntityPropertySchema($table);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('id', $result);
+ $this->assertArrayHasKey('name', $result);
+
+ $this->assertEquals('column', $result['id']['kind']);
+ $this->assertEquals('integer', $result['id']['type']);
+ $this->assertEquals(false, $result['id']['null']);
+ $this->assertEquals('Primary key', $result['id']['comment']);
+
+ $this->assertEquals('column', $result['name']['kind']);
+ $this->assertEquals('string', $result['name']['type']);
+ $this->assertEquals(false, $result['name']['null']);
+ $this->assertEquals('Name field', $result['name']['comment']);
+ }
+
+ /**
+ * Test getEntityPropertySchema method with associations
+ *
+ * @return void
+ */
+ public function testGetEntityPropertySchemaWithAssociations(): void
+ {
+ /** @var Table&MockObject $table */
+ $table = $this->getMockBuilder(Table::class)
+ ->onlyMethods(['getSchema', 'associations'])
+ ->getMock();
+
+ $schema = new TableSchema('test_table', [
+ 'id' => ['type' => 'integer', 'null' => false],
+ ]);
+
+ /** @var BelongsTo&MockObject $association */
+ $association = $this->getMockBuilder(BelongsTo::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getProperty', 'getTarget'])
+ ->getMock();
+
+ /** @var Table&MockObject $targetTable */
+ $targetTable = $this->getMockBuilder(Table::class)
+ ->onlyMethods(['getEntityClass', 'getRegistryAlias', 'getAlias'])
+ ->getMock();
+
+ $association->expects($this->any())
+ ->method('getProperty')
+ ->willReturn('associated');
+
+ $association->expects($this->any())
+ ->method('getTarget')
+ ->willReturn($targetTable);
+
+ $targetTable->expects($this->any())
+ ->method('getEntityClass')
+ ->willReturn('App\Model\Entity\Associated');
+
+ $targetTable->expects($this->any())
+ ->method('getRegistryAlias')
+ ->willReturn('Associated');
+
+ $targetTable->expects($this->any())
+ ->method('getAlias')
+ ->willReturn('Associated');
+
+ $associationCollection = new AssociationCollection();
+ $associationCollection->add('Associated', $association);
+
+ $table->expects($this->once())
+ ->method('getSchema')
+ ->willReturn($schema);
+
+ $table->expects($this->once())
+ ->method('associations')
+ ->willReturn($associationCollection);
+
+ $result = $this->command->getEntityPropertySchema($table);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('id', $result);
+ $this->assertArrayHasKey('associated', $result);
+
+ $this->assertEquals('column', $result['id']['kind']);
+ $this->assertEquals('integer', $result['id']['type']);
+
+ $this->assertEquals('association', $result['associated']['kind']);
+ $this->assertEquals('\App\Model\Entity\Associated', $result['associated']['type']);
+ }
+}
\ No newline at end of file
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 1575f86..5cbafc2 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -24,17 +24,91 @@
chdir($root);
+if (!defined('DS')) {
+ define('DS', DIRECTORY_SEPARATOR);
+}
+define('ROOT', $root);
+define('APP_DIR', 'TestApp');
+define('APP', ROOT . DS . 'tests' . DS . 'test_app' . DS . 'src' . DS);
+define('TMP', sys_get_temp_dir() . DS);
+define('CONFIG', ROOT . DS . 'tests' . DS . 'config' . DS);
+define('CAKE_CORE_INCLUDE_PATH', ROOT . DS . 'vendor' . DS . 'cakephp' . DS . 'cakephp');
+define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS);
+define('CAKE', CORE_PATH . 'src' . DS);
+define('TESTS', ROOT . DS . 'tests' . DS);
+define('TEST_APP', TESTS . 'test_app' . DS . 'src' . DS);
+define('WWW_ROOT', TEST_APP . 'webroot' . DS);
+
require_once $root . '/vendor/autoload.php';
+require_once CORE_PATH . 'config/bootstrap.php';
-/**
- * Define fallback values for required constants and configuration.
- * To customize constants and configuration remove this require
- * and define the data required by your plugin here.
- */
-require_once $root . '/vendor/cakephp/cakephp/tests/bootstrap.php';
+use Cake\Cache\Cache;
+use Cake\Core\Configure;
+use Cake\Database\Connection;
+use Cake\Database\Driver\Mysql;
+use Cake\Datasource\ConnectionManager;
+use Cake\Log\Log;
+use Cake\Utility\Security;
-if (file_exists($root . '/config/bootstrap.php')) {
- require $root . '/config/bootstrap.php';
+Configure::write('debug', true);
+Security::setSalt('dummy-salt-for-tests');
- return;
-}
+// Setup test database configuration
+ConnectionManager::setConfig('default', [
+ 'className' => Connection::class,
+ 'driver' => Mysql::class,
+ 'database' => 'test_openapi_theme',
+ 'username' => 'root',
+ 'password' => '',
+ 'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+ 'log' => false,
+]);
+
+ConnectionManager::setConfig('test', [
+ 'className' => Connection::class,
+ 'driver' => Mysql::class,
+ 'database' => 'test_openapi_theme_test',
+ 'username' => 'root',
+ 'password' => '',
+ 'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+ 'log' => false,
+]);
+
+Cache::setConfig([
+ '_cake_core_' => [
+ 'engine' => 'File',
+ 'prefix' => 'cake_core_',
+ 'serialize' => true,
+ 'path' => TMP,
+ ],
+ '_cake_model_' => [
+ 'engine' => 'File',
+ 'prefix' => 'cake_model_',
+ 'serialize' => true,
+ 'path' => TMP,
+ ],
+]);
+
+Log::setConfig([
+ 'debug' => [
+ 'engine' => 'Cake\Log\Engine\FileLog',
+ 'path' => TMP . 'logs/',
+ 'file' => 'debug',
+ 'levels' => ['notice', 'info', 'debug'],
+ ],
+ 'error' => [
+ 'engine' => 'Cake\Log\Engine\FileLog',
+ 'path' => TMP . 'logs/',
+ 'file' => 'error',
+ 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
+ ],
+]);
+
+// Load test specific plugin configuration
+Configure::load('app', 'default', false);
diff --git a/tests/config/app.php b/tests/config/app.php
new file mode 100644
index 0000000..bdec49e
--- /dev/null
+++ b/tests/config/app.php
@@ -0,0 +1,57 @@
+ [
+ 'namespace' => 'TestApp',
+ 'encoding' => 'UTF-8',
+ 'defaultLocale' => 'en_US',
+ 'defaultTimezone' => 'UTC',
+ 'base' => false,
+ 'dir' => 'src',
+ 'webroot' => 'webroot',
+ 'wwwRoot' => WWW_ROOT,
+ 'fullBaseUrl' => false,
+ 'imageBaseUrl' => 'img/',
+ 'cssBaseUrl' => 'css/',
+ 'jsBaseUrl' => 'js/',
+ 'paths' => [
+ 'plugins' => [dirname(dirname(__DIR__))],
+ 'templates' => [APP . 'Template' . DS],
+ 'locales' => [APP . 'Locale' . DS],
+ ],
+ ],
+ 'Security' => [
+ 'salt' => 'test-salt-for-testing',
+ ],
+ 'Datasources' => [
+ 'default' => [
+ 'className' => Connection::class,
+ 'driver' => Mysql::class,
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'username' => 'root',
+ 'password' => '',
+ 'database' => 'test_openapi_theme',
+ 'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ ],
+ 'test' => [
+ 'className' => Connection::class,
+ 'driver' => Mysql::class,
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'username' => 'root',
+ 'password' => '',
+ 'database' => 'test_openapi_theme_test',
+ 'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ ],
+ ],
+ 'debug' => true,
+];
\ No newline at end of file
diff --git a/tests/config/bootstrap.php b/tests/config/bootstrap.php
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/tests/config/bootstrap.php
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/test_app/config/app.php b/tests/test_app/config/app.php
new file mode 100644
index 0000000..bf19bb6
--- /dev/null
+++ b/tests/test_app/config/app.php
@@ -0,0 +1,28 @@
+ [
+ 'namespace' => 'TestApp',
+ 'encoding' => 'UTF-8',
+ 'defaultLocale' => 'en_US',
+ 'defaultTimezone' => 'UTC',
+ 'base' => false,
+ 'dir' => 'src',
+ 'webroot' => 'webroot',
+ 'wwwRoot' => WWW_ROOT,
+ 'fullBaseUrl' => false,
+ 'imageBaseUrl' => 'img/',
+ 'cssBaseUrl' => 'css/',
+ 'jsBaseUrl' => 'js/',
+ 'paths' => [
+ 'plugins' => [dirname(dirname(dirname(__DIR__)))],
+ 'templates' => [APP . 'Template' . DS],
+ 'locales' => [APP . 'Locale' . DS],
+ ],
+ ],
+ 'Security' => [
+ 'salt' => 'test-salt-for-testing',
+ ],
+ 'debug' => true,
+];
\ No newline at end of file
diff --git a/tests/test_app/config/app_local.php b/tests/test_app/config/app_local.php
new file mode 100644
index 0000000..29c708e
--- /dev/null
+++ b/tests/test_app/config/app_local.php
@@ -0,0 +1,34 @@
+ [
+ 'default' => [
+ 'className' => Connection::class,
+ 'driver' => Mysql::class,
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'username' => 'root',
+ 'password' => '',
+ 'database' => 'test_openapi_theme',
+ 'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ ],
+ 'test' => [
+ 'className' => Connection::class,
+ 'driver' => Mysql::class,
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'username' => 'root',
+ 'password' => '',
+ 'database' => 'test_openapi_theme_test',
+ 'encoding' => 'utf8mb4',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ ],
+ ],
+];
\ No newline at end of file
diff --git a/tests/test_app/config/bootstrap.php b/tests/test_app/config/bootstrap.php
new file mode 100644
index 0000000..15745c9
--- /dev/null
+++ b/tests/test_app/config/bootstrap.php
@@ -0,0 +1,6 @@
+addPlugin('Bake');
+ $this->addPlugin('OpenApiTheme');
+ }
+
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ return $middlewareQueue;
+ }
+
+ public function routes(RouteBuilder $routes): void
+ {
+ $routes->scope('/', function (RouteBuilder $builder): void {
+ $builder->fallbacks();
+ });
+ parent::routes($routes);
+ }
+}
\ No newline at end of file
diff --git a/tests/test_app/src/Controller/AppController.php b/tests/test_app/src/Controller/AppController.php
new file mode 100644
index 0000000..bb8d7d7
--- /dev/null
+++ b/tests/test_app/src/Controller/AppController.php
@@ -0,0 +1,10 @@
+