Files
appwrite/tests/unit/Platform/Modules/Installer/ModuleTest.php
T
Chirag Aggarwal d2230f8fe7 chore: bump PHPStan to level 4 and fix all new errors
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:

- `app/controllers/general.php`: path-traversal guard was negating
  `\substr(...)` before the strict comparison (`!\substr(...) === $base`
  was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
  and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
  `DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
  result as an array with `deviceName/deviceBrand/deviceModel` keys.
  Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
  used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
  key was checking `$resourceKey === 'functions'` when `$resourceKey`
  is `'functionId'|'siteId'` — always false. Switched to the intended
  `$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
  to `string|false` to match `openssl_encrypt`; this lets callers'
  `=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
  `array_key_exists('from', [])` branch in the Msg91 provider (empty
  array literal; branch was unreachable).

Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
  PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
  `markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
  on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
  on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
  were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
  Installer State, `$source` on MigrationsWorker, `$account2` on two
  GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
  and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
  `assertNotNull` assertions with `addToAssertionCount(1)` or
  `assertNotEmpty` where the runtime type was already known.
2026-04-19 17:31:20 +05:30

301 lines
12 KiB
PHP

<?php
namespace Tests\Unit\Platform\Modules\Installer;
use Appwrite\Platform\Installer\Http\Installer\Complete;
use Appwrite\Platform\Installer\Http\Installer\Error;
use Appwrite\Platform\Installer\Http\Installer\Install;
use Appwrite\Platform\Installer\Http\Installer\Reset;
use Appwrite\Platform\Installer\Http\Installer\Shutdown;
use Appwrite\Platform\Installer\Http\Installer\Status;
use Appwrite\Platform\Installer\Http\Installer\Validate;
use Appwrite\Platform\Installer\Http\Installer\View;
use Appwrite\Platform\Installer\Module;
use PHPUnit\Framework\TestCase;
use Utopia\Platform\Action;
use Utopia\Platform\Platform;
use Utopia\Platform\Service;
class ModuleTest extends TestCase
{
protected ?Module $module = null;
protected function setUp(): void
{
$this->module = new Module();
}
protected function tearDown(): void
{
$this->module = null;
}
public function testModuleHasHttpService(): void
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$this->assertCount(1, $services);
}
public function testHttpServiceRegistersAllActions(): void
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$service = reset($services);
$actions = $service->getActions();
$this->assertCount(8, $actions);
$this->assertArrayHasKey('installerView', $actions);
$this->assertArrayHasKey('installerStatus', $actions);
$this->assertArrayHasKey('installerValidate', $actions);
$this->assertArrayHasKey('installerComplete', $actions);
$this->assertArrayHasKey('installerShutdown', $actions);
$this->assertArrayHasKey('installerReset', $actions);
$this->assertArrayHasKey('installerInstall', $actions);
$this->assertArrayHasKey('installerCertificateGet', $actions);
}
public function testViewAction(): void
{
$action = $this->getAction('installerView');
$this->assertEquals('installerView', View::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_GET, $action->getHttpMethod());
$this->assertEquals('/', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionParams($action, ['step', 'partial']);
$this->assertActionInjects($action, ['request', 'response', 'installerConfig', 'installerPaths']);
}
public function testStatusAction(): void
{
$action = $this->getAction('installerStatus');
$this->assertEquals('installerStatus', Status::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_GET, $action->getHttpMethod());
$this->assertEquals('/install/status', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionParams($action, ['installId']);
$this->assertActionInjects($action, ['response', 'installerState']);
}
public function testValidateAction(): void
{
$action = $this->getAction('installerValidate');
$this->assertEquals('installerValidate', Validate::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_POST, $action->getHttpMethod());
$this->assertEquals('/install/validate', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionInjects($action, ['request', 'response']);
}
public function testCompleteAction(): void
{
$action = $this->getAction('installerComplete');
$this->assertEquals('installerComplete', Complete::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_POST, $action->getHttpMethod());
$this->assertEquals('/install/complete', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionParams($action, ['installId', 'sessionId', 'sessionSecret', 'sessionExpire']);
$this->assertActionInjects($action, ['request', 'response', 'installerState']);
}
public function testShutdownAction(): void
{
$action = $this->getAction('installerShutdown');
$this->assertEquals('installerShutdown', Shutdown::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_POST, $action->getHttpMethod());
$this->assertEquals('/install/shutdown', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionInjects($action, ['request', 'response', 'swooleServer']);
}
public function testResetAction(): void
{
$action = $this->getAction('installerReset');
$this->assertEquals('installerReset', Reset::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_POST, $action->getHttpMethod());
$this->assertEquals('/install/reset', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionParams($action, ['installId', 'hard']);
$this->assertActionInjects($action, ['request', 'response', 'installerState', 'installerConfig']);
}
public function testInstallAction(): void
{
$action = $this->getAction('installerInstall');
$this->assertEquals('installerInstall', Install::getName());
$this->assertEquals(Action::HTTP_REQUEST_METHOD_POST, $action->getHttpMethod());
$this->assertEquals('/install', $action->getHttpPath());
$this->assertEquals(Action::TYPE_DEFAULT, $action->getType());
$this->assertActionParams($action, [
'appDomain', 'httpPort', 'httpsPort', 'emailCertificates', 'opensslKey',
'assistantOpenAIKey', 'accountEmail', 'accountPassword', 'database',
'installId', 'retryStep', 'migrate',
]);
$this->assertActionInjects($action, ['request', 'response', 'swooleResponse', 'installerState', 'installerConfig', 'installerPaths']);
}
public function testErrorActionClass(): void
{
$error = new Error();
$this->assertEquals('installerError', Error::getName());
$this->assertEquals(Action::TYPE_ERROR, $error->getType());
$this->assertIsCallable($error->getCallback());
}
/**
* @runInSeparateProcess
*/
public function testRouteRegistration(): void
{
$platform = new class (new Module()) extends Platform {};
$platform->init(Service::TYPE_HTTP);
// If we get here without exceptions, route registration succeeded
$this->addToAssertionCount(1);
}
public function testModuleHasNoTaskServices(): void
{
$services = $this->module->getServicesByType(Service::TYPE_TASK);
$this->assertEmpty($services);
}
public function testModuleHasNoWorkerServices(): void
{
$services = $this->module->getServicesByType(Service::TYPE_WORKER);
$this->assertEmpty($services);
}
public function testAllDefaultActionsHaveDescriptions(): void
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$service = reset($services);
foreach ($service->getActions() as $name => $action) {
$desc = $action->getDesc();
$this->assertNotNull($desc, "Action '$name' should have a description");
$this->assertNotEmpty($desc, "Action '$name' description should not be empty");
}
}
public function testAllActionsHaveCallableCallbacks(): void
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$service = reset($services);
foreach ($service->getActions() as $name => $action) {
$callback = $action->getCallback();
$this->assertIsCallable($callback, "Action '$name' callback should be callable");
}
}
public function testActionNamesAreUnique(): void
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$service = reset($services);
$actions = $service->getActions();
$names = array_keys($actions);
$this->assertEquals($names, array_unique($names));
}
public function testRoutePathsAreUniquePerMethod(): void
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$service = reset($services);
$routes = [];
foreach ($service->getActions() as $action) {
$key = $action->getHttpMethod() . ' ' . $action->getHttpPath();
$this->assertArrayNotHasKey($key, $routes, "Duplicate route: $key");
$routes[$key] = true;
}
}
public function testStaticGetNameValues(): void
{
$this->assertEquals('installerView', View::getName());
$this->assertEquals('installerStatus', Status::getName());
$this->assertEquals('installerValidate', Validate::getName());
$this->assertEquals('installerComplete', Complete::getName());
$this->assertEquals('installerShutdown', Shutdown::getName());
$this->assertEquals('installerReset', Reset::getName());
$this->assertEquals('installerInstall', Install::getName());
$this->assertEquals('installerError', Error::getName());
}
public function testActionInstanceTypes(): void
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$service = reset($services);
$actions = $service->getActions();
$this->assertInstanceOf(View::class, $actions['installerView']);
$this->assertInstanceOf(Status::class, $actions['installerStatus']);
$this->assertInstanceOf(Validate::class, $actions['installerValidate']);
$this->assertInstanceOf(Complete::class, $actions['installerComplete']);
$this->assertInstanceOf(Shutdown::class, $actions['installerShutdown']);
$this->assertInstanceOf(Reset::class, $actions['installerReset']);
$this->assertInstanceOf(Install::class, $actions['installerInstall']);
}
public function testGetRoutesUseGetMethod(): void
{
$getActions = ['installerView', 'installerStatus'];
foreach ($getActions as $name) {
$action = $this->getAction($name);
$this->assertEquals(
Action::HTTP_REQUEST_METHOD_GET,
$action->getHttpMethod(),
"Action '$name' should use GET method"
);
}
}
public function testPostRoutesUsePostMethod(): void
{
$postActions = ['installerValidate', 'installerComplete', 'installerShutdown', 'installerReset', 'installerInstall'];
foreach ($postActions as $name) {
$action = $this->getAction($name);
$this->assertEquals(
Action::HTTP_REQUEST_METHOD_POST,
$action->getHttpMethod(),
"Action '$name' should use POST method"
);
}
}
private function getAction(string $name): Action
{
$services = $this->module->getServicesByType(Service::TYPE_HTTP);
$service = reset($services);
$actions = $service->getActions();
$this->assertArrayHasKey($name, $actions);
return $actions[$name];
}
private function assertActionInjects(Action $action, array $expectedInjections): void
{
$injections = [];
foreach ($action->getOptions() as $option) {
if ($option['type'] === 'injection') {
$injections[] = $option['name'];
}
}
$this->assertEquals($expectedInjections, $injections);
}
private function assertActionParams(Action $action, array $expectedParams): void
{
$params = [];
foreach ($action->getOptions() as $key => $option) {
if ($option['type'] === 'param') {
$params[] = substr($key, 6); // strip 'param:' prefix
}
}
$this->assertEquals($expectedParams, $params);
}
}