mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
d2230f8fe7
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.
1149 lines
42 KiB
PHP
1149 lines
42 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Platform\Modules\Installer\Runtime;
|
|
|
|
use Appwrite\Platform\Installer\Runtime\Config;
|
|
use Appwrite\Platform\Installer\Runtime\State;
|
|
use Appwrite\Platform\Installer\Server;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class StateTest extends TestCase
|
|
{
|
|
protected ?State $state = null;
|
|
private string $tempDir;
|
|
private array $progressFiles = [];
|
|
private ?string $savedEnv = null;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->tempDir = sys_get_temp_dir() . '/appwrite-installer-test-' . uniqid();
|
|
mkdir($this->tempDir, 0755, true);
|
|
|
|
$this->state = new State();
|
|
|
|
// Preserve env state
|
|
$env = getenv('APPWRITE_INSTALLER_CONFIG');
|
|
$this->savedEnv = $env !== false ? $env : null;
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
// Clean up temp files
|
|
$files = glob($this->tempDir . '/*');
|
|
if (is_array($files)) {
|
|
foreach ($files as $file) {
|
|
@unlink($file);
|
|
}
|
|
}
|
|
@rmdir($this->tempDir);
|
|
|
|
// Clean up progress files
|
|
foreach ($this->progressFiles as $file) {
|
|
@unlink($file);
|
|
}
|
|
|
|
// Clean up lock file
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
@unlink(Server::INSTALLER_CONFIG_FILE);
|
|
|
|
// Restore env state
|
|
if ($this->savedEnv !== null) {
|
|
putenv('APPWRITE_INSTALLER_CONFIG=' . $this->savedEnv);
|
|
} else {
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
}
|
|
|
|
$this->state = null;
|
|
}
|
|
|
|
private function trackProgressFile(string $installId): void
|
|
{
|
|
$this->progressFiles[] = $this->state->progressFilePath($installId);
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithValidId(): void
|
|
{
|
|
$this->assertEquals('abc123', $this->state->sanitizeInstallId('abc123'));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithSpecialChars(): void
|
|
{
|
|
$this->assertEquals('abc123', $this->state->sanitizeInstallId('abc!@#123'));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithHyphensAndUnderscores(): void
|
|
{
|
|
$this->assertEquals('abc-123_def', $this->state->sanitizeInstallId('abc-123_def'));
|
|
}
|
|
|
|
public function testSanitizeInstallIdTruncatesTo64Chars(): void
|
|
{
|
|
$long = str_repeat('a', 100);
|
|
$this->assertEquals(64, strlen($this->state->sanitizeInstallId($long)));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithEmptyString(): void
|
|
{
|
|
$this->assertEquals('', $this->state->sanitizeInstallId(''));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithNonString(): void
|
|
{
|
|
$this->assertEquals('', $this->state->sanitizeInstallId(123));
|
|
$this->assertEquals('', $this->state->sanitizeInstallId(null));
|
|
}
|
|
|
|
public function testHashSensitiveValueProducesConsistentHash(): void
|
|
{
|
|
$hash1 = $this->state->hashSensitiveValue('secret');
|
|
$hash2 = $this->state->hashSensitiveValue('secret');
|
|
$this->assertEquals($hash1, $hash2);
|
|
}
|
|
|
|
public function testHashSensitiveValueDifferentInputsDifferentHashes(): void
|
|
{
|
|
$hash1 = $this->state->hashSensitiveValue('secret1');
|
|
$hash2 = $this->state->hashSensitiveValue('secret2');
|
|
$this->assertNotEquals($hash1, $hash2);
|
|
}
|
|
|
|
public function testHashSensitiveValueTrimsWhitespace(): void
|
|
{
|
|
$hash1 = $this->state->hashSensitiveValue('secret');
|
|
$hash2 = $this->state->hashSensitiveValue(' secret ');
|
|
$this->assertEquals($hash1, $hash2);
|
|
}
|
|
|
|
public function testHashSensitiveValueEmptyStringReturnsEmpty(): void
|
|
{
|
|
$this->assertEquals('', $this->state->hashSensitiveValue(''));
|
|
$this->assertEquals('', $this->state->hashSensitiveValue(' '));
|
|
}
|
|
|
|
public function testHashSensitiveValueReturnsSha256(): void
|
|
{
|
|
$hash = $this->state->hashSensitiveValue('test');
|
|
$this->assertEquals(64, strlen($hash)); // SHA-256 produces 64 hex chars
|
|
$this->assertMatchesRegularExpression('/^[a-f0-9]{64}$/', $hash);
|
|
}
|
|
|
|
public function testIsValidPortWithValidPorts(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidPort('1'));
|
|
$this->assertTrue($this->state->isValidPort('80'));
|
|
$this->assertTrue($this->state->isValidPort('443'));
|
|
$this->assertTrue($this->state->isValidPort('8080'));
|
|
$this->assertTrue($this->state->isValidPort('65535'));
|
|
}
|
|
|
|
public function testIsValidPortWithInvalidPorts(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidPort('0'));
|
|
$this->assertFalse($this->state->isValidPort('65536'));
|
|
$this->assertFalse($this->state->isValidPort('-1'));
|
|
$this->assertFalse($this->state->isValidPort('abc'));
|
|
$this->assertFalse($this->state->isValidPort(''));
|
|
$this->assertFalse($this->state->isValidPort('80.5'));
|
|
$this->assertFalse($this->state->isValidPort('80abc'));
|
|
}
|
|
|
|
public function testIsValidPortWithIntegerInput(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidPort(80));
|
|
$this->assertTrue($this->state->isValidPort(443));
|
|
$this->assertFalse($this->state->isValidPort(0));
|
|
}
|
|
|
|
public function testIsValidEmailAddressWithValidEmails(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidEmailAddress('user@example.com'));
|
|
$this->assertTrue($this->state->isValidEmailAddress('test.user@domain.org'));
|
|
$this->assertTrue($this->state->isValidEmailAddress('admin+tag@example.co.uk'));
|
|
}
|
|
|
|
public function testIsValidEmailAddressWithInvalidEmails(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidEmailAddress(''));
|
|
$this->assertFalse($this->state->isValidEmailAddress('notanemail'));
|
|
$this->assertFalse($this->state->isValidEmailAddress('@domain.com'));
|
|
$this->assertFalse($this->state->isValidEmailAddress('user@'));
|
|
}
|
|
|
|
public function testIsValidPasswordWithValidPasswords(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidPassword('12345678'));
|
|
$this->assertTrue($this->state->isValidPassword('abcdefgh'));
|
|
$this->assertTrue($this->state->isValidPassword('P@ssw0rd!'));
|
|
}
|
|
|
|
public function testIsValidPasswordWithInvalidPasswords(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidPassword(''));
|
|
$this->assertFalse($this->state->isValidPassword('short'));
|
|
$this->assertFalse($this->state->isValidPassword('1234567')); // 7 chars
|
|
$this->assertFalse($this->state->isValidPassword(' ')); // 8 spaces, no non-whitespace
|
|
}
|
|
|
|
public function testIsValidSecretKeyWithValidKeys(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidSecretKey('a'));
|
|
$this->assertTrue($this->state->isValidSecretKey('my-secret-key'));
|
|
$this->assertTrue($this->state->isValidSecretKey(str_repeat('x', 64)));
|
|
}
|
|
|
|
public function testIsValidSecretKeyWithInvalidKeys(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidSecretKey(''));
|
|
$this->assertFalse($this->state->isValidSecretKey(str_repeat('x', 65)));
|
|
}
|
|
|
|
public function testIsValidAccountNameWithValidNames(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidAccountName('John'));
|
|
$this->assertTrue($this->state->isValidAccountName('a'));
|
|
}
|
|
|
|
public function testIsValidAccountNameWithInvalidNames(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidAccountName(''));
|
|
$this->assertFalse($this->state->isValidAccountName(' '));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithValidDomains(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidAppDomainInput('localhost'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('example.com'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('sub.example.com'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('127.0.0.1'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('192.168.1.1'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithPort(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidAppDomainInput('localhost:8080'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('example.com:443'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('127.0.0.1:3000'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithIpv6(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidAppDomainInput('[::1]'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('[::1]:8080'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithInvalidDomains(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidAppDomainInput(''));
|
|
$this->assertFalse($this->state->isValidAppDomainInput(' '));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('localhost:99999'));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('localhost:0'));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('host:port:extra'));
|
|
}
|
|
|
|
public function testIsValidDatabaseAdapterWithValidAdapters(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidDatabaseAdapter('mongodb'));
|
|
$this->assertTrue($this->state->isValidDatabaseAdapter('mariadb'));
|
|
$this->assertTrue($this->state->isValidDatabaseAdapter('postgresql'));
|
|
}
|
|
|
|
public function testIsValidDatabaseAdapterWithInvalidAdapters(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter(''));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('mysql'));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('postgres'));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('PostgreSQL'));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('MongoDB')); // case sensitive
|
|
}
|
|
|
|
public function testProgressFilePathFormat(): void
|
|
{
|
|
$path = $this->state->progressFilePath('test123');
|
|
$this->assertStringContainsString('appwrite-install-test123.json', $path);
|
|
$this->assertStringStartsWith(sys_get_temp_dir(), $path);
|
|
}
|
|
|
|
public function testReadProgressFileReturnsDefaultForMissing(): void
|
|
{
|
|
$data = $this->state->readProgressFile('nonexistent-id-' . uniqid());
|
|
$this->assertArrayHasKey('installId', $data);
|
|
$this->assertArrayHasKey('steps', $data);
|
|
$this->assertEmpty($data['steps']);
|
|
}
|
|
|
|
public function testWriteAndReadProgressFile(): void
|
|
{
|
|
$installId = 'test-' . uniqid();
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Writing environment variables',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayHasKey('steps', $data);
|
|
$this->assertArrayHasKey(Server::STEP_ENV_VARS, $data['steps']);
|
|
$this->assertEquals(Server::STATUS_IN_PROGRESS, $data['steps'][Server::STEP_ENV_VARS]['status']);
|
|
$this->assertEquals('Writing environment variables', $data['steps'][Server::STEP_ENV_VARS]['message']);
|
|
|
|
// Cleanup
|
|
@unlink($this->state->progressFilePath($installId));
|
|
}
|
|
|
|
public function testWriteProgressFileAccumulatesSteps(): void
|
|
{
|
|
$installId = 'test-multi-' . uniqid();
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'Done',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_DOCKER_COMPOSE,
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Generating compose file',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertCount(2, $data['steps']);
|
|
$this->assertArrayHasKey(Server::STEP_ENV_VARS, $data['steps']);
|
|
$this->assertArrayHasKey(Server::STEP_DOCKER_COMPOSE, $data['steps']);
|
|
|
|
// Cleanup
|
|
@unlink($this->state->progressFilePath($installId));
|
|
}
|
|
|
|
public function testWriteProgressFileStoresPayload(): void
|
|
{
|
|
$installId = 'test-payload-' . uniqid();
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'payload' => [
|
|
'httpPort' => '80',
|
|
'httpsPort' => '443',
|
|
'database' => 'mariadb',
|
|
],
|
|
'step' => 'start',
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Started',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayHasKey('payload', $data);
|
|
$this->assertEquals('80', $data['payload']['httpPort']);
|
|
$this->assertEquals('443', $data['payload']['httpsPort']);
|
|
$this->assertEquals('mariadb', $data['payload']['database']);
|
|
$this->assertArrayHasKey('startedAt', $data);
|
|
|
|
// Cleanup
|
|
@unlink($this->state->progressFilePath($installId));
|
|
}
|
|
|
|
public function testWriteProgressFileStoresErrorMessage(): void
|
|
{
|
|
$installId = 'test-error-' . uniqid();
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_DOCKER_CONTAINERS,
|
|
'status' => Server::STATUS_ERROR,
|
|
'message' => 'Container failed to start',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayHasKey('error', $data);
|
|
$this->assertEquals('Container failed to start', $data['error']);
|
|
|
|
// Cleanup
|
|
@unlink($this->state->progressFilePath($installId));
|
|
}
|
|
|
|
public function testWriteProgressFileStoresDetails(): void
|
|
{
|
|
$installId = 'test-details-' . uniqid();
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_DOCKER_COMPOSE,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'Done',
|
|
'details' => ['composeFile' => '/path/to/docker-compose.yml'],
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayHasKey('details', $data);
|
|
$this->assertArrayHasKey(Server::STEP_DOCKER_COMPOSE, $data['details']);
|
|
$this->assertEquals('/path/to/docker-compose.yml', $data['details'][Server::STEP_DOCKER_COMPOSE]['composeFile']);
|
|
|
|
// Cleanup
|
|
@unlink($this->state->progressFilePath($installId));
|
|
}
|
|
|
|
public function testBuildConfigReturnsConfigInstance(): void
|
|
{
|
|
// Clear env to avoid interference
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
|
|
$config = $this->state->buildConfig([], false);
|
|
$this->assertInstanceOf(Config::class, $config);
|
|
}
|
|
|
|
public function testBuildConfigAppliesOverrides(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
|
|
$config = $this->state->buildConfig(['defaultHttpPort' => '9090'], false);
|
|
$this->assertEquals('9090', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testBuildConfigFromEnvVar(): void
|
|
{
|
|
$envData = json_encode([
|
|
'defaultHttpPort' => '8888',
|
|
'isUpgrade' => true,
|
|
]);
|
|
putenv('APPWRITE_INSTALLER_CONFIG=' . $envData);
|
|
|
|
$config = $this->state->buildConfig([], true);
|
|
$this->assertEquals('8888', $config->getDefaultHttpPort());
|
|
$this->assertTrue($config->isUpgrade());
|
|
|
|
// Cleanup
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
}
|
|
|
|
public function testBuildConfigOverridesEnv(): void
|
|
{
|
|
$envData = json_encode(['defaultHttpPort' => '8888']);
|
|
putenv('APPWRITE_INSTALLER_CONFIG=' . $envData);
|
|
|
|
$config = $this->state->buildConfig(['defaultHttpPort' => '7777'], true);
|
|
$this->assertEquals('7777', $config->getDefaultHttpPort());
|
|
|
|
// Cleanup
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithOnlySpecialChars(): void
|
|
{
|
|
$this->assertEquals('', $this->state->sanitizeInstallId('!@#$%^&*()'));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithUnicode(): void
|
|
{
|
|
// Unicode letters are stripped byte-by-byte, only ASCII alphanum + hyphen + underscore kept
|
|
// 'é' is 2 bytes (0xC3 0xA9), both stripped => 'héllo' becomes 'hllo'
|
|
$this->assertEquals('hllo', $this->state->sanitizeInstallId('héllo'));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithExactly64Chars(): void
|
|
{
|
|
$exact = str_repeat('b', 64);
|
|
$this->assertEquals($exact, $this->state->sanitizeInstallId($exact));
|
|
$this->assertEquals(64, strlen($this->state->sanitizeInstallId($exact)));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithBooleanInput(): void
|
|
{
|
|
$this->assertEquals('', $this->state->sanitizeInstallId(true));
|
|
$this->assertEquals('', $this->state->sanitizeInstallId(false));
|
|
}
|
|
|
|
public function testSanitizeInstallIdWithArrayInput(): void
|
|
{
|
|
$this->assertEquals('', $this->state->sanitizeInstallId([]));
|
|
}
|
|
|
|
public function testSanitizeInstallIdPreservesCase(): void
|
|
{
|
|
$this->assertEquals('AbCdEf', $this->state->sanitizeInstallId('AbCdEf'));
|
|
}
|
|
|
|
public function testIsValidPortBoundaryValues(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidPort('1'));
|
|
$this->assertTrue($this->state->isValidPort('65535'));
|
|
$this->assertFalse($this->state->isValidPort('0'));
|
|
$this->assertFalse($this->state->isValidPort('65536'));
|
|
}
|
|
|
|
public function testIsValidPortWithLeadingZeros(): void
|
|
{
|
|
// '080' is digits-only and parses to 80 which is in range
|
|
$this->assertTrue($this->state->isValidPort('080'));
|
|
// '00' parses to 0, which is out of range
|
|
$this->assertFalse($this->state->isValidPort('00'));
|
|
}
|
|
|
|
public function testIsValidPortWithWhitespace(): void
|
|
{
|
|
// Contains non-digit characters
|
|
$this->assertFalse($this->state->isValidPort(' 80'));
|
|
$this->assertFalse($this->state->isValidPort('80 '));
|
|
$this->assertFalse($this->state->isValidPort(' 80 '));
|
|
}
|
|
|
|
public function testIsValidPortWithNegativeNumber(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidPort('-80'));
|
|
$this->assertFalse($this->state->isValidPort('-1'));
|
|
}
|
|
|
|
public function testIsValidPortWithVeryLargeNumber(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidPort('999999'));
|
|
$this->assertFalse($this->state->isValidPort('100000'));
|
|
}
|
|
|
|
public function testIsValidPasswordExactly8Chars(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidPassword('12345678'));
|
|
$this->assertFalse($this->state->isValidPassword('1234567'));
|
|
}
|
|
|
|
public function testIsValidPasswordWithTabsAndNewlines(): void
|
|
{
|
|
// Tabs/newlines count as whitespace, but need at least one non-whitespace
|
|
$this->assertFalse($this->state->isValidPassword("\t\t\t\t\t\t\t\t")); // 8 tabs
|
|
$this->assertTrue($this->state->isValidPassword("\t\t\t\ttest")); // mixed
|
|
}
|
|
|
|
public function testIsValidPasswordWithMixedWhitespaceAndChars(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidPassword(' a ')); // has non-whitespace
|
|
}
|
|
|
|
public function testIsValidSecretKeyExactly64Chars(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidSecretKey(str_repeat('a', 64)));
|
|
}
|
|
|
|
public function testIsValidSecretKeyWithWhitespace(): void
|
|
{
|
|
// Whitespace-only is still non-empty and <= 64 chars
|
|
$this->assertTrue($this->state->isValidSecretKey(' '));
|
|
$this->assertTrue($this->state->isValidSecretKey(' '));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithEmptyPort(): void
|
|
{
|
|
// "host:" splits to ['host', ''] - empty port with null check
|
|
$this->assertTrue($this->state->isValidAppDomainInput('localhost:'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithIpv4Address(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidAppDomainInput('10.0.0.1'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('255.255.255.255'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('0.0.0.0'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputIpv6WithoutBrackets(): void
|
|
{
|
|
// Raw IPv6 without brackets: "::1" has two colons, so count($parts) > 2 => false
|
|
$this->assertFalse($this->state->isValidAppDomainInput('::1'));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('fe80::1'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputIpv6MalformedBrackets(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidAppDomainInput('['));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('[]'));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('[invalid'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithSubdomains(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidAppDomainInput('a.b.c.d.example.com'));
|
|
$this->assertTrue($this->state->isValidAppDomainInput('my-app.example.io:8080'));
|
|
}
|
|
|
|
public function testIsValidAppDomainInputWithInvalidPortNumber(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidAppDomainInput('localhost:abc'));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('localhost:70000'));
|
|
$this->assertFalse($this->state->isValidAppDomainInput('[::1]:70000'));
|
|
}
|
|
|
|
public function testIsValidDatabaseAdapterWithWhitespace(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter(' mongodb'));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('mariadb '));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter(' postgresql'));
|
|
}
|
|
|
|
public function testIsValidDatabaseAdapterCaseSensitivity(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('MongoDB'));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('MariaDB'));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('PostgreSQL'));
|
|
$this->assertFalse($this->state->isValidDatabaseAdapter('MONGODB'));
|
|
}
|
|
|
|
public function testReadProgressFileWithCorruptedJson(): void
|
|
{
|
|
$installId = 'test-corrupt-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
$path = $this->state->progressFilePath($installId);
|
|
file_put_contents($path, 'not valid json {{{');
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayHasKey('installId', $data);
|
|
$this->assertArrayHasKey('steps', $data);
|
|
$this->assertEmpty($data['steps']);
|
|
}
|
|
|
|
public function testReadProgressFileWithEmptyFile(): void
|
|
{
|
|
$installId = 'test-empty-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
$path = $this->state->progressFilePath($installId);
|
|
file_put_contents($path, '');
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayHasKey('installId', $data);
|
|
$this->assertEmpty($data['steps']);
|
|
}
|
|
|
|
public function testReadProgressFileWithJsonScalar(): void
|
|
{
|
|
$installId = 'test-scalar-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
$path = $this->state->progressFilePath($installId);
|
|
file_put_contents($path, '"just a string"');
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertEmpty($data['steps']);
|
|
}
|
|
|
|
public function testWriteProgressFileOverwritesExistingStep(): void
|
|
{
|
|
$installId = 'test-overwrite-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Working...',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'Done!',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertCount(1, $data['steps']); // Still 1 step, overwritten
|
|
$this->assertEquals(Server::STATUS_COMPLETED, $data['steps'][Server::STEP_ENV_VARS]['status']);
|
|
$this->assertEquals('Done!', $data['steps'][Server::STEP_ENV_VARS]['message']);
|
|
}
|
|
|
|
public function testWriteProgressFileWithEmptyStep(): void
|
|
{
|
|
$installId = 'test-emptystep-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => '',
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'No step name',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
// Empty step name treated as falsy, should not add to steps
|
|
$this->assertEmpty($data['steps']);
|
|
}
|
|
|
|
public function testWriteProgressFilePreservesPayloadAcrossWrites(): void
|
|
{
|
|
$installId = 'test-persist-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'payload' => ['httpPort' => '80', 'database' => 'mongodb'],
|
|
'step' => 'start',
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Starting',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'Env done',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
// Payload from first write should still be present
|
|
$this->assertArrayHasKey('payload', $data);
|
|
$this->assertEquals('80', $data['payload']['httpPort']);
|
|
$this->assertEquals('mongodb', $data['payload']['database']);
|
|
// Both steps should exist
|
|
$this->assertArrayHasKey('start', $data['steps']);
|
|
$this->assertArrayHasKey(Server::STEP_ENV_VARS, $data['steps']);
|
|
}
|
|
|
|
public function testWriteProgressFileUpdatesTimestamp(): void
|
|
{
|
|
$installId = 'test-time-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
$now = time();
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'test',
|
|
'updatedAt' => $now,
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertEquals($now, $data['updatedAt']);
|
|
}
|
|
|
|
public function testWriteProgressFileStartedAtOnlySetOnce(): void
|
|
{
|
|
$installId = 'test-startedat-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
$firstTime = time() - 100;
|
|
|
|
// First write with payload sets startedAt
|
|
$this->state->writeProgressFile($installId, [
|
|
'payload' => ['httpPort' => '80'],
|
|
'step' => 'start',
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Starting',
|
|
'updatedAt' => $firstTime,
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$startedAt = $data['startedAt'];
|
|
|
|
// Second write with payload should NOT overwrite startedAt
|
|
$this->state->writeProgressFile($installId, [
|
|
'payload' => ['httpPort' => '80'],
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Env',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertEquals($startedAt, $data['startedAt']);
|
|
}
|
|
|
|
public function testReserveGlobalLockFirstLockSucceeds(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId = 'lock-test-' . uniqid();
|
|
$result = $this->state->reserveGlobalLock($installId);
|
|
$this->assertEquals('ok', $result);
|
|
}
|
|
|
|
public function testReserveGlobalLockSameIdCanRelock(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId = 'lock-relock-' . uniqid();
|
|
|
|
$result1 = $this->state->reserveGlobalLock($installId);
|
|
$this->assertEquals('ok', $result1);
|
|
|
|
// Same ID can re-reserve
|
|
$result2 = $this->state->reserveGlobalLock($installId);
|
|
$this->assertEquals('ok', $result2);
|
|
}
|
|
|
|
public function testReserveGlobalLockDifferentIdBlocked(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId1 = 'lock-id1-' . uniqid();
|
|
$installId2 = 'lock-id2-' . uniqid();
|
|
|
|
$result1 = $this->state->reserveGlobalLock($installId1);
|
|
$this->assertEquals('ok', $result1);
|
|
|
|
// Different ID should be blocked
|
|
$result2 = $this->state->reserveGlobalLock($installId2);
|
|
$this->assertEquals('locked', $result2);
|
|
}
|
|
|
|
public function testReserveGlobalLockAfterCompleted(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId1 = 'lock-done-' . uniqid();
|
|
$installId2 = 'lock-new-' . uniqid();
|
|
|
|
$this->state->reserveGlobalLock($installId1);
|
|
$this->state->updateGlobalLock($installId1, Server::STATUS_COMPLETED);
|
|
|
|
// After completion, a new install should be able to lock
|
|
$result = $this->state->reserveGlobalLock($installId2);
|
|
$this->assertEquals('ok', $result);
|
|
}
|
|
|
|
public function testReserveGlobalLockAfterError(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId1 = 'lock-err-' . uniqid();
|
|
$installId2 = 'lock-retry-' . uniqid();
|
|
|
|
$this->state->reserveGlobalLock($installId1);
|
|
$this->state->updateGlobalLock($installId1, Server::STATUS_ERROR);
|
|
|
|
// After error, a new install should be able to lock
|
|
$result = $this->state->reserveGlobalLock($installId2);
|
|
$this->assertEquals('ok', $result);
|
|
}
|
|
|
|
public function testReserveGlobalLockExpiredLockAllowsNew(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
|
|
// Manually write an expired lock (updatedAt way in the past)
|
|
$expiredLock = [
|
|
'installId' => 'expired-lock',
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'updatedAt' => time() - 7200, // 2 hours ago, timeout is 1 hour
|
|
];
|
|
file_put_contents(Server::INSTALLER_LOCK_FILE, json_encode($expiredLock));
|
|
|
|
$newId = 'lock-after-expired-' . uniqid();
|
|
$result = $this->state->reserveGlobalLock($newId);
|
|
$this->assertEquals('ok', $result);
|
|
}
|
|
|
|
public function testUpdateGlobalLockUpdatesOwnLock(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId = 'lock-update-' . uniqid();
|
|
|
|
$this->state->reserveGlobalLock($installId);
|
|
$this->state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
|
|
|
// Read lock file directly to verify
|
|
$contents = file_get_contents(Server::INSTALLER_LOCK_FILE);
|
|
$this->assertNotFalse($contents);
|
|
$lock = json_decode($contents, true);
|
|
$this->assertIsArray($lock);
|
|
$this->assertEquals($installId, $lock['installId']);
|
|
$this->assertEquals(Server::STATUS_COMPLETED, $lock['status']);
|
|
}
|
|
|
|
public function testUpdateGlobalLockIgnoresDifferentId(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId1 = 'lock-owner-' . uniqid();
|
|
$installId2 = 'lock-intruder-' . uniqid();
|
|
|
|
$this->state->reserveGlobalLock($installId1);
|
|
|
|
// Attempt to update with a different ID should be silently ignored
|
|
$this->state->updateGlobalLock($installId2, Server::STATUS_COMPLETED);
|
|
|
|
// Original lock should still be in progress
|
|
$contents = file_get_contents(Server::INSTALLER_LOCK_FILE);
|
|
$lock = json_decode($contents, true);
|
|
$this->assertEquals($installId1, $lock['installId']);
|
|
$this->assertEquals(Server::STATUS_IN_PROGRESS, $lock['status']);
|
|
}
|
|
|
|
public function testApplyEnvConfigWithConfigObject(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
@unlink(Server::INSTALLER_CONFIG_FILE);
|
|
|
|
$cfg = new Config(['defaultHttpPort' => '5555', 'isLocal' => true]);
|
|
$this->state->applyEnvConfig($cfg);
|
|
|
|
// Verify env var was set
|
|
$envVal = getenv('APPWRITE_INSTALLER_CONFIG');
|
|
$this->assertNotFalse($envVal);
|
|
|
|
$decoded = json_decode($envVal, true);
|
|
$this->assertIsArray($decoded);
|
|
$this->assertEquals('5555', $decoded['defaultHttpPort']);
|
|
$this->assertTrue($decoded['isLocal']);
|
|
|
|
// Verify config file was written
|
|
$this->assertFileExists(Server::INSTALLER_CONFIG_FILE);
|
|
$fileContents = file_get_contents(Server::INSTALLER_CONFIG_FILE);
|
|
$this->assertNotFalse($fileContents);
|
|
$fileDecoded = json_decode($fileContents, true);
|
|
$this->assertEquals('5555', $fileDecoded['defaultHttpPort']);
|
|
}
|
|
|
|
public function testApplyEnvConfigWithArray(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
@unlink(Server::INSTALLER_CONFIG_FILE);
|
|
|
|
$this->state->applyEnvConfig(['defaultHttpPort' => '6666']);
|
|
|
|
$envVal = getenv('APPWRITE_INSTALLER_CONFIG');
|
|
$this->assertNotFalse($envVal);
|
|
$decoded = json_decode($envVal, true);
|
|
$this->assertEquals('6666', $decoded['defaultHttpPort']);
|
|
}
|
|
|
|
public function testApplyEnvConfigThenBuildConfigReadsIt(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
@unlink(Server::INSTALLER_CONFIG_FILE);
|
|
|
|
$cfg = new Config(['defaultHttpPort' => '4444', 'isUpgrade' => true]);
|
|
$this->state->applyEnvConfig($cfg);
|
|
|
|
// buildConfig with useEnv=true should pick up the env var
|
|
$rebuilt = $this->state->buildConfig([], true);
|
|
$this->assertEquals('4444', $rebuilt->getDefaultHttpPort());
|
|
$this->assertTrue($rebuilt->isUpgrade());
|
|
}
|
|
|
|
public function testBuildConfigWithInvalidEnvJson(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG=not-valid-json');
|
|
|
|
// Should fall back to config file (or defaults if file doesn't exist)
|
|
@unlink(Server::INSTALLER_CONFIG_FILE);
|
|
$config = $this->state->buildConfig([], true);
|
|
// Should get defaults since both env and file are invalid/missing
|
|
$this->assertEquals('80', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testBuildConfigWithEmptyEnvVar(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG=');
|
|
|
|
@unlink(Server::INSTALLER_CONFIG_FILE);
|
|
$config = $this->state->buildConfig([], true);
|
|
$this->assertEquals('80', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testBuildConfigFallsBackToConfigFile(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
|
|
// Write a config file
|
|
$data = json_encode(['defaultHttpPort' => '3333']);
|
|
file_put_contents(Server::INSTALLER_CONFIG_FILE, $data);
|
|
|
|
$config = $this->state->buildConfig([], true);
|
|
$this->assertEquals('3333', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testBuildConfigWithCorruptedConfigFile(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
|
|
file_put_contents(Server::INSTALLER_CONFIG_FILE, 'garbage data {{{');
|
|
|
|
$config = $this->state->buildConfig([], true);
|
|
// Should get defaults
|
|
$this->assertEquals('80', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testBuildConfigWithEmptyConfigFile(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG');
|
|
|
|
file_put_contents(Server::INSTALLER_CONFIG_FILE, '');
|
|
|
|
$config = $this->state->buildConfig([], true);
|
|
$this->assertEquals('80', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testBuildConfigUseEnvFalseIgnoresEnvAndFile(): void
|
|
{
|
|
putenv('APPWRITE_INSTALLER_CONFIG=' . json_encode(['defaultHttpPort' => '9999']));
|
|
file_put_contents(Server::INSTALLER_CONFIG_FILE, json_encode(['defaultHttpPort' => '8888']));
|
|
|
|
$config = $this->state->buildConfig([], false);
|
|
// Neither env nor file should be used
|
|
$this->assertEquals('80', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testBuildConfigWithJsonScalarEnvVar(): void
|
|
{
|
|
// A JSON scalar (string) is not an array, so decoding succeeds but is_array fails
|
|
putenv('APPWRITE_INSTALLER_CONFIG="just a string"');
|
|
@unlink(Server::INSTALLER_CONFIG_FILE);
|
|
|
|
$config = $this->state->buildConfig([], true);
|
|
$this->assertEquals('80', $config->getDefaultHttpPort());
|
|
}
|
|
|
|
public function testHashSensitiveValueWithNewlines(): void
|
|
{
|
|
// Newlines are not stripped by trim but surrounding whitespace is
|
|
$hash1 = $this->state->hashSensitiveValue("line1\nline2");
|
|
$hash2 = $this->state->hashSensitiveValue("line1\nline2");
|
|
$this->assertEquals($hash1, $hash2);
|
|
$this->assertNotEmpty($hash1);
|
|
}
|
|
|
|
public function testHashSensitiveValueWithOnlyNewline(): void
|
|
{
|
|
// A newline is not whitespace that trim() removes? Actually trim() removes \n
|
|
// "\n" trimmed becomes "" => should return ''
|
|
$this->assertEquals('', $this->state->hashSensitiveValue("\n"));
|
|
}
|
|
|
|
public function testIsValidEmailAddressWithUnicodeLocal(): void
|
|
{
|
|
// PHP's FILTER_VALIDATE_EMAIL does not support internationalized emails
|
|
$this->assertFalse($this->state->isValidEmailAddress('ünïcödé@example.com'));
|
|
}
|
|
|
|
public function testIsValidEmailAddressWithDoubleAt(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidEmailAddress('user@@example.com'));
|
|
}
|
|
|
|
public function testIsValidEmailAddressWithSpaces(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidEmailAddress('user @example.com'));
|
|
$this->assertFalse($this->state->isValidEmailAddress('user@ example.com'));
|
|
}
|
|
|
|
public function testIsValidAccountNameWithOnlyTabs(): void
|
|
{
|
|
$this->assertFalse($this->state->isValidAccountName("\t\t"));
|
|
}
|
|
|
|
public function testIsValidAccountNameWithMixedWhitespace(): void
|
|
{
|
|
$this->assertTrue($this->state->isValidAccountName(" a "));
|
|
}
|
|
|
|
public function testProgressFilePathWithSpecialCharsInId(): void
|
|
{
|
|
// The ID would normally be sanitized before this call, but the method itself
|
|
// just concatenates
|
|
$path = $this->state->progressFilePath('test-with-special');
|
|
$this->assertStringContainsString('appwrite-install-test-with-special.json', $path);
|
|
}
|
|
|
|
public function testProgressFilePathWithEmptyId(): void
|
|
{
|
|
$path = $this->state->progressFilePath('');
|
|
$this->assertStringContainsString('appwrite-install-.json', $path);
|
|
}
|
|
|
|
public function testWriteProgressFileCompletedDoesNotSetError(): void
|
|
{
|
|
$installId = 'test-noerror-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'All good',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayNotHasKey('error', $data);
|
|
}
|
|
|
|
public function testWriteProgressFileInProgressDoesNotSetError(): void
|
|
{
|
|
$installId = 'test-noerrip-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_DOCKER_COMPOSE,
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Working',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertArrayNotHasKey('error', $data);
|
|
}
|
|
|
|
public function testWriteProgressFileWithNoStep(): void
|
|
{
|
|
$installId = 'test-nostep-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'No step provided',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$data = $this->state->readProgressFile($installId);
|
|
// No step key means no step should be recorded
|
|
$this->assertEmpty($data['steps']);
|
|
// But updatedAt should still be set
|
|
$this->assertArrayHasKey('updatedAt', $data);
|
|
}
|
|
|
|
public function testFullInstallationLifecycle(): void
|
|
{
|
|
@unlink(Server::INSTALLER_LOCK_FILE);
|
|
$installId = 'lifecycle-' . uniqid();
|
|
$this->trackProgressFile($installId);
|
|
|
|
// 1. Reserve lock
|
|
$lockResult = $this->state->reserveGlobalLock($installId);
|
|
$this->assertEquals('ok', $lockResult);
|
|
|
|
// 2. Write progress through multiple steps
|
|
$this->state->writeProgressFile($installId, [
|
|
'payload' => ['httpPort' => '80', 'database' => 'mongodb'],
|
|
'step' => 'start',
|
|
'status' => Server::STATUS_IN_PROGRESS,
|
|
'message' => 'Started',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_ENV_VARS,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'Env vars written',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_DOCKER_COMPOSE,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'Compose generated',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
$this->state->writeProgressFile($installId, [
|
|
'step' => Server::STEP_DOCKER_CONTAINERS,
|
|
'status' => Server::STATUS_COMPLETED,
|
|
'message' => 'Containers started',
|
|
'updatedAt' => time(),
|
|
]);
|
|
|
|
// 3. Verify progress
|
|
$data = $this->state->readProgressFile($installId);
|
|
$this->assertCount(4, $data['steps']); // start + 3 steps
|
|
$this->assertArrayHasKey('payload', $data);
|
|
$this->assertArrayHasKey('startedAt', $data);
|
|
|
|
// 4. Complete the lock
|
|
$this->state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
|
|
|
// 5. Verify a new install can now proceed
|
|
$newId = 'lifecycle-new-' . uniqid();
|
|
$this->trackProgressFile($newId);
|
|
$newResult = $this->state->reserveGlobalLock($newId);
|
|
$this->assertEquals('ok', $newResult);
|
|
}
|
|
}
|