mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/feat-memory-adapter-tests' into feat-memory-adapter-tests
This commit is contained in:
@@ -48,6 +48,8 @@ _APP_DB_USER=user
|
||||
_APP_DB_PASS=password
|
||||
_APP_DB_ROOT_PASS=rootsecretpassword
|
||||
_APP_DB_SQLITE_PATH=/storage/sqlite/appwrite.db
|
||||
_APP_DATABASE_SHARED_TABLES=
|
||||
_APP_DATABASE_SHARED_NAMESPACE=
|
||||
_APP_DB_ADAPTER_DOCUMENTSDB=mongodb
|
||||
_APP_DB_HOST_DOCUMENTSDB=mongodb
|
||||
_APP_DB_PORT_DOCUMENTSDB=27017
|
||||
|
||||
+20
@@ -157,12 +157,19 @@ $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform,
|
||||
}
|
||||
|
||||
if (isset($databases[$dsn->getHost()])) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database = $databases[$dsn->getHost()];
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -182,9 +189,16 @@ $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform,
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -212,6 +226,11 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
|
||||
return $database;
|
||||
}
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -220,6 +239,7 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setNamespace('logsV1')
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_TASK)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
"emails.invitation.thanks": "Gracias.,",
|
||||
"emails.invitation.buttonText": "Aceptar invitación a {{team}}",
|
||||
"emails.invitation.signature": "El equipo de {{project}}",
|
||||
"emails.sessionAlert.subject": "Alerta de seguridad: nueva sesión en tu cuenta de {{project}}",
|
||||
"emails.sessionAlert.preview": "Nuevo inicio de sesión detectado en {{project}} a las {{time}} UTC.",
|
||||
"emails.sessionAlert.hello": "Hola {{user}},",
|
||||
"emails.sessionAlert.body": "Se ha creado una nueva sesión en tu cuenta de {{b}}{{project}}{{/b}}, {{b}}el {{date}} de {{year}} a las {{time}} UTC{{/b}}.\nEstos son los detalles de la nueva sesión:",
|
||||
"emails.sessionAlert.listDevice": "Dispositivo: {{b}}{{device}}{{/b}}",
|
||||
"emails.sessionAlert.listIpAddress": "Dirección IP: {{b}}{{ipAddress}}{{/b}}",
|
||||
"emails.sessionAlert.listCountry": "País: {{b}}{{country}}{{/b}}",
|
||||
"emails.sessionAlert.footer": "Si has sido tú, no tienes que hacer nada más.\nSi no has iniciado esta sesión o sospechas actividad no autorizada, protege tu cuenta.",
|
||||
"emails.sessionAlert.thanks": "Gracias,",
|
||||
"emails.sessionAlert.signature": "El equipo de {{project}}",
|
||||
"locale.country.unknown": "Desconocido",
|
||||
"countries.af": "Afganistán",
|
||||
"countries.ao": "Angola",
|
||||
|
||||
@@ -159,10 +159,16 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
|
||||
@@ -204,9 +204,16 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant($project->getSequence())
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
@@ -223,6 +230,11 @@ return function (Container $container): void {
|
||||
$adapter = null;
|
||||
|
||||
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$adapter ??= new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -230,6 +242,7 @@ return function (Container $container): void {
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
@@ -690,8 +703,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -1292,6 +1312,12 @@ return function (Container $container): void {
|
||||
$database = new Database($adapter, $cache);
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
@@ -1314,6 +1340,7 @@ return function (Container $container): void {
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($databaseDSN->getParam('namespace'));
|
||||
} else {
|
||||
@@ -1325,6 +1352,7 @@ return function (Container $container): void {
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,7 @@ use Appwrite\Utopia\Database\Documents\User;
|
||||
use Utopia\Audit\Adapter\Database as AdapterDatabase;
|
||||
use Utopia\Audit\Audit as UtopiaAudit;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Adapter\Pool as DatabasePool;
|
||||
use Utopia\Database\Database;
|
||||
@@ -90,8 +91,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -130,8 +138,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -152,8 +167,15 @@ return function (Container $container): void {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -210,6 +232,14 @@ return function (Container $container): void {
|
||||
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database->setGlobalCollections($projectsGlobalCollections);
|
||||
|
||||
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
|
||||
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
@@ -222,6 +252,7 @@ return function (Container $container): void {
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($databaseDSN->getParam('namespace'));
|
||||
} else {
|
||||
@@ -233,6 +264,7 @@ return function (Container $container): void {
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
@@ -257,6 +289,11 @@ return function (Container $container): void {
|
||||
return $database;
|
||||
}
|
||||
|
||||
/** @var array $collections */
|
||||
$collections = Config::getParam('collections', []);
|
||||
$logsCollections = $collections['logs'] ?? [];
|
||||
$logsCollections = array_keys($logsCollections);
|
||||
|
||||
$adapter = new DatabasePool($pools->get('logs'));
|
||||
$database = new Database($adapter, $cache);
|
||||
|
||||
@@ -264,6 +301,7 @@ return function (Container $container): void {
|
||||
->setDatabase(APP_DATABASE)
|
||||
->setAuthorization($authorization)
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($logsCollections)
|
||||
->setNamespace('logsV1')
|
||||
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
|
||||
|
||||
@@ -130,8 +130,14 @@ if (!function_exists('getProjectDB')) {
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$collections = Config::getParam('collections', []);
|
||||
$projectCollections = $collections['projects'] ?? [];
|
||||
$projectsGlobalCollections = array_keys($projectCollections);
|
||||
$projectsGlobalCollections[] = 'audit';
|
||||
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setGlobalCollections($projectsGlobalCollections)
|
||||
->setTenant($project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
|
||||
Generated
+29
-29
@@ -3351,16 +3351,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/abuse",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/abuse.git",
|
||||
"reference": "20bee84fd14dbe81d50ecabf1ffd81cceca06152"
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/20bee84fd14dbe81d50ecabf1ffd81cceca06152",
|
||||
"reference": "20bee84fd14dbe81d50ecabf1ffd81cceca06152",
|
||||
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"reference": "53f4274939353522ba331f55bcff6e6011ffc56c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3397,9 +3397,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/abuse/issues",
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.2"
|
||||
"source": "https://github.com/utopia-php/abuse/tree/1.2.3"
|
||||
},
|
||||
"time": "2026-02-02T10:43:10+00:00"
|
||||
"time": "2026-04-29T11:19:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
@@ -4094,16 +4094,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/domains.git",
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
|
||||
"reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
|
||||
"url": "https://api.github.com/repos/utopia-php/domains/zipball/c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
|
||||
"reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4150,9 +4150,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/domains/issues",
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.5"
|
||||
"source": "https://github.com/utopia-php/domains/tree/1.0.6"
|
||||
},
|
||||
"time": "2026-03-03T09:20:50+00:00"
|
||||
"time": "2026-04-29T11:08:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dsn",
|
||||
@@ -4562,16 +4562,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.9.4",
|
||||
"version": "1.9.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "969dc9477ea962f16da9254facdbd8944cf13477"
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/969dc9477ea962f16da9254facdbd8944cf13477",
|
||||
"reference": "969dc9477ea962f16da9254facdbd8944cf13477",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4611,9 +4611,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.4"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.5"
|
||||
},
|
||||
"time": "2026-04-27T12:42:51+00:00"
|
||||
"time": "2026-04-29T11:19:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
@@ -5052,16 +5052,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/storage",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/storage.git",
|
||||
"reference": "52d1f89a47165ef0d3deff63043cda182175adfb"
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/52d1f89a47165ef0d3deff63043cda182175adfb",
|
||||
"reference": "52d1f89a47165ef0d3deff63043cda182175adfb",
|
||||
"url": "https://api.github.com/repos/utopia-php/storage/zipball/8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"reference": "8a2e3a86fd01aaed675884146665308c2122264e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5098,9 +5098,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/storage/issues",
|
||||
"source": "https://github.com/utopia-php/storage/tree/2.0.0"
|
||||
"source": "https://github.com/utopia-php/storage/tree/2.0.1"
|
||||
},
|
||||
"time": "2026-04-27T11:39:32+00:00"
|
||||
"time": "2026-04-29T09:05:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/system",
|
||||
@@ -6253,11 +6253,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.52",
|
||||
"version": "2.1.54",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/08a34f8db7ca4daabff74a474fe13c0e56e2b4e5",
|
||||
"reference": "08a34f8db7ca4daabff74a474fe13c0e56e2b4e5",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
||||
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6302,7 +6302,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-28T12:17:53+00:00"
|
||||
"time": "2026-04-29T13:31:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -8506,5 +8506,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,13 @@ class Authentik extends OAuth2
|
||||
return 'authentik';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getAuthentikDomain())) {
|
||||
throw new \Exception('Authentik endpoint is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,13 @@ class FusionAuth extends OAuth2
|
||||
return 'fusionauth';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getFusionAuthDomain())) {
|
||||
throw new \Exception('FusionAuth endpoint is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,17 @@ class Keycloak extends OAuth2
|
||||
return 'keycloak';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getKeycloakDomain())) {
|
||||
throw new \Exception('Keycloak endpoint is required.');
|
||||
}
|
||||
|
||||
if (empty($this->getKeycloakRealm())) {
|
||||
throw new \Exception('Keycloak realm name is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -36,6 +36,13 @@ class Microsoft extends OAuth2
|
||||
return 'microsoft';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getTenantID())) {
|
||||
throw new \Exception('Microsoft tenant is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -201,7 +208,7 @@ class Microsoft extends OAuth2
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the Tenant Id from the JSON stored in appSecret. Defaults to 'common' as a fallback
|
||||
* Extracts the Tenant Id from the JSON stored in appSecret.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -209,6 +216,6 @@ class Microsoft extends OAuth2
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
|
||||
return $secret['tenantID'] ?? 'common';
|
||||
return $secret['tenantID'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ class XList extends Action
|
||||
$actions = OAuth2Base::getProviderActions();
|
||||
|
||||
$providers = [];
|
||||
foreach ($actions as $providerId => $updateClass) {
|
||||
$config = $providersConfig[$providerId] ?? null;
|
||||
if ($config === null) {
|
||||
foreach ($providersConfig as $providerId => $config) {
|
||||
$updateClass = $actions[$providerId] ?? null;
|
||||
if ($updateClass === null) {
|
||||
continue;
|
||||
}
|
||||
if (!($config['enabled'] ?? false)) {
|
||||
|
||||
@@ -146,13 +146,14 @@ class Update extends Base
|
||||
{
|
||||
$providerId = static::getProviderId();
|
||||
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
|
||||
$storedSecret = $this->decodeStoredSecret($project);
|
||||
|
||||
return new Document([
|
||||
'$id' => $providerId,
|
||||
'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false,
|
||||
static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '',
|
||||
'keyId' => '',
|
||||
'teamId' => '',
|
||||
'keyId' => $storedSecret['keyID'] ?? '',
|
||||
'teamId' => $storedSecret['teamID'] ?? '',
|
||||
'p8File' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class Update extends Base
|
||||
))
|
||||
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
|
||||
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
|
||||
->param('endpoint', '', new Text(256, 1), 'Domain of Authentik instance. For example: example.authentik.com', optional: false)
|
||||
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Authentik instance. For example: example.authentik.com', optional: true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
@@ -138,7 +138,7 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
string $endpoint,
|
||||
?string $endpoint,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -151,7 +151,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "authentikDomain": "..."}`
|
||||
// to match the shape Authentik's OAuth2 adapter expects (getAuthentikDomain()).
|
||||
// The `endpoint` param is required on every call, so it's always written.
|
||||
// The `endpoint` param is optional; if omitted, the existing stored endpoint is preserved.
|
||||
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -160,7 +160,7 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'authentikDomain' => $endpoint,
|
||||
'authentikDomain' => $endpoint ?? ($existing['authentikDomain'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -105,7 +105,7 @@ class Update extends Base
|
||||
))
|
||||
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
|
||||
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
|
||||
->param('endpoint', '', new Text(256, 1), 'Domain of FusionAuth instance. For example: example.fusionauth.io', optional: false)
|
||||
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of FusionAuth instance. For example: example.fusionauth.io', optional: true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
@@ -138,7 +138,7 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
string $endpoint,
|
||||
?string $endpoint,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -151,7 +151,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "fusionAuthDomain": "..."}`
|
||||
// to match the shape FusionAuth's OAuth2 adapter expects (getFusionAuthDomain()).
|
||||
// The `endpoint` param is required on every call, so it's always written.
|
||||
// The `endpoint` param is optional; if omitted, the existing stored endpoint is preserved.
|
||||
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -160,7 +160,7 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'fusionAuthDomain' => $endpoint,
|
||||
'fusionAuthDomain' => $endpoint ?? ($existing['fusionAuthDomain'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -35,7 +35,7 @@ class Update extends Base
|
||||
|
||||
public static function getClientIdName(): string
|
||||
{
|
||||
return 'OAuth 2 app Client ID, or App ID';
|
||||
return 'OAuth2 app Client ID, or App ID';
|
||||
}
|
||||
|
||||
public static function getClientIdExample(): string
|
||||
|
||||
@@ -111,8 +111,8 @@ class Update extends Base
|
||||
))
|
||||
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
|
||||
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
|
||||
->param('endpoint', '', new Text(256, 1), 'Domain of Keycloak instance. For example: keycloak.example.com', optional: false)
|
||||
->param('realmName', '', new Text(256, 1), 'Keycloak realm name. For example: appwrite-realm', optional: false)
|
||||
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Keycloak instance. For example: keycloak.example.com', optional: true)
|
||||
->param('realmName', null, new Nullable(new Text(256, 0)), 'Keycloak realm name. For example: appwrite-realm', optional: true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
@@ -147,8 +147,8 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
string $endpoint,
|
||||
string $realmName,
|
||||
?string $endpoint,
|
||||
?string $realmName,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -161,7 +161,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "keycloakDomain": "...", "keycloakRealm": "..."}`
|
||||
// to match the shape Keycloak's OAuth2 adapter expects (getKeycloakDomain(), getKeycloakRealm()).
|
||||
// The `endpoint` and `realmName` params are required on every call, so they're always written.
|
||||
// The `endpoint` and `realmName` params are optional; if omitted, existing stored values are preserved.
|
||||
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -170,8 +170,8 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'keycloakDomain' => $endpoint,
|
||||
'keycloakRealm' => $realmName,
|
||||
'keycloakDomain' => $endpoint ?? ($existing['keycloakDomain'] ?? ''),
|
||||
'keycloakRealm' => $realmName ?? ($existing['keycloakRealm'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -115,7 +115,7 @@ class Update extends Base
|
||||
))
|
||||
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
|
||||
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
|
||||
->param('tenant', '', new Text(256, 1), 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID. For example: common', optional: false)
|
||||
->param('tenant', null, new Nullable(new Text(256, 0)), 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID. For example: common', true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
@@ -148,7 +148,7 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $applicationId,
|
||||
?string $applicationSecret,
|
||||
string $tenant,
|
||||
?string $tenant,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -161,7 +161,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "tenantID": "..."}`
|
||||
// to match the shape Microsoft's OAuth2 adapter expects (getTenantID()).
|
||||
// The `tenant` param is required on every call, so it's always written.
|
||||
// The `tenant` param is optional; if omitted, the existing stored tenant is preserved.
|
||||
// `applicationSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -170,7 +170,7 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $applicationSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'tenantID' => $tenant,
|
||||
'tenantID' => $tenant ?? ($existing['tenantID'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -633,6 +633,89 @@ class Deletes extends Action
|
||||
$dsn = new DSN('mysql://' . $document->getAttribute('database', 'console'));
|
||||
}
|
||||
|
||||
// Delete Platforms
|
||||
try {
|
||||
$this->deleteByGroup('platforms', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete platforms: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
// Delete project and function rules
|
||||
try {
|
||||
$this->deleteByGroup('rules', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
|
||||
$this->deleteRule($dbForPlatform, $document, $certificates);
|
||||
});
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete rules: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
// Delete Keys
|
||||
try {
|
||||
$this->deleteByGroup('keys', [
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete keys: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
// Delete Webhooks
|
||||
try {
|
||||
$this->deleteByGroup('webhooks', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete webhooks: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
// Delete VCS Installations
|
||||
try {
|
||||
$this->deleteByGroup('installations', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete installations: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
// Delete VCS Repositories
|
||||
try {
|
||||
$this->deleteByGroup('repositories', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete repositories: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
// Delete VCS comments
|
||||
try {
|
||||
$this->deleteByGroup('vcsComments', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete VCS comments: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
// Delete Schedules
|
||||
try {
|
||||
$this->deleteByGroup('schedules', [
|
||||
Query::equal('projectId', [$projectId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete schedules: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Database $dbForProject
|
||||
*/
|
||||
@@ -685,75 +768,35 @@ class Deletes extends Action
|
||||
};
|
||||
|
||||
batch(array_map(
|
||||
fn ($databaseDoc) => fn () => $this->cleanDatabase(
|
||||
$databaseDoc,
|
||||
$executionActionPerDatabase,
|
||||
$projectTables,
|
||||
$projectCollectionIds
|
||||
),
|
||||
fn ($databaseDoc) => function () use ($databaseDoc, $executionActionPerDatabase, $projectTables, $projectCollectionIds) {
|
||||
try {
|
||||
$this->cleanDatabase(
|
||||
$databaseDoc,
|
||||
$executionActionPerDatabase,
|
||||
$projectTables,
|
||||
$projectCollectionIds
|
||||
);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete database ' . $databaseDoc->getAttribute('database') . ': ' . $th->getMessage());
|
||||
}
|
||||
},
|
||||
$databasesToClean
|
||||
));
|
||||
|
||||
// Delete Platforms
|
||||
$this->deleteByGroup('platforms', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
|
||||
// Delete project and function rules
|
||||
$this->deleteByGroup('rules', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
|
||||
$this->deleteRule($dbForPlatform, $document, $certificates);
|
||||
});
|
||||
|
||||
// Delete Keys
|
||||
$this->deleteByGroup('keys', [
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
|
||||
// Delete Webhooks
|
||||
$this->deleteByGroup('webhooks', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
|
||||
// Delete VCS Installations
|
||||
$this->deleteByGroup('installations', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
|
||||
// Delete VCS Repositories
|
||||
$this->deleteByGroup('repositories', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
|
||||
// Delete VCS comments
|
||||
$this->deleteByGroup('vcsComments', [
|
||||
Query::equal('projectInternalId', [$projectInternalId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
|
||||
// Delete Schedules
|
||||
$this->deleteByGroup('schedules', [
|
||||
Query::equal('projectId', [$projectId]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform);
|
||||
|
||||
// Delete metadata table
|
||||
if ($projectTables) {
|
||||
batch(array_map(
|
||||
fn ($databaseDoc) => fn () =>
|
||||
$executionActionPerDatabase(
|
||||
$databaseDoc,
|
||||
fn (Database $dbForDatabases) =>
|
||||
$dbForDatabases->deleteCollection(Database::METADATA)
|
||||
),
|
||||
fn ($databaseDoc) => function () use ($databaseDoc, $executionActionPerDatabase) {
|
||||
try {
|
||||
$executionActionPerDatabase(
|
||||
$databaseDoc,
|
||||
fn (Database $dbForDatabases) =>
|
||||
$dbForDatabases->deleteCollection(Database::METADATA)
|
||||
);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete metadata table for database ' . $databaseDoc->getAttribute('database') . ': ' . $th->getMessage());
|
||||
}
|
||||
},
|
||||
$databasesToClean
|
||||
));
|
||||
} else {
|
||||
@@ -764,19 +807,47 @@ class Deletes extends Action
|
||||
|
||||
$queries[] = Query::orderAsc();
|
||||
|
||||
$this->deleteByGroup(
|
||||
Database::METADATA,
|
||||
$queries,
|
||||
$dbForProject
|
||||
);
|
||||
try {
|
||||
$this->deleteByGroup(
|
||||
Database::METADATA,
|
||||
$queries,
|
||||
$dbForProject
|
||||
);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete metadata documents: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all storage directories
|
||||
$deviceForFiles->delete($deviceForFiles->getRoot(), true);
|
||||
$deviceForSites->delete($deviceForSites->getRoot(), true);
|
||||
$deviceForFunctions->delete($deviceForFunctions->getRoot(), true);
|
||||
$deviceForBuilds->delete($deviceForBuilds->getRoot(), true);
|
||||
$deviceForCache->delete($deviceForCache->getRoot(), true);
|
||||
try {
|
||||
$deviceForFiles->delete($deviceForFiles->getRoot(), true);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete files storage directory: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$deviceForSites->delete($deviceForSites->getRoot(), true);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete sites storage directory: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$deviceForFunctions->delete($deviceForFunctions->getRoot(), true);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete functions storage directory: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$deviceForBuilds->delete($deviceForBuilds->getRoot(), true);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete builds storage directory: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$deviceForCache->delete($deviceForCache->getRoot(), true);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Failed to delete cache storage directory: ' . $th->getMessage());
|
||||
}
|
||||
|
||||
} finally {
|
||||
$dbForProject->enableValidation();
|
||||
|
||||
@@ -56,6 +56,8 @@ class ConsoleConsoleClientTest extends Scope
|
||||
$this->assertEquals($response['body']['total'], \count($response['body']['oAuth2Providers']));
|
||||
|
||||
$providerIds = \array_column($response['body']['oAuth2Providers'], '$id');
|
||||
$this->assertEquals('amazon', $providerIds[0]);
|
||||
$this->assertEquals('zoom', $providerIds[\count($providerIds) - 1]);
|
||||
|
||||
// Well-known providers must be present
|
||||
$this->assertContains('github', $providerIds);
|
||||
@@ -99,7 +101,7 @@ class ConsoleConsoleClientTest extends Scope
|
||||
$this->assertCount(2, $github['parameters']);
|
||||
$clientId = $github['parameters'][0];
|
||||
$this->assertEquals('clientId', $clientId['$id']);
|
||||
$this->assertEquals('OAuth 2 app Client ID, or App ID', $clientId['name']);
|
||||
$this->assertEquals('OAuth2 app Client ID, or App ID', $clientId['name']);
|
||||
$this->assertEquals('e4d87900000000540733', $clientId['example']);
|
||||
$this->assertEquals('Example of wrong value: 370006', $clientId['hint']);
|
||||
$clientSecret = $github['parameters'][1];
|
||||
|
||||
@@ -478,9 +478,8 @@ trait OAuth2Base
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('apple', $response['body']['$id']);
|
||||
$this->assertSame('ip.appwrite.app.web', $response['body']['serviceId']);
|
||||
// keyId / teamId / p8File are write-only — PATCH response must not echo them back.
|
||||
$this->assertSame('', $response['body']['keyId']);
|
||||
$this->assertSame('', $response['body']['teamId']);
|
||||
$this->assertSame('P4000000N8', $response['body']['keyId']);
|
||||
$this->assertSame('D4000000R6', $response['body']['teamId']);
|
||||
$this->assertSame('', $response['body']['p8File']);
|
||||
$this->assertSame(false, $response['body']['enabled']);
|
||||
|
||||
@@ -511,12 +510,10 @@ trait OAuth2Base
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
// serviceId is the (non-secret) clientId; keyId/teamId are write-only
|
||||
// and must not surface in the response. Persistence of the merged
|
||||
// values is verified separately via the enable-after-merge tests.
|
||||
$this->assertSame('ip.appwrite.app.seed', $response['body']['serviceId']);
|
||||
$this->assertSame('', $response['body']['keyId']);
|
||||
$this->assertSame('', $response['body']['teamId']);
|
||||
$this->assertSame('KEYUPDATED', $response['body']['keyId']);
|
||||
$this->assertSame('TEAMSEED01', $response['body']['teamId']);
|
||||
$this->assertSame('', $response['body']['p8File']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('apple', [
|
||||
@@ -546,9 +543,9 @@ trait OAuth2Base
|
||||
'teamId' => 'TEAMROTATED',
|
||||
]);
|
||||
$this->assertSame(200, $teamOnly['headers']['status-code']);
|
||||
// teamId is write-only; verify only the non-secret serviceId echo.
|
||||
// The actual merge is validated by the enable-after-merge call below.
|
||||
$this->assertSame('', $teamOnly['body']['teamId']);
|
||||
$this->assertSame('TEAMROTATED', $teamOnly['body']['teamId']);
|
||||
$this->assertSame('KEYMERGE01', $teamOnly['body']['keyId']);
|
||||
$this->assertSame('', $teamOnly['body']['p8File']);
|
||||
$this->assertSame('ip.appwrite.app.merge', $teamOnly['body']['serviceId']);
|
||||
|
||||
// Patch only `serviceId` — keyId/teamId/p8File live in the JSON blob
|
||||
@@ -669,9 +666,8 @@ trait OAuth2Base
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('ip.appwrite.app.read', $response['body']['serviceId']);
|
||||
// All three secret-bearing fields must be hidden on read.
|
||||
$this->assertSame('', $response['body']['keyId']);
|
||||
$this->assertSame('', $response['body']['teamId']);
|
||||
$this->assertSame('KEYREAD', $response['body']['keyId']);
|
||||
$this->assertSame('TEAMREAD', $response['body']['teamId']);
|
||||
$this->assertSame('', $response['body']['p8File']);
|
||||
|
||||
// Cleanup
|
||||
@@ -699,13 +695,13 @@ trait OAuth2Base
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
$this->assertTrue($update['body']['enabled']);
|
||||
|
||||
// GET must hide all three secret-bearing fields while keeping serviceId.
|
||||
// GET must hide p8File while keeping the non-secret fields.
|
||||
$get = $this->getOAuth2Provider('apple');
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertTrue($get['body']['enabled']);
|
||||
$this->assertSame('ip.appwrite.app.enable', $get['body']['serviceId']);
|
||||
$this->assertSame('', $get['body']['keyId']);
|
||||
$this->assertSame('', $get['body']['teamId']);
|
||||
$this->assertSame('ENABLEKEY', $get['body']['keyId']);
|
||||
$this->assertSame('ENABLETEAM', $get['body']['teamId']);
|
||||
$this->assertSame('', $get['body']['p8File']);
|
||||
|
||||
// Cleanup
|
||||
@@ -876,30 +872,36 @@ trait OAuth2Base
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Authentik (clientId + clientSecret + REQUIRED endpoint)
|
||||
// Update Authentik (clientId + clientSecret + optional endpoint)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2AuthentikRequiresEndpoint(): void
|
||||
public function testUpdateOAuth2AuthentikAllowsOmittedEndpointWhenDisabled(): void
|
||||
{
|
||||
// The `endpoint` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('authentik', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('authentik', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2AuthentikEmptyEndpointRejected(): void
|
||||
public function testUpdateOAuth2AuthentikEmptyEndpointRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `endpoint` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting — the validator should treat the
|
||||
// empty-string degenerate case as a missing required field.
|
||||
$response = $this->updateOAuth2('authentik', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -924,15 +926,14 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.authentik.com',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2AuthentikPartialPreservesSecret(): void
|
||||
{
|
||||
// Authentik's `endpoint` is required on every call, so we always
|
||||
// re-send it. The `clientSecret` lives in the JSON blob and must
|
||||
// The `clientSecret` and `endpoint` live in the JSON blob and must
|
||||
// survive when omitted on a subsequent call that only changes clientId.
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => 'authentik-merge-client',
|
||||
@@ -943,27 +944,24 @@ trait OAuth2Base
|
||||
|
||||
$response = $this->updateOAuth2('authentik', [
|
||||
'clientId' => 'authentik-rotated-client',
|
||||
'endpoint' => 'merge.authentik.com',
|
||||
]);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('authentik-rotated-client', $response['body']['clientId']);
|
||||
$this->assertSame('merge.authentik.com', $response['body']['endpoint']);
|
||||
|
||||
// Confirm clientSecret survived the omitted-field merge by enabling
|
||||
// — Authentik has no verifyCredentials() hook, so non-empty stored
|
||||
// secret is enough. `endpoint` must be re-sent (required on enable too).
|
||||
// without re-sending endpoint.
|
||||
$enable = $this->updateOAuth2('authentik', [
|
||||
'endpoint' => 'merge.authentik.com',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Cleanup — endpoint is required, use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.authentik.com',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -988,40 +986,46 @@ trait OAuth2Base
|
||||
$this->assertSame('enable.authentik.com', $get['body']['endpoint']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup — endpoint is required (Text(min=1)) so use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.authentik.com',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update FusionAuth (clientId + clientSecret + REQUIRED endpoint)
|
||||
// Update FusionAuth (clientId + clientSecret + optional endpoint)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2FusionAuthRequiresEndpoint(): void
|
||||
public function testUpdateOAuth2FusionAuthAllowsOmittedEndpointWhenDisabled(): void
|
||||
{
|
||||
// The `endpoint` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('fusionauth', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2FusionAuthEmptyEndpointRejected(): void
|
||||
public function testUpdateOAuth2FusionAuthEmptyEndpointRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `endpoint` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting — the validator should treat the
|
||||
// empty-string degenerate case as a missing required field.
|
||||
$response = $this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -1046,15 +1050,14 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.fusionauth.io',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2FusionAuthPartialPreservesSecret(): void
|
||||
{
|
||||
// FusionAuth's `endpoint` is required on every call, so we always
|
||||
// re-send it. The `clientSecret` lives in the JSON blob and must
|
||||
// The `clientSecret` and `endpoint` live in the JSON blob and must
|
||||
// survive when omitted on a subsequent call that only changes clientId.
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'fusionauth-merge-client',
|
||||
@@ -1065,27 +1068,24 @@ trait OAuth2Base
|
||||
|
||||
$response = $this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'fusionauth-rotated-client',
|
||||
'endpoint' => 'merge.fusionauth.io',
|
||||
]);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('fusionauth-rotated-client', $response['body']['clientId']);
|
||||
$this->assertSame('merge.fusionauth.io', $response['body']['endpoint']);
|
||||
|
||||
// Confirm clientSecret survived the omitted-field merge by enabling
|
||||
// — FusionAuth has no verifyCredentials() hook, so non-empty stored
|
||||
// secret is enough. `endpoint` must be re-sent (required on enable too).
|
||||
// without re-sending endpoint.
|
||||
$enable = $this->updateOAuth2('fusionauth', [
|
||||
'endpoint' => 'merge.fusionauth.io',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Cleanup — endpoint is required, use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.fusionauth.io',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1110,70 +1110,85 @@ trait OAuth2Base
|
||||
$this->assertSame('enable.fusionauth.io', $get['body']['endpoint']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup — endpoint is required (Text(min=1)) so use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.fusionauth.io',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Keycloak (clientId + clientSecret + REQUIRED endpoint + REQUIRED realmName)
|
||||
// Update Keycloak (clientId + clientSecret + optional endpoint + optional realmName)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2KeycloakRequiresEndpoint(): void
|
||||
public function testUpdateOAuth2KeycloakAllowsOmittedEndpointWhenDisabled(): void
|
||||
{
|
||||
// The `endpoint` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'realmName' => 'appwrite-realm',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('keycloak', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakEmptyEndpointRejected(): void
|
||||
public function testUpdateOAuth2KeycloakEmptyEndpointRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `endpoint` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting — the validator should treat the
|
||||
// empty-string degenerate case as a missing required field.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => '',
|
||||
'realmName' => 'appwrite-realm',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakRequiresRealmName(): void
|
||||
public function testUpdateOAuth2KeycloakAllowsOmittedRealmNameWhenDisabled(): void
|
||||
{
|
||||
// The `realmName` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => 'keycloak.example.com',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('keycloak', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakEmptyRealmNameRejected(): void
|
||||
public function testUpdateOAuth2KeycloakEmptyRealmNameRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `realmName` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => 'keycloak.example.com',
|
||||
'realmName' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -1200,16 +1215,15 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.keycloak.com',
|
||||
'realmName' => 'cleanup-realm',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakPartialPreservesSecret(): void
|
||||
{
|
||||
// Keycloak's `endpoint` and `realmName` are required on every call,
|
||||
// so we always re-send them. The `clientSecret` lives in the JSON
|
||||
// The `clientSecret`, `endpoint`, and `realmName` live in the JSON
|
||||
// blob and must survive when omitted on a subsequent call that only
|
||||
// changes clientId.
|
||||
$this->updateOAuth2('keycloak', [
|
||||
@@ -1222,8 +1236,6 @@ trait OAuth2Base
|
||||
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'keycloak-rotated-client',
|
||||
'endpoint' => 'merge.keycloak.com',
|
||||
'realmName' => 'merge-realm',
|
||||
]);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('keycloak-rotated-client', $response['body']['clientId']);
|
||||
@@ -1231,23 +1243,19 @@ trait OAuth2Base
|
||||
$this->assertSame('merge-realm', $response['body']['realmName']);
|
||||
|
||||
// Confirm clientSecret survived the omitted-field merge by enabling
|
||||
// — Keycloak has no verifyCredentials() hook, so non-empty stored
|
||||
// secret is enough. `endpoint`/`realmName` must be re-sent (required
|
||||
// on enable too).
|
||||
// without re-sending endpoint or realmName.
|
||||
$enable = $this->updateOAuth2('keycloak', [
|
||||
'endpoint' => 'merge.keycloak.com',
|
||||
'realmName' => 'merge-realm',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Cleanup — endpoint and realmName are required, use placeholders.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.keycloak.com',
|
||||
'realmName' => 'cleanup-realm',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1274,40 +1282,47 @@ trait OAuth2Base
|
||||
$this->assertSame('enable-realm', $get['body']['realmName']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup — endpoint and realmName are required (Text(min=1)) so use placeholders.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.keycloak.com',
|
||||
'realmName' => 'cleanup-realm',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Microsoft (applicationId + applicationSecret + REQUIRED tenant)
|
||||
// Update Microsoft (applicationId + applicationSecret + optional tenant)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2MicrosoftRequiresTenant(): void
|
||||
public function testUpdateOAuth2MicrosoftAllowsOmittedTenantWhenDisabled(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('microsoft', [
|
||||
'applicationId' => 'whatever',
|
||||
'applicationSecret' => 'whatever',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('microsoft', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2MicrosoftEmptyTenantRejected(): void
|
||||
public function testUpdateOAuth2MicrosoftEmptyTenantRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `tenant` validator is Text(min=1). Sending `''` must be rejected
|
||||
// the same way as omitting — the validator should treat the empty
|
||||
// string as a missing required field.
|
||||
$response = $this->updateOAuth2('microsoft', [
|
||||
'applicationId' => 'whatever',
|
||||
'applicationSecret' => 'whatever',
|
||||
'tenant' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -1335,7 +1350,7 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => 'common',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1350,23 +1365,21 @@ trait OAuth2Base
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
// Patch with only `tenant` (it's required on every call) and a new
|
||||
// applicationId, leaving applicationSecret omitted. The stored secret
|
||||
// must not be wiped.
|
||||
// Patch with only a new applicationId, leaving applicationSecret and
|
||||
// tenant omitted. The stored JSON values must not be wiped.
|
||||
$response = $this->updateOAuth2('microsoft', [
|
||||
'applicationId' => 'updated-app-id',
|
||||
'tenant' => 'organizations',
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('updated-app-id', $response['body']['applicationId']);
|
||||
$this->assertSame('organizations', $response['body']['tenant']);
|
||||
$this->assertSame('common', $response['body']['tenant']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => 'common',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1391,11 +1404,11 @@ trait OAuth2Base
|
||||
$this->assertSame('common', $get['body']['tenant']);
|
||||
$this->assertSame('', $get['body']['applicationSecret']);
|
||||
|
||||
// Cleanup — tenant is required (Text(min=1)) so use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => 'common',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -2405,8 +2418,9 @@ trait OAuth2Base
|
||||
//
|
||||
// Ensures each provider's Update endpoint is wired up correctly: routing,
|
||||
// provider class, response model and `$id`. Custom-shaped providers
|
||||
// (apple, auth0, authentik, gitlab, microsoft, oidc, okta, dropbox) and
|
||||
// sandboxes (paypalSandbox, tradeshiftSandbox) have dedicated tests above.
|
||||
// (apple, auth0, authentik, fusionauth, gitlab, keycloak, microsoft, oidc,
|
||||
// okta, dropbox) and sandboxes (paypalSandbox, tradeshiftSandbox) have
|
||||
// dedicated tests above.
|
||||
// Github is excluded because its `verifyCredentials()` hook is exercised
|
||||
// separately.
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user