diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index a0e7173..602e7ee 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -61,6 +61,7 @@ jobs:
POSTGRES_PASSWORD: 'bedita'
POSTGRES_DB: 'bedita'
ports:
+ - '3306:3306'
- '5432:5432'
options: '${{ fromJson(matrix.db).options }}'
diff --git a/config/app.php b/config/app.php
index b04a7c5..2871840 100644
--- a/config/app.php
+++ b/config/app.php
@@ -213,7 +213,10 @@
'skipLog' => ['Cake\Network\Exception\NotFoundException', 'BEdita\API\Exception\ExpiredTokenException'],
'log' => true,
'trace' => true,
- 'ignoredDeprecationPaths' => ['vendor/cakephp/cakephp/src/Log/Engine/FileLog.php'],
+ 'ignoredDeprecationPaths' => [
+ 'vendor/cakephp/cakephp/src/Log/Engine/FileLog.php',
+ 'vendor/cakephp/migrations/src/Db/Table/ForeignKey.php',
+ ],
],
/*
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index cb0d17d..b1bc3e0 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -7,3 +7,4 @@ parameters:
- tests
excludePaths:
- src/Console/Installer.php
+ - tests/Fixture
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index affe678..9b6c640 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -7,25 +7,26 @@
bootstrap="./tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.1/phpunit.xsd"
+ displayDetailsOnTestsThatTriggerDeprecations="false"
+ displayDetailsOnTestsThatTriggerErrors="false"
+ displayDetailsOnTestsThatTriggerNotices="false"
+ displayDetailsOnTestsThatTriggerWarnings="false"
+ displayDetailsOnPhpunitDeprecations="false"
>
+
-
+
./tests/TestCase/
-
-
-
-
-
diff --git a/tests/Fixture/ApplicationsFixture.php b/tests/Fixture/ApplicationsFixture.php
new file mode 100644
index 0000000..4e499f3
--- /dev/null
+++ b/tests/Fixture/ApplicationsFixture.php
@@ -0,0 +1,26 @@
+ API_KEY,
+ 'client_secret' => null,
+ 'name' => 'my_webapp',
+ 'created' => '2026-01-29 07:10:57',
+ 'modified' => '2026-01-29 07:10:57',
+ 'enabled' => 1,
+ ],
+ ];
+}
diff --git a/tests/Fixture/ObjectTypesFixture.php b/tests/Fixture/ObjectTypesFixture.php
new file mode 100644
index 0000000..825a9d1
--- /dev/null
+++ b/tests/Fixture/ObjectTypesFixture.php
@@ -0,0 +1,73 @@
+ 'object',
+ 'name' => 'objects',
+ 'is_abstract' => true,
+ 'parent_id' => null,
+ 'tree_left' => 1,
+ 'tree_right' => 24,
+ 'description' => null,
+ 'plugin' => 'BEdita/Core',
+ 'model' => 'Objects',
+ 'created' => '2025-11-10 09:27:23',
+ 'modified' => '2025-11-10 09:27:23',
+ 'enabled' => true,
+ 'core_type' => true,
+ 'translation_rules' => null,
+ 'is_translatable' => false,
+ ],
+ // 2
+ [
+ 'singular' => 'user',
+ 'name' => 'users',
+ 'is_abstract' => false,
+ 'parent_id' => 1,
+ 'tree_left' => 6,
+ 'tree_right' => 7,
+ 'description' => null,
+ 'plugin' => 'BEdita/Core',
+ 'model' => 'Users',
+ 'created' => '2025-11-10 09:27:23',
+ 'modified' => '2025-11-10 09:27:23',
+ 'enabled' => true,
+ 'core_type' => true,
+ 'translation_rules' => null,
+ 'is_translatable' => false,
+ ],
+ // 3
+ [
+ 'singular' => 'profile',
+ 'name' => 'profiles',
+ 'is_abstract' => false,
+ 'parent_id' => 1,
+ 'tree_left' => 4,
+ 'tree_right' => 5,
+ 'description' => null,
+ 'associations' => ['Tags'],
+ 'plugin' => 'BEdita/Core',
+ 'model' => 'Profiles',
+ 'created' => '2025-11-10 09:27:23',
+ 'modified' => '2025-11-10 09:27:23',
+ 'enabled' => true,
+ 'core_type' => true,
+ 'translation_rules' => null,
+ 'is_translatable' => false,
+ ],
+ ];
+}
diff --git a/tests/Fixture/ObjectsFixture.php b/tests/Fixture/ObjectsFixture.php
new file mode 100644
index 0000000..4e35492
--- /dev/null
+++ b/tests/Fixture/ObjectsFixture.php
@@ -0,0 +1,76 @@
+ 2,
+ 'status' => 'on',
+ 'uname' => 'gustavo-admin',
+ 'locked' => 1,
+ 'deleted' => 0,
+ 'created' => '2026-01-29 07:09:23',
+ 'modified' => '2026-01-29 07:09:23',
+ 'title' => 'Gustavo Admin',
+ 'lang' => 'it',
+ 'created_by' => 1,
+ 'modified_by' => 1,
+ ],
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function init(): void
+ {
+ parent::init();
+
+ // remove `objects_createdby_fk` and `objects_modifiedby_fk` constraints
+ // to avoid PostgreSQL error inserting first user that references itself.
+ // CakePHP inserting fixture disables constraints but
+ // when the constraints are enabled again PostgreSQL give an SQL error.
+ $connection = ConnectionManager::get($this->connection());
+ if (!$connection->getDriver() instanceof Postgres) {
+ return;
+ }
+
+ $constraints = $this->_schema->constraints();
+ $removeConstraints = ['objects_createdby_fk', 'objects_modifiedby_fk'];
+ if (empty(array_intersect($constraints, $removeConstraints))) {
+ return;
+ }
+
+ $restoreConstraints = [];
+ foreach ($this->_schema->constraints() as $name) {
+ if (in_array($name, $removeConstraints)) {
+ continue;
+ }
+
+ $restoreConstraints[$name] = $this->_schema->getConstraint($name);
+ $this->_schema->dropConstraint($name);
+ }
+
+ $dropConstraintSql = $this->_schema->dropConstraintSql($connection);
+ foreach ($dropConstraintSql as $sql) {
+ $connection->execute($sql);
+ }
+
+ foreach ($restoreConstraints as $name => $attrs) {
+ $this->_schema->addConstraint($name, $attrs);
+ }
+ }
+}
diff --git a/tests/Fixture/ProfilesFixture.php b/tests/Fixture/ProfilesFixture.php
new file mode 100644
index 0000000..c896158
--- /dev/null
+++ b/tests/Fixture/ProfilesFixture.php
@@ -0,0 +1,24 @@
+ 1,
+ 'name' => 'Gustavo',
+ 'surname' => 'Admin',
+ 'email' => 'gustavo-admin@example.com',
+ ],
+ ];
+}
diff --git a/tests/Fixture/PropertiesFixture.php b/tests/Fixture/PropertiesFixture.php
new file mode 100644
index 0000000..ff7a47b
--- /dev/null
+++ b/tests/Fixture/PropertiesFixture.php
@@ -0,0 +1,18 @@
+records = [
+ // 1
+ [
+ 'name' => 'string',
+ 'params' => ['type' => 'string'],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 2
+ [
+ 'name' => 'text',
+ 'params' => [
+ 'type' => 'string',
+ 'contentMediaType' => 'text/html',
+ ],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 3
+ [
+ 'name' => 'status',
+ 'params' => [
+ 'type' => 'string',
+ 'enum' => ['on', 'off', 'draft'],
+ ],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 4
+ [
+ 'name' => 'email',
+ 'params' => [
+ 'type' => 'string',
+ 'format' => 'email',
+ ],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 5
+ [
+ 'name' => 'url',
+ 'params' => [
+ 'type' => 'string',
+ 'format' => 'uri',
+ ],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 6
+ [
+ 'name' => 'date',
+ 'params' => [
+ 'type' => 'string',
+ 'format' => 'date',
+ ],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 7
+ [
+ 'name' => 'datetime',
+ 'params' => [
+ 'type' => 'string',
+ 'format' => 'date-time',
+ ],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 8
+ [
+ 'name' => 'number',
+ 'params' => ['type' => 'number'],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 9
+ [
+ 'name' => 'integer',
+ 'params' => ['type' => 'integer'],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 10
+ [
+ 'name' => 'boolean',
+ 'params' => ['type' => 'boolean'],
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 11
+ [
+ 'name' => 'json',
+ 'params' => new stdClass(),
+ 'created' => '2025-11-01 09:23:43',
+ 'modified' => '2025-11-01 09:23:43',
+ 'core_type' => true,
+ ],
+ // 12
+ [
+ 'name' => 'unused property type',
+ 'params' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'gustavo' => ['const' => 'supporto'],
+ ],
+ 'required' => ['gustavo'],
+ ],
+ 'created' => '2025-11-02 09:23:43',
+ 'modified' => '2025-11-02 09:23:43',
+ 'core_type' => false,
+ ],
+ // 13
+ [
+ 'name' => 'children_order',
+ 'params' => [
+ 'type' => 'string',
+ 'enum' => [
+ 'position',
+ '-position',
+ 'title',
+ '-title',
+ 'created',
+ '-created',
+ 'modified',
+ '-modified',
+ 'publish_start',
+ '-publish_start',
+ ],
+ ],
+ 'created' => '2022-12-01 15:35:21',
+ 'modified' => '2022-12-01 15:35:21',
+ 'core_type' => true,
+ ],
+ ];
+
+ parent::init();
+ }
+}
diff --git a/tests/Fixture/RolesFixture.php b/tests/Fixture/RolesFixture.php
new file mode 100644
index 0000000..999964a
--- /dev/null
+++ b/tests/Fixture/RolesFixture.php
@@ -0,0 +1,26 @@
+ 'admin',
+ 'unchangeable' => 1,
+ 'created' => '2025-12-29 11:36:00',
+ 'modified' => '2025-12-29 11:36:00',
+ 'priority' => 0,
+ ],
+ ];
+}
diff --git a/tests/Fixture/RolesUsersFixture.php b/tests/Fixture/RolesUsersFixture.php
new file mode 100644
index 0000000..773ba41
--- /dev/null
+++ b/tests/Fixture/RolesUsersFixture.php
@@ -0,0 +1,22 @@
+ 1,
+ 'user_id' => 1,
+ ],
+ ];
+}
diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php
new file mode 100644
index 0000000..cac6cb3
--- /dev/null
+++ b/tests/Fixture/UsersFixture.php
@@ -0,0 +1,35 @@
+records = [
+ [
+ 'id' => 1,
+ 'username' => 'gustavo-admin',
+ 'password_hash' => (new LegacyPasswordHasher(['hashType' => 'md5']))->hash('supporto'),
+ 'blocked' => 0,
+ 'last_login' => null,
+ 'last_login_err' => null,
+ 'num_login_err' => 0,
+ 'verified' => '2026-01-29 11:36:00',
+ 'password_modified' => '2026-01-29 11:36:00',
+ ],
+ ];
+
+ parent::init();
+ }
+}
diff --git a/tests/IntegrationTestCase.php b/tests/IntegrationTestCase.php
new file mode 100644
index 0000000..ddd4014
--- /dev/null
+++ b/tests/IntegrationTestCase.php
@@ -0,0 +1,47 @@
+
+ */
+ protected array $fixtures = [];
+
+ /**
+ * The required fixtures for authentication.
+ * They are added to fixtures present in test case class
+ *
+ * @var array
+ */
+ protected array $authFixtures = [
+ 'app.Applications',
+ 'app.ObjectTypes',
+ 'app.Objects',
+ 'app.Profiles',
+ 'app.Users',
+ 'app.Roles',
+ 'app.RolesUsers',
+ 'app.PropertyTypes',
+ 'app.Properties',
+ ];
+
+ /**
+ * Default user used for authentication
+ *
+ * @var array
+ */
+ protected array $defaultUser = [
+ 'username' => 'gustavo-admin',
+ 'password' => 'supporto',
+ ];
+}
diff --git a/tests/TestCase/UsersTest.php b/tests/TestCase/UsersTest.php
new file mode 100644
index 0000000..5d40e0d
--- /dev/null
+++ b/tests/TestCase/UsersTest.php
@@ -0,0 +1,38 @@
+configRequestHeaders('GET', $this->getUserAuthHeader(username: 'gustavo-admin', password: 'supporto'));
+ $this->get('/users/1');
+ $this->assertResponseOk();
+ }
+}