diff --git a/web/app/Installers/Server/Applications/NodeJsInstaller.php b/web/app/Installers/Server/Applications/NodeJsInstaller.php index c055f71..e8b2d48 100644 --- a/web/app/Installers/Server/Applications/NodeJsInstaller.php +++ b/web/app/Installers/Server/Applications/NodeJsInstaller.php @@ -26,6 +26,14 @@ class NodeJsInstaller $commands[] = 'apt-get install -y nodejs' . $nodejsVersion; } + // Install Apache Passenger + $commands[] = 'curl https://oss-binaries.phusionpassenger.com/auto-software-signing-gpg-key.txt | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/phusion.gpg >/dev/null'; + $commands[] = "sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger jammy main > /etc/apt/sources.list.d/passenger.list'"; + $commands[] = 'apt-get update'; + $commands[] = 'sudo apt-get install -y libapache2-mod-passenger'; + $commands[] = 'sudo a2enmod passenger'; + $commands[] = 'sudo service apache2 restart'; + $shellFileContent = ''; foreach ($commands as $command) { $shellFileContent .= $command . PHP_EOL; diff --git a/web/app/Installers/Server/Applications/PythonInstaller.php b/web/app/Installers/Server/Applications/PythonInstaller.php index b3a9b02..517ba4e 100644 --- a/web/app/Installers/Server/Applications/PythonInstaller.php +++ b/web/app/Installers/Server/Applications/PythonInstaller.php @@ -29,6 +29,14 @@ class PythonInstaller $commands[] = 'apt-get install -y python' . $pythonVersion . '-wheel'; } + // Install Apache Passenger + $commands[] = 'curl https://oss-binaries.phusionpassenger.com/auto-software-signing-gpg-key.txt | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/phusion.gpg >/dev/null'; + $commands[] = "sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger jammy main > /etc/apt/sources.list.d/passenger.list'"; + $commands[] = 'apt-get update'; + $commands[] = 'sudo apt-get install -y libapache2-mod-passenger'; + $commands[] = 'sudo a2enmod passenger'; + $commands[] = 'sudo service apache2 restart'; + $shellFileContent = ''; foreach ($commands as $command) { $shellFileContent .= $command . PHP_EOL; diff --git a/web/app/Installers/Server/Applications/RubyInstaller.php b/web/app/Installers/Server/Applications/RubyInstaller.php index 21fc8c7..bef7053 100644 --- a/web/app/Installers/Server/Applications/RubyInstaller.php +++ b/web/app/Installers/Server/Applications/RubyInstaller.php @@ -27,6 +27,14 @@ class RubyInstaller $commands[] = 'apt-get install -y ruby' . $rubyVersion . '-bundler'; } + // Install Apache Passenger + $commands[] = 'curl https://oss-binaries.phusionpassenger.com/auto-software-signing-gpg-key.txt | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/phusion.gpg >/dev/null'; + $commands[] = "sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger jammy main > /etc/apt/sources.list.d/passenger.list'"; + $commands[] = 'apt-get update'; + $commands[] = 'sudo apt-get install -y libapache2-mod-passenger'; + $commands[] = 'sudo a2enmod passenger'; + $commands[] = 'sudo service apache2 restart'; + $shellFileContent = ''; foreach ($commands as $command) { $shellFileContent .= $command . PHP_EOL; diff --git a/web/tests/Unit/AHostingSubscriptionCreateTest.php b/web/tests/Unit/HostingSubscriptionCreateTest.php similarity index 99% rename from web/tests/Unit/AHostingSubscriptionCreateTest.php rename to web/tests/Unit/HostingSubscriptionCreateTest.php index 4864969..0dd22f8 100644 --- a/web/tests/Unit/AHostingSubscriptionCreateTest.php +++ b/web/tests/Unit/HostingSubscriptionCreateTest.php @@ -13,7 +13,7 @@ use Illuminate\Support\Str; use PHPUnit\Framework\TestCase; use Tests\Feature\Api\ActionTestCase; -class AHostingSubscriptionCreateTest extends ActionTestCase +class HostingSubscriptionCreateTest extends ActionTestCase { function test_route_contains_middleware() { diff --git a/web/tests/Unit/HostingSubscriptionWithNodeJSCreateTest.php b/web/tests/Unit/HostingSubscriptionWithNodeJSCreateTest.php new file mode 100644 index 0000000..2afb160 --- /dev/null +++ b/web/tests/Unit/HostingSubscriptionWithNodeJSCreateTest.php @@ -0,0 +1,227 @@ +assertRouteContainsMiddleware( + 'api.hosting-subscriptions.index', + ApiKeyMiddleware::class + ); + + $this->assertRouteContainsMiddleware( + 'api.hosting-subscriptions.store', + ApiKeyMiddleware::class + ); + + $this->assertRouteContainsMiddleware( + 'api.hosting-subscriptions.update', + ApiKeyMiddleware::class + ); + + $this->assertRouteContainsMiddleware( + 'api.hosting-subscriptions.destroy', + ApiKeyMiddleware::class + ); + + } + + function test_create() + { + $this->assertTrue(Str::contains(php_uname(),'Ubuntu')); +// + // Make Apache+PHP Application Server with all supported php versions and modules + $installLogFilePath = storage_path('install-apache-nodejs-log-unit-test.txt'); + $phpInstaller = new NodeJsInstaller(); + $phpInstaller->setNodeJsVersions(array_keys(SupportedApplicationTypes::getNodeJsVersions())); + $phpInstaller->setLogFilePath($installLogFilePath); + $phpInstaller->install(); + + $installationSuccess = false; + for ($i = 1; $i <= 100; $i++) { + $logContent = file_get_contents($installLogFilePath); + if (str_contains($logContent, 'All packages installed successfully!')) { + $installationSuccess = true; + break; + } + sleep(3); + } + + if (!$installationSuccess) { + $logContent = file_get_contents($installLogFilePath); + $this->fail('Apache+NodeJS installation failed. Log: '.$logContent); + } + + $this->assertTrue($installationSuccess, 'Apache+NodeJS installation failed'); + + // Make unauthorized call + $callUnauthorizedResponse = $this->callRouteAction('api.hosting-subscriptions.store')->json(); + $this->assertArrayHasKey('error', $callUnauthorizedResponse); + $this->assertTrue($callUnauthorizedResponse['error'] == 'Unauthorized'); + + // Make authorized call without required parameters + $callStoreResponse = $this->callApiAuthorizedRouteAction('api.hosting-subscriptions.store')->json(); + + $this->assertArrayHasKey('message', $callStoreResponse); + $this->assertArrayHasKey('errors', $callStoreResponse); + + $this->assertIsString($callStoreResponse['message']); + $this->assertIsArray($callStoreResponse['errors']); + + // Create a customer + $randId = rand(1000, 9999); + $callCustomerStoreResponse = $this->callApiAuthorizedRouteAction( + 'api.customers.store', + [ + 'name' => 'Phyre Unit Test #'.$randId, + 'email' => 'unit-test-'.$randId.'@phyre.com', + ] + )->json(); + $this->assertArrayHasKey('status', $callCustomerStoreResponse); + $this->assertTrue($callCustomerStoreResponse['status'] == 'ok'); + + $this->assertArrayHasKey('message', $callCustomerStoreResponse); + $this->assertArrayHasKey('data', $callCustomerStoreResponse); + $this->assertArrayHasKey('customer', $callCustomerStoreResponse['data']); + $this->assertArrayHasKey('id', $callCustomerStoreResponse['data']['customer']); + $this->assertIsInt($callCustomerStoreResponse['data']['customer']['id']); + $customerId = $callCustomerStoreResponse['data']['customer']['id']; + + // Create a hosting subscription + $randId = rand(1000, 9999); + $hostingPlanId = null; + + $createHostingPlan = new HostingPlan(); + $createHostingPlan->name = 'Phyre Unit Test #'.$randId; + $createHostingPlan->description = 'Unit Test Hosting Plan'; + $createHostingPlan->disk_space = 1000; + $createHostingPlan->bandwidth = 1000; + $createHostingPlan->default_server_application_type = 'apache_nodejs'; + $createHostingPlan->default_server_application_settings = [ + 'nodejs_version' => '20', + ]; + $createHostingPlan->save(); + $hostingPlanId = $createHostingPlan->id; + + + $hostingSubscriptionDomain = 'phyre-unit-test-'.$randId.'.com'; + $callHostingSubscriptionStoreResponse = $this->callApiAuthorizedRouteAction( + 'api.hosting-subscriptions.store', + [ + 'customer_id' => $customerId, + 'hosting_plan_id'=> $hostingPlanId, + 'domain' => $hostingSubscriptionDomain, + ] + )->json(); + + $this->assertArrayHasKey('status', $callHostingSubscriptionStoreResponse); + $this->assertTrue($callHostingSubscriptionStoreResponse['status'] == 'ok'); + + $this->assertArrayHasKey('message', $callHostingSubscriptionStoreResponse); + $this->assertArrayHasKey('data', $callHostingSubscriptionStoreResponse); + $this->assertArrayHasKey('hostingSubscription', $callHostingSubscriptionStoreResponse['data']); + + $this->assertArrayHasKey('id', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + $this->assertIsInt($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['id']); + + $this->assertArrayHasKey('customer_id', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + $this->assertIsInt($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['customer_id']); + $this->assertTrue($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['customer_id'] == $customerId); + + $this->assertArrayHasKey('hosting_plan_id', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + $this->assertIsInt($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['hosting_plan_id']); + $this->assertTrue($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['hosting_plan_id'] == $hostingPlanId); + + $this->assertArrayHasKey('domain', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + + $this->assertIsString($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['domain']); + $this->assertTrue($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['domain'] == $hostingSubscriptionDomain); + + $hostingSubscriptionData = $callHostingSubscriptionStoreResponse['data']['hostingSubscription']; + + // Get domain details + $callDomainDetailsResponse = $this->callApiAuthorizedRouteAction( + 'api.domains.index', + [ + 'domain' => $hostingSubscriptionDomain, + ] + )->json(); + $callDomainDetailsResponseData = $callDomainDetailsResponse['data']['domains']; + $this->assertIsArray($callDomainDetailsResponseData); + $this->assertNotEmpty($callDomainDetailsResponseData); + $this->assertArrayHasKey('id', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('domain', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('hosting_subscription_id', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('status', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('created_at', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('updated_at', $callDomainDetailsResponseData[0]); + $this->assertTrue($callDomainDetailsResponseData[0]['domain'] == $hostingSubscriptionDomain); + $this->assertTrue($callDomainDetailsResponseData[0]['hosting_subscription_id'] == $hostingSubscriptionData['id']); + $this->assertTrue($callDomainDetailsResponseData[0]['status'] == Domain::STATUS_ACTIVE); + $this->assertTrue($callDomainDetailsResponseData[0]['is_main'] == 1); + $domainData = $callDomainDetailsResponseData[0]; + + // Check virtual host is created + $virtualHostFile = '/etc/apache2/apache2.conf'; + $this->assertFileExists($virtualHostFile); + $virtualHostFileContent = file_get_contents($virtualHostFile); + + + $this->assertStringContainsString('ServerName '.$hostingSubscriptionDomain, $virtualHostFileContent); + //$this->assertStringContainsString('ServerAlias www.'.$hostingSubscriptionDomain, $virtualHostFileContent); + + $this->assertStringContainsString('Directory '.$domainData['domain_public'], $virtualHostFileContent); + $this->assertStringContainsString('DocumentRoot '.$domainData['domain_public'], $virtualHostFileContent); + + // Check virtual host is enabled + $this->assertFileExists('/etc/apache2/apache2.conf'); + + // Check apache config is valid + shell_exec('apachectl -t >> /tmp/apache_config_check.txt 2>&1'); + $apacheConfigTest = file_get_contents('/tmp/apache_config_check.txt'); + unlink('/tmp/apache_config_check.txt'); + + $this->assertTrue(Str::contains($apacheConfigTest,'Syntax OK')); + + // Check domain is accessible +// shell_exec('sudo echo "0.0.0.0 '.$hostingSubscriptionDomain.'" | sudo tee -a /etc/hosts'); +// +// $domainAccess = shell_exec('curl -s -o /dev/null -w "%{http_code}" http://'.$hostingSubscriptionDomain); +// $this->assertTrue($domainAccess == 200); +// +// $indexPageContent = shell_exec('curl -s http://'.$hostingSubscriptionDomain); +// +// $this->assertTrue(Str::contains($indexPageContent,'Phyre Panel - PHP App')); + + + // Check hosting subscription local database creation + $newDatabase = new Database(); + $newDatabase->hosting_subscription_id = $hostingSubscriptionData['id']; + $newDatabase->is_remote_database_server = 0; + $newDatabase->database_name = 'ppdb'.$randId; + $newDatabase->save(); + + $newDatabaseUser = new DatabaseUser(); + $newDatabaseUser->database_id = $newDatabase->id; + $newDatabaseUser->username = 'pput'.$randId; + $newDatabaseUser->password = Str::password(24); + $newDatabaseUser->save(); + } + +} diff --git a/web/tests/Unit/HostingSubscriptionWithPythonCreateTest.php b/web/tests/Unit/HostingSubscriptionWithPythonCreateTest.php new file mode 100644 index 0000000..6adc72a --- /dev/null +++ b/web/tests/Unit/HostingSubscriptionWithPythonCreateTest.php @@ -0,0 +1,226 @@ +assertRouteContainsMiddleware( + 'api.hosting-subscriptions.index', + ApiKeyMiddleware::class + ); + + $this->assertRouteContainsMiddleware( + 'api.hosting-subscriptions.store', + ApiKeyMiddleware::class + ); + + $this->assertRouteContainsMiddleware( + 'api.hosting-subscriptions.update', + ApiKeyMiddleware::class + ); + + $this->assertRouteContainsMiddleware( + 'api.hosting-subscriptions.destroy', + ApiKeyMiddleware::class + ); + + } + + function test_create() + { + $this->assertTrue(Str::contains(php_uname(),'Ubuntu')); +// + // Make Apache+PHP Application Server with all supported php versions and modules + $installLogFilePath = storage_path('install-apache-python-log-unit-test.txt'); + $phpInstaller = new PythonInstaller(); + $phpInstaller->setPythonVersions(array_keys(SupportedApplicationTypes::getPythonVersions())); + $phpInstaller->setLogFilePath($installLogFilePath); + $phpInstaller->install(); + + $installationSuccess = false; + for ($i = 1; $i <= 100; $i++) { + $logContent = file_get_contents($installLogFilePath); + if (str_contains($logContent, 'All packages installed successfully!')) { + $installationSuccess = true; + break; + } + sleep(3); + } + + if (!$installationSuccess) { + $logContent = file_get_contents($installLogFilePath); + $this->fail('Apache+PHP installation failed. Log: '.$logContent); + } + + $this->assertTrue($installationSuccess, 'Apache+Python installation failed'); + + // Make unauthorized call + $callUnauthorizedResponse = $this->callRouteAction('api.hosting-subscriptions.store')->json(); + $this->assertArrayHasKey('error', $callUnauthorizedResponse); + $this->assertTrue($callUnauthorizedResponse['error'] == 'Unauthorized'); + + // Make authorized call without required parameters + $callStoreResponse = $this->callApiAuthorizedRouteAction('api.hosting-subscriptions.store')->json(); + + $this->assertArrayHasKey('message', $callStoreResponse); + $this->assertArrayHasKey('errors', $callStoreResponse); + + $this->assertIsString($callStoreResponse['message']); + $this->assertIsArray($callStoreResponse['errors']); + + // Create a customer + $randId = rand(1000, 9999); + $callCustomerStoreResponse = $this->callApiAuthorizedRouteAction( + 'api.customers.store', + [ + 'name' => 'Phyre Unit Test #'.$randId, + 'email' => 'unit-test-'.$randId.'@phyre.com', + ] + )->json(); + $this->assertArrayHasKey('status', $callCustomerStoreResponse); + $this->assertTrue($callCustomerStoreResponse['status'] == 'ok'); + + $this->assertArrayHasKey('message', $callCustomerStoreResponse); + $this->assertArrayHasKey('data', $callCustomerStoreResponse); + $this->assertArrayHasKey('customer', $callCustomerStoreResponse['data']); + $this->assertArrayHasKey('id', $callCustomerStoreResponse['data']['customer']); + $this->assertIsInt($callCustomerStoreResponse['data']['customer']['id']); + $customerId = $callCustomerStoreResponse['data']['customer']['id']; + + // Create a hosting subscription + $randId = rand(1000, 9999); + $hostingPlanId = null; + + $createHostingPlan = new HostingPlan(); + $createHostingPlan->name = 'Phyre Unit Test #'.$randId; + $createHostingPlan->description = 'Unit Test Hosting Plan'; + $createHostingPlan->disk_space = 1000; + $createHostingPlan->bandwidth = 1000; + $createHostingPlan->default_server_application_type = 'apache_python'; + $createHostingPlan->default_server_application_settings = [ + 'python_version' => '3.9', + ]; + $createHostingPlan->save(); + $hostingPlanId = $createHostingPlan->id; + + + $hostingSubscriptionDomain = 'phyre-unit-test-'.$randId.'.com'; + $callHostingSubscriptionStoreResponse = $this->callApiAuthorizedRouteAction( + 'api.hosting-subscriptions.store', + [ + 'customer_id' => $customerId, + 'hosting_plan_id'=> $hostingPlanId, + 'domain' => $hostingSubscriptionDomain, + ] + )->json(); + + $this->assertArrayHasKey('status', $callHostingSubscriptionStoreResponse); + $this->assertTrue($callHostingSubscriptionStoreResponse['status'] == 'ok'); + + $this->assertArrayHasKey('message', $callHostingSubscriptionStoreResponse); + $this->assertArrayHasKey('data', $callHostingSubscriptionStoreResponse); + $this->assertArrayHasKey('hostingSubscription', $callHostingSubscriptionStoreResponse['data']); + + $this->assertArrayHasKey('id', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + $this->assertIsInt($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['id']); + + $this->assertArrayHasKey('customer_id', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + $this->assertIsInt($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['customer_id']); + $this->assertTrue($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['customer_id'] == $customerId); + + $this->assertArrayHasKey('hosting_plan_id', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + $this->assertIsInt($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['hosting_plan_id']); + $this->assertTrue($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['hosting_plan_id'] == $hostingPlanId); + + $this->assertArrayHasKey('domain', $callHostingSubscriptionStoreResponse['data']['hostingSubscription']); + + $this->assertIsString($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['domain']); + $this->assertTrue($callHostingSubscriptionStoreResponse['data']['hostingSubscription']['domain'] == $hostingSubscriptionDomain); + + $hostingSubscriptionData = $callHostingSubscriptionStoreResponse['data']['hostingSubscription']; + + // Get domain details + $callDomainDetailsResponse = $this->callApiAuthorizedRouteAction( + 'api.domains.index', + [ + 'domain' => $hostingSubscriptionDomain, + ] + )->json(); + $callDomainDetailsResponseData = $callDomainDetailsResponse['data']['domains']; + $this->assertIsArray($callDomainDetailsResponseData); + $this->assertNotEmpty($callDomainDetailsResponseData); + $this->assertArrayHasKey('id', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('domain', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('hosting_subscription_id', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('status', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('created_at', $callDomainDetailsResponseData[0]); + $this->assertArrayHasKey('updated_at', $callDomainDetailsResponseData[0]); + $this->assertTrue($callDomainDetailsResponseData[0]['domain'] == $hostingSubscriptionDomain); + $this->assertTrue($callDomainDetailsResponseData[0]['hosting_subscription_id'] == $hostingSubscriptionData['id']); + $this->assertTrue($callDomainDetailsResponseData[0]['status'] == Domain::STATUS_ACTIVE); + $this->assertTrue($callDomainDetailsResponseData[0]['is_main'] == 1); + $domainData = $callDomainDetailsResponseData[0]; + + // Check virtual host is created + $virtualHostFile = '/etc/apache2/apache2.conf'; + $this->assertFileExists($virtualHostFile); + $virtualHostFileContent = file_get_contents($virtualHostFile); + + + $this->assertStringContainsString('ServerName '.$hostingSubscriptionDomain, $virtualHostFileContent); + //$this->assertStringContainsString('ServerAlias www.'.$hostingSubscriptionDomain, $virtualHostFileContent); + + $this->assertStringContainsString('Directory '.$domainData['domain_public'], $virtualHostFileContent); + $this->assertStringContainsString('DocumentRoot '.$domainData['domain_public'], $virtualHostFileContent); + + // Check virtual host is enabled + $this->assertFileExists('/etc/apache2/apache2.conf'); + + // Check apache config is valid + shell_exec('apachectl -t >> /tmp/apache_config_check.txt 2>&1'); + $apacheConfigTest = file_get_contents('/tmp/apache_config_check.txt'); + unlink('/tmp/apache_config_check.txt'); + + $this->assertTrue(Str::contains($apacheConfigTest,'Syntax OK')); + + // Check domain is accessible +// shell_exec('sudo echo "0.0.0.0 '.$hostingSubscriptionDomain.'" | sudo tee -a /etc/hosts'); +// +// $domainAccess = shell_exec('curl -s -o /dev/null -w "%{http_code}" http://'.$hostingSubscriptionDomain); +// $this->assertTrue($domainAccess == 200); +// +// $indexPageContent = shell_exec('curl -s http://'.$hostingSubscriptionDomain); +// +// $this->assertTrue(Str::contains($indexPageContent,'Phyre Panel - PHP App')); + + + // Check hosting subscription local database creation + $newDatabase = new Database(); + $newDatabase->hosting_subscription_id = $hostingSubscriptionData['id']; + $newDatabase->is_remote_database_server = 0; + $newDatabase->database_name = 'ppdb'.$randId; + $newDatabase->save(); + + $newDatabaseUser = new DatabaseUser(); + $newDatabaseUser->database_id = $newDatabase->id; + $newDatabaseUser->username = 'pput'.$randId; + $newDatabaseUser->password = Str::password(24); + $newDatabaseUser->save(); + } + +} diff --git a/web/tests/Unit/BackupTest.php b/web/tests/Unit/ZeroBackupTest.php similarity index 99% rename from web/tests/Unit/BackupTest.php rename to web/tests/Unit/ZeroBackupTest.php index b00473e..ed49a0d 100644 --- a/web/tests/Unit/BackupTest.php +++ b/web/tests/Unit/ZeroBackupTest.php @@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Storage; use Tests\Feature\Api\ActionTestCase; -class BackupTest extends ActionTestCase +class ZeroBackupTest extends ActionTestCase { public function testFullBackup() {