(feat): installer improvements — reset, state resilience, container progress, SSL email fallback

This commit is contained in:
Jake Barnby
2026-03-24 21:25:57 +13:00
parent 4fffeda596
commit 76684874e9
13 changed files with 410 additions and 42 deletions
@@ -691,6 +691,18 @@ body {
transform: translateY(10px);
}
.install-counter {
margin-left: auto;
opacity: 0;
transition: opacity 0.2s ease;
white-space: nowrap;
user-select: none;
}
.install-row[data-status='in-progress'] .install-counter:not(:empty) {
opacity: 1;
}
.install-row-toggle {
margin-left: auto;
width: 32px;
@@ -897,6 +909,17 @@ body {
gap: var(--gap-m);
}
.install-global-actions {
display: flex;
justify-content: center;
gap: var(--gap-m);
padding: var(--space-4) 0;
}
.install-global-actions.is-hidden {
display: none;
}
.install-error-details .button {
align-self: center;
margin-top: 0;
@@ -114,7 +114,7 @@
return step.inProgress;
};
const updateInstallRow = (row, step, status, message) => {
const updateInstallRow = (row, step, status, message, details) => {
if (!row || !step) return;
row.dataset.status = status;
row.dataset.step = step.id;
@@ -138,6 +138,15 @@
}
}
const counter = row.querySelector('[data-install-counter]');
if (counter) {
const started = details?.containerStarted;
const total = details?.containerTotal;
counter.textContent = (status === STATUS.IN_PROGRESS && started > 0 && total > 0)
? `${started}/${total}`
: '';
}
// Show/hide "Navigate to Console" button for account setup errors
const consoleBtn = row.querySelector('[data-install-console]');
if (consoleBtn) {
@@ -349,7 +358,7 @@
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
const normalizedEmail = (formState?.emailCertificates || '').trim();
const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
@@ -529,7 +538,7 @@
if (!state) return;
const row = ensureRow(step);
if (row) {
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
if (state.status === STATUS?.ERROR) {
updateInstallErrorDetails(row, {
message: state.message,
@@ -579,6 +588,9 @@
}
}
}
if (payload.status === STATUS.ERROR) {
showGlobalActions();
}
scheduleFallback();
};
@@ -616,6 +628,7 @@
const applySnapshot = (snapshot) => {
if (!snapshot || !snapshot.steps) return;
let hasErrors = false;
INSTALLATION_STEPS.forEach((step) => {
const detail = snapshot.steps[step.id];
if (!detail) return;
@@ -624,8 +637,14 @@
message: detail.message,
details: snapshot.details?.[step.id]
});
if (detail.status === STATUS.ERROR) {
hasErrors = true;
}
});
renderProgress();
if (hasErrors) {
showGlobalActions();
}
};
const checkAllCompleted = () => {
@@ -966,6 +985,58 @@
}
});
const globalActions = root.querySelector('[data-install-global-actions]');
const showGlobalActions = () => {
if (globalActions) {
globalActions.classList.remove('is-hidden');
}
};
const performReset = async (hard) => {
const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();
try {
const res = await fetch('/install/reset', {
method: 'POST',
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ installId: installId || '', hard })
});
if (hard && !res.ok) {
const data = await res.json().catch(() => ({}));
showToast?.({
status: 'error',
title: 'Reset failed',
description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
dismissible: true
});
return;
}
} catch (e) {}
clearInstallLock?.();
clearInstallId?.();
cleanupInstallFlow();
window.location.href = '/?step=1';
};
const startOverButton = root.querySelector('[data-install-start-over]');
if (startOverButton) {
startOverButton.addEventListener('click', () => performReset(false));
}
const hardResetButton = root.querySelector('[data-install-hard-reset]');
if (hardResetButton) {
hardResetButton.addEventListener('click', () => {
const confirmed = window.confirm(
'This will stop all containers, remove all volumes (including database data, uploads, and certificates), and delete configuration files.\n\nThis action cannot be undone. Continue?'
);
if (confirmed) {
performReset(true);
}
});
}
// When the user switches back to this tab, check if installation
// finished while the tab was in the background.
document.addEventListener('visibilitychange', () => {
+39 -11
View File
@@ -7,6 +7,8 @@
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
const INSTALL_ID_KEY = 'appwrite-install-id';
const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';
const formState = {
appDomain: null,
@@ -55,13 +57,24 @@
const getInstallLock = () => {
try {
const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
return parsed;
} catch (error) {
return null;
}
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') return parsed;
}
} catch (error) {}
try {
const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
return parsed;
}
}
} catch (error) {}
return null;
};
const setInstallLock = (installId, payload) => {
@@ -79,6 +92,9 @@
try {
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
} catch (error) {}
try {
localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
} catch (error) {}
if (document.body) {
document.body.dataset.installLocked = 'true';
}
@@ -89,6 +105,9 @@
try {
sessionStorage.removeItem(INSTALL_LOCK_KEY);
} catch (error) {}
try {
localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
} catch (error) {}
if (document.body) {
delete document.body.dataset.installLocked;
}
@@ -121,22 +140,31 @@
const getStoredInstallId = () => {
try {
return sessionStorage.getItem(INSTALL_ID_KEY);
} catch (error) {
return null;
}
const val = sessionStorage.getItem(INSTALL_ID_KEY);
if (val) return val;
} catch (error) {}
try {
return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
} catch (error) {}
return null;
};
const storeInstallId = (installId) => {
try {
sessionStorage.setItem(INSTALL_ID_KEY, installId);
} catch (error) {}
try {
localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
} catch (error) {}
};
const clearInstallId = () => {
try {
sessionStorage.removeItem(INSTALL_ID_KEY);
} catch (error) {}
try {
localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
} catch (error) {}
};
window.InstallerStepsState = {
@@ -240,6 +240,9 @@
if (key === 'database') {
value = toDatabaseLabel(formState?.database);
}
if (key === 'emailCertificates' && !value) {
value = formState?.accountEmail;
}
if (value) {
node.textContent = value;
}
+1 -4
View File
@@ -390,10 +390,7 @@
if (!parsePort(httpPort, 'HTTP')) valid = false;
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
if (!sslEmail || !sslEmail.value.trim()) {
setFieldError?.(sslEmail, 'Please enter an email address for SSL certificates');
valid = false;
} else if (!isValidEmail?.(sslEmail.value.trim())) {
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
setFieldError?.(sslEmail, 'Please enter a valid email address');
valid = false;
}
@@ -30,6 +30,7 @@ $isUpgrade = $isUpgrade ?? false;
</span>
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
</div>
<span class="install-counter typography-text-s-400 text-neutral-secondary" data-install-counter></span>
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
</button>
@@ -50,4 +51,13 @@ $isUpgrade = $isUpgrade ?? false;
</div>
</div>
</template>
<div class="install-global-actions is-hidden" data-install-global-actions>
<button type="button" class="button secondary" data-install-start-over>
<span class="button-text typography-text-m-500">Start Over</span>
</button>
<button type="button" class="button secondary" data-install-hard-reset>
<span class="button-text typography-text-m-500">Reset Everything</span>
</button>
</div>
</div>
@@ -35,7 +35,7 @@ class Install extends Action
->param('appDomain', '', new AppDomain(), 'Application domain (hostname, IP, or bracket IPv6 with optional port)')
->param('httpPort', 80, new Range(1, 65535), 'HTTP port')
->param('httpsPort', 443, new Range(1, 65535), 'HTTPS port')
->param('emailCertificates', '', new Email(), 'Email for SSL certificates')
->param('emailCertificates', '', new Email(allowEmpty: true), 'Email for SSL certificates', true)
->param('opensslKey', '', new Text(64, 0), 'Secret API key', true)
->param('assistantOpenAIKey', '', new Text(256, 0), 'OpenAI API key for assistant', true)
->param('accountEmail', '', new Email(allowEmpty: true), 'Account email address', true)
@@ -90,6 +90,9 @@ class Install extends Action
$appDomain = trim($appDomain);
$emailCertificates = trim($emailCertificates);
if ($emailCertificates === '') {
$emailCertificates = trim($accountEmail);
}
$opensslKey = trim($opensslKey);
$assistantOpenAIKey = trim($assistantOpenAIKey);
@@ -140,6 +143,8 @@ class Install extends Action
@unlink(Server::INSTALLER_COMPLETE_FILE);
$state->clearStaleLockIfNeeded();
try {
$lockResult = $state->reserveGlobalLock($installId);
} catch (\Throwable $e) {
@@ -175,15 +180,23 @@ class Install extends Action
if (file_exists($existingPath)) {
$existing = $state->readProgressFile($installId);
if (!empty($existing['steps']) && $retryStep === null) {
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
if ($wantsStream) {
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
$swooleResponse->end();
$previousHadError = isset($existing['error']);
$allCompleted = !$previousHadError && $this->allStepsCompleted($existing['steps']);
if ($previousHadError || $allCompleted) {
@unlink($existingPath);
$existing = null;
} else {
$response->setStatusCode(Response::STATUS_CODE_CONFLICT);
$response->json(['success' => false, 'message' => 'Installation already started']);
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
if ($wantsStream) {
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
$swooleResponse->end();
} else {
$response->setStatusCode(Response::STATUS_CODE_CONFLICT);
$response->json(['success' => false, 'message' => 'Installation already started']);
}
return;
}
return;
}
}
@@ -207,7 +220,8 @@ class Install extends Action
'_APP_ASSISTANT_OPENAI_API_KEY' => $assistantOpenAIKey,
];
if ($this->hasPayload($existing)) {
$previousHadError = is_array($existing) && isset($existing['error']);
if ($this->hasPayload($existing) && !$previousHadError) {
$stored = $existing['payload'];
$inputValues = [
'httpPort' => (string) $httpPort,
@@ -368,8 +382,6 @@ class Install extends Action
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
}
@unlink(Server::INSTALLER_CONFIG_FILE);
if ($wantsStream) {
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, [
'message' => $e->getMessage(),
@@ -392,6 +404,16 @@ class Install extends Action
return is_array($data) && isset($data['payload']) && is_array($data['payload']);
}
private function allStepsCompleted(array $steps): bool
{
foreach ($steps as $step) {
if (($step['status'] ?? '') !== Server::STATUS_COMPLETED) {
return false;
}
}
return true;
}
private function deriveNameFromEmail(string $email): string
{
$parts = explode('@', $email);
@@ -0,0 +1,110 @@
<?php
namespace Appwrite\Platform\Installer\Http\Installer;
use Appwrite\Platform\Installer\Runtime\Config;
use Appwrite\Platform\Installer\Runtime\State;
use Appwrite\Platform\Installer\Server;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Http\Adapter\Swoole\Response;
use Utopia\Platform\Action;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Reset extends Action
{
public static function getName(): string
{
return 'installerReset';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/install/reset')
->desc('Reset installation state')
->param('installId', '', new Text(64, 0), 'Installation ID', true)
->param('hard', false, new Boolean(true), 'Remove all data including volumes and config files', true)
->inject('request')
->inject('response')
->inject('installerState')
->inject('installerConfig')
->callback($this->action(...));
}
public function action(string $installId, bool $hard, Request $request, Response $response, State $state, Config $config): void
{
if (!Validate::validateCsrf($request)) {
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
$response->json(['success' => false, 'message' => 'Invalid CSRF token']);
return;
}
$installId = $state->sanitizeInstallId($installId);
if ($installId !== '') {
@unlink($state->progressFilePath($installId));
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
}
// Use direct clearStaleLock (not throttled) since reset is an
// explicit user action that should guarantee all stale state is gone.
$state->clearStaleLock();
if ($hard) {
$error = $this->performHardReset($config);
if ($error !== null) {
$response->setStatusCode(Response::STATUS_CODE_INTERNAL_SERVER_ERROR);
$response->json(['success' => false, 'message' => $error]);
return;
}
}
$response->json(['success' => true]);
}
private function performHardReset(Config $config): ?string
{
$isLocal = $config->isLocal();
$composeFileName = $isLocal ? 'docker-compose.web-installer.yml' : 'docker-compose.yml';
$envFileName = $isLocal ? '.env.web-installer' : '.env';
$path = $isLocal ? '/usr/src/code' : '/usr/src/code/appwrite';
$composeFile = $path . '/' . $composeFileName;
if (file_exists($composeFile)) {
$command = array_map(escapeshellarg(...), [
'docker', 'compose',
'-f', $composeFile,
...($isLocal ? ['--project-name', 'appwrite'] : []),
'--project-directory', $path,
'down', '-v', '--remove-orphans',
]);
$output = [];
@exec(implode(' ', $command) . ' 2>&1', $output, $exitCode);
if ($exitCode !== 0) {
return 'Failed to stop containers: ' . trim(implode("\n", $output));
}
@unlink($composeFile);
}
$envFile = $path . '/' . $envFileName;
if (file_exists($envFile)) {
@unlink($envFile);
}
@unlink(Server::INSTALLER_CONFIG_FILE);
@unlink(Server::INSTALLER_LOCK_FILE);
$tempDir = sys_get_temp_dir();
foreach ((array) glob($tempDir . '/appwrite-install-*.json') as $file) {
@unlink($file);
}
return null;
}
}
@@ -28,6 +28,8 @@ class Status extends Action
public function action(string $installId, Response $response, State $state): void
{
$state->clearStaleLockIfNeeded();
$installId = $state->sanitizeInstallId($installId);
if ($installId === '') {
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
@@ -13,13 +13,15 @@ class State
private const string PATTERN_IPV6_WITH_PORT = '/^\[(.+)](?::(\d+))?$/';
private const int CONFIG_FILE_PERMISSION = 0600;
private const int GLOBAL_LOCK_TIMEOUT_SECONDS = 3600;
private const int GLOBAL_LOCK_TIMEOUT_SECONDS = 300;
private const int STALE_LOCK_CHECK_INTERVAL_SECONDS = 30;
private const int PORT_MIN = 1;
private const int PORT_MAX = 65535;
private array $paths;
private bool $bootstrapped = false;
private int $lastStaleLockClearAt = 0;
public function __construct(array $paths)
{
@@ -254,6 +256,16 @@ class State
}
}
public function clearStaleLockIfNeeded(): void
{
$now = time();
if ($now - $this->lastStaleLockClearAt < self::STALE_LOCK_CHECK_INTERVAL_SECONDS) {
return;
}
$this->lastStaleLockClearAt = $now;
$this->clearStaleLock();
}
public function reserveGlobalLock(string $installId): string
{
return (string) $this->withGlobalLock(function ($handle, $lock) use ($installId) {
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Installer\Services;
use Appwrite\Platform\Installer\Http\Installer\Certificate\Get as CertificateGet;
use Appwrite\Platform\Installer\Http\Installer\Complete;
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;
@@ -22,6 +23,7 @@ class Http extends Service
$this->addAction(Validate::getName(), new Validate());
$this->addAction(Complete::getName(), new Complete());
$this->addAction(Shutdown::getName(), new Shutdown());
$this->addAction(Reset::getName(), new Reset());
$this->addAction(Install::getName(), new Install());
$this->addAction(CertificateGet::getName(), new CertificateGet());
}
+82 -10
View File
@@ -599,7 +599,7 @@ class Install extends Action
if (!$noStart && $startIndex <= 2) {
$currentStep = InstallerServer::STEP_DOCKER_CONTAINERS;
$this->updateProgress($progress, InstallerServer::STEP_DOCKER_CONTAINERS, InstallerServer::STATUS_IN_PROGRESS, $messages);
$this->runDockerCompose($input, $isLocalInstall, $useExistingConfig, $isCLI);
$this->runDockerCompose($input, $isLocalInstall, $useExistingConfig, $isCLI, $progress, $isUpgrade);
if (!$isLocalInstall) {
$this->connectInstallerToAppwriteNetwork();
@@ -732,6 +732,8 @@ class Install extends Action
$name = $account['name'] ?? 'Admin';
$email = $account['email'] ?? 'admin@selfhosted.local';
$hostIp = gethostbyname($domain);
$payload = [
'action' => $type,
'account' => 'self-hosted',
@@ -744,6 +746,11 @@ class Install extends Action
'email' => $email,
'domain' => $domain,
'database' => $database,
'hostIp' => $hostIp !== $domain ? $hostIp : null,
'os' => php_uname('s') . ' ' . php_uname('r'),
'arch' => php_uname('m'),
'cpus' => ((int) trim((string) \shell_exec('nproc'))) ?: null,
'ram' => (int) round(((float) trim((string) \shell_exec('grep MemTotal /proc/meminfo | awk \'{print $2}\''))) / 1024),
]),
];
@@ -776,12 +783,16 @@ class Install extends Action
$healthPath = '/v1/health/version';
// Local dev: reach Traefik via localhost on the host.
// Docker: reach Appwrite directly via Docker internal DNS (network connect is guaranteed).
$candidate = $isLocalInstall
? 'http://localhost:' . $httpPort . $healthPath
: self::APPWRITE_API_URL . $healthPath;
$candidates = [$candidate];
if ($isLocalInstall) {
$candidates = [
'http://localhost:' . $httpPort . $healthPath,
];
} else {
$candidates = [
self::APPWRITE_API_URL . $healthPath,
'http://host.docker.internal:' . $httpPort . $healthPath,
];
}
$lastErrors = [];
@@ -964,7 +975,7 @@ class Install extends Action
}
}
protected function runDockerCompose(array $input, bool $isLocalInstall, bool $useExistingConfig, bool $isCLI): void
protected function runDockerCompose(array $input, bool $isLocalInstall, bool $useExistingConfig, bool $isCLI, ?callable $progress = null, bool $isUpgrade = false): void
{
$env = '';
if (!$useExistingConfig) {
@@ -1004,8 +1015,16 @@ class Install extends Action
$command[] = '-d';
$command[] = '--remove-orphans';
$command[] = '--renew-anon-volumes';
$commandLine = $env . implode(' ', array_map(escapeshellarg(...), $command)) . ' 2>&1';
\exec($commandLine, $output, $exit);
$commandLine = $env . implode(' ', array_map(escapeshellarg(...), $command));
if ($progress) {
$totalServices = $this->countComposeServices($composeFile);
$result = $this->execWithContainerProgress($commandLine, $totalServices, $progress, $isUpgrade);
$output = $result['output'];
$exit = $result['exit'];
} else {
\exec($commandLine . ' 2>&1', $output, $exit);
}
if ($exit !== 0) {
$message = trim(implode("\n", $output));
@@ -1017,6 +1036,59 @@ class Install extends Action
}
}
private function countComposeServices(string $composeFile): int
{
$content = @file_get_contents($composeFile);
if ($content === false) {
return 0;
}
$count = preg_match_all('/^\s*container_name:/m', $content);
return $count !== false ? $count : 0;
}
private function execWithContainerProgress(string $commandLine, int $totalServices, callable $progress, bool $isUpgrade): array
{
$verb = $isUpgrade ? 'Restarting' : 'Starting';
$message = "$verb Docker containers...";
$started = 0;
$output = [];
$process = proc_open(
$commandLine . ' 2>&1',
[1 => ['pipe', 'w']],
$pipes
);
if (!is_resource($process)) {
return ['output' => [], 'exit' => 1];
}
while (($line = fgets($pipes[1])) !== false) {
$trimmed = rtrim($line, "\n\r");
$output[] = $trimmed;
if (str_contains($trimmed, 'Container') && (str_contains($trimmed, 'Started') || str_contains($trimmed, 'Running'))) {
$started++;
if ($totalServices > 0) {
try {
$progress(
InstallerServer::STEP_DOCKER_CONTAINERS,
InstallerServer::STATUS_IN_PROGRESS,
$message,
['containerStarted' => $started, 'containerTotal' => $totalServices]
);
} catch (\Throwable) {
}
}
}
}
fclose($pipes[1]);
$exit = proc_close($process);
return ['output' => $output, 'exit' => $exit];
}
protected function isLocalInstall(): bool
{
if ($this->isLocalInstall === null) {
@@ -5,6 +5,7 @@ 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;
@@ -41,12 +42,13 @@ class ModuleTest extends TestCase
$service = reset($services);
$actions = $service->getActions();
$this->assertCount(7, $actions);
$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);
}
@@ -109,6 +111,18 @@ class ModuleTest extends TestCase
$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');
@@ -207,6 +221,7 @@ class ModuleTest extends TestCase
$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());
}
@@ -222,6 +237,7 @@ class ModuleTest extends TestCase
$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']);
}
@@ -240,7 +256,7 @@ class ModuleTest extends TestCase
public function testPostRoutesUsePostMethod(): void
{
$postActions = ['installerValidate', 'installerComplete', 'installerShutdown', 'installerInstall'];
$postActions = ['installerValidate', 'installerComplete', 'installerShutdown', 'installerReset', 'installerInstall'];
foreach ($postActions as $name) {
$action = $this->getAction($name);
$this->assertEquals(