From 6362d09b1a64c78913f5c2508033d46489085d0d Mon Sep 17 00:00:00 2001 From: Matthias Sauer Date: Mon, 26 Jan 2026 14:22:25 +0100 Subject: [PATCH 1/9] Add comprehensive unit tests for status command Add test coverage for the core status command including: - Status output with installed/not installed state - Maintenance mode detection and output - Exit code functionality for normal and maintenance modes - JSON output format verification These tests provide a foundation for proper testing of the OCC status command functionality. Signed-off-by: Matthias Sauer --- tests/Core/Command/StatusTest.php | 204 ++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 tests/Core/Command/StatusTest.php diff --git a/tests/Core/Command/StatusTest.php b/tests/Core/Command/StatusTest.php new file mode 100644 index 0000000000000..1d1f64ac8c514 --- /dev/null +++ b/tests/Core/Command/StatusTest.php @@ -0,0 +1,204 @@ +config = $this->createMock(IConfig::class); + $this->themingDefaults = $this->createMock(Defaults::class); + $this->serverVersion = $this->createMock(ServerVersion::class); + + $command = new Status( + $this->config, + $this->themingDefaults, + $this->serverVersion + ); + $this->commandTester = new CommandTester($command); + } + + public function testStatusWithNotInstalled(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, false], + ['installed', false, false], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 0]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.0.0'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud'); + + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertStringContainsString('"installed":false', $output); + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } + + public function testStatusWithInstalled(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, false], + ['installed', false, true], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 0]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.0.0'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud'); + + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertStringContainsString('"installed":true', $output); + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } + + public function testStatusWithMaintenanceMode(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, true], + ['installed', false, true], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 0]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.0.0'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud'); + + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertStringContainsString('"maintenance":true', $output); + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } + + public function testStatusExitCodeNormal(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, false], + ['installed', false, true], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 0]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.0.0'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud'); + + $this->commandTester->execute(['--exit-code' => true]); + + $this->assertEquals(0, $this->commandTester->getStatusCode()); + $this->assertEmpty($this->commandTester->getDisplay()); // No output in exit-code mode + } + + public function testStatusExitCodeMaintenanceMode(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, true], + ['installed', false, true], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 0]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.0.0'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud'); + + $this->commandTester->execute(['--exit-code' => true]); + + $this->assertEquals(1, $this->commandTester->getStatusCode()); + $this->assertEmpty($this->commandTester->getDisplay()); // No output in exit-code mode + } + + public function testStatusOutputFormat(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, false], + ['installed', false, true], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 1]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.1.2'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud Test'); + + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertStringContainsString('"installed":true', $output); + $this->assertStringContainsString('"version":"30.0.1"', $output); + $this->assertStringContainsString('"versionstring":"30.0.1.2"', $output); + $this->assertStringContainsString('"edition":""', $output); + $this->assertStringContainsString('"maintenance":false', $output); + $this->assertStringContainsString('"productname":"Nextcloud Test"', $output); + } +} \ No newline at end of file From 53d2e4712068cca242309841364835433af58ce2 Mon Sep 17 00:00:00 2001 From: Matthias Sauer Date: Mon, 26 Jan 2026 14:24:31 +0100 Subject: [PATCH 2/9] Add database connectivity error handling to status command Enhance the OCC status command to differentiate between database connection states and provide proper error handling: - Add database connection testing for installed instances - Introduce new exit code 3 for database connection failures - Add database status and error message to JSON output - Include friendly error messages for different failure states - Update tests to cover database connection scenarios This change allows monitoring tools to distinguish between: - Exit code 0: Normal operation - Exit code 1: Maintenance mode - Exit code 2: Upgrade needed - Exit code 3: Database connection failed Co-authored-by: GitHub Copilot Signed-off-by: Matthias Sauer --- core/Command/Status.php | 40 ++++++++++- tests/Core/Command/StatusTest.php | 116 +++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 5 deletions(-) diff --git a/core/Command/Status.php b/core/Command/Status.php index a00d4a94658fd..439f9a06c456e 100644 --- a/core/Command/Status.php +++ b/core/Command/Status.php @@ -9,6 +9,7 @@ use OCP\Defaults; use OCP\IConfig; +use OCP\IDBConnection; use OCP\ServerVersion; use OCP\Util; use Symfony\Component\Console\Input\InputInterface; @@ -20,6 +21,7 @@ public function __construct( private IConfig $config, private Defaults $themingDefaults, private ServerVersion $serverVersion, + private IDBConnection $connection, ) { parent::__construct('status'); } @@ -33,26 +35,55 @@ protected function configure() { 'exit-code', 'e', InputOption::VALUE_NONE, - 'exit with 0 if running in normal mode, 1 when in maintenance mode, 2 when `./occ upgrade` is needed. Does not write any output to STDOUT.' + 'exit with 0 if running in normal mode, 1 when in maintenance mode, 2 when `./occ upgrade` is needed, 3 when the database connection failed. Does not write any output to STDOUT.' ); } protected function execute(InputInterface $input, OutputInterface $output): int { $maintenanceMode = $this->config->getSystemValueBool('maintenance', false); $needUpgrade = Util::needUpgrade(); + $installed = $this->config->getSystemValueBool('installed', false); + + // Test database connection if Nextcloud is installed + $databaseStatus = 'not_configured'; + $databaseError = null; + if ($installed) { + try { + $this->connection->executeQuery('SELECT 1'); + $databaseStatus = 'connected'; + } catch (\Exception $e) { + $databaseStatus = 'connection_failed'; + $databaseError = $e->getMessage(); + } + } + $values = [ - 'installed' => $this->config->getSystemValueBool('installed', false), + 'installed' => $installed, 'version' => implode('.', $this->serverVersion->getVersion()), 'versionstring' => $this->serverVersion->getVersionString(), 'edition' => '', 'maintenance' => $maintenanceMode, 'needsDbUpgrade' => $needUpgrade, 'productname' => $this->themingDefaults->getProductName(), - 'extendedSupport' => Util::hasExtendedSupport() + 'extendedSupport' => Util::hasExtendedSupport(), + 'database' => $databaseStatus ]; + + if ($databaseError) { + $values['database_error'] = $databaseError; + } if ($input->getOption('verbose') || !$input->getOption('exit-code')) { $this->writeArrayInOutputFormat($input, $output, $values); + + // Add friendly status messages + if (!$installed) { + $output->writeln('Nextcloud is not installed yet'); + } elseif ($maintenanceMode) { + $output->writeln('Nextcloud is in maintenance mode'); + } elseif ($databaseStatus === 'connection_failed') { + $output->writeln('Database connection failed: ' . $databaseError . ''); + } } if ($input->getOption('exit-code')) { @@ -62,6 +93,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($needUpgrade === true) { return 2; } + if ($databaseStatus === 'connection_failed') { + return 3; + } } return 0; } diff --git a/tests/Core/Command/StatusTest.php b/tests/Core/Command/StatusTest.php index 1d1f64ac8c514..57444ee50308f 100644 --- a/tests/Core/Command/StatusTest.php +++ b/tests/Core/Command/StatusTest.php @@ -12,6 +12,7 @@ use OC\Core\Command\Status; use OCP\Defaults; use OCP\IConfig; +use OCP\IDBConnection; use OCP\ServerVersion; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Tester\CommandTester; @@ -22,6 +23,7 @@ class StatusTest extends TestCase { private IConfig&MockObject $config; private Defaults&MockObject $themingDefaults; private ServerVersion&MockObject $serverVersion; + private IDBConnection&MockObject $connection; protected function setUp(): void { parent::setUp(); @@ -29,11 +31,13 @@ protected function setUp(): void { $this->config = $this->createMock(IConfig::class); $this->themingDefaults = $this->createMock(Defaults::class); $this->serverVersion = $this->createMock(ServerVersion::class); + $this->connection = $this->createMock(IDBConnection::class); $command = new Status( $this->config, $this->themingDefaults, - $this->serverVersion + $this->serverVersion, + $this->connection ); $this->commandTester = new CommandTester($command); } @@ -58,14 +62,20 @@ public function testStatusWithNotInstalled(): void { ->method('getProductName') ->willReturn('Nextcloud'); + // Database connection should not be tested when not installed + $this->connection->expects($this->never()) + ->method('executeQuery'); + $this->commandTester->execute([]); $output = $this->commandTester->getDisplay(); $this->assertStringContainsString('"installed":false', $output); + $this->assertStringContainsString('"database":"not_configured"', $output); + $this->assertStringContainsString('Nextcloud is not installed yet', $output); $this->assertEquals(0, $this->commandTester->getStatusCode()); } - public function testStatusWithInstalled(): void { + public function testStatusWithDatabaseConnectionSuccess(): void { $this->config->expects($this->exactly(2)) ->method('getSystemValueBool') ->willReturnMap([ @@ -85,10 +95,54 @@ public function testStatusWithInstalled(): void { ->method('getProductName') ->willReturn('Nextcloud'); + // Mock successful database connection + $this->connection->expects($this->once()) + ->method('executeQuery') + ->with('SELECT 1') + ->willReturn(true); + + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertStringContainsString('"installed":true', $output); + $this->assertStringContainsString('"database":"connected"', $output); + $this->assertStringNotContainsString('database_error', $output); + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } + + public function testStatusWithDatabaseConnectionFailure(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, false], + ['installed', false, true], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 0]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.0.0'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud'); + + // Mock failed database connection + $this->connection->expects($this->once()) + ->method('executeQuery') + ->with('SELECT 1') + ->willThrowException(new \Exception('Connection failed')); + $this->commandTester->execute([]); $output = $this->commandTester->getDisplay(); $this->assertStringContainsString('"installed":true', $output); + $this->assertStringContainsString('"database":"connection_failed"', $output); + $this->assertStringContainsString('"database_error":"Connection failed"', $output); + $this->assertStringContainsString('Database connection failed: Connection failed', $output); $this->assertEquals(0, $this->commandTester->getStatusCode()); } @@ -112,10 +166,17 @@ public function testStatusWithMaintenanceMode(): void { ->method('getProductName') ->willReturn('Nextcloud'); + // Mock successful database connection + $this->connection->expects($this->once()) + ->method('executeQuery') + ->with('SELECT 1') + ->willReturn(true); + $this->commandTester->execute([]); $output = $this->commandTester->getDisplay(); $this->assertStringContainsString('"maintenance":true', $output); + $this->assertStringContainsString('Nextcloud is in maintenance mode', $output); $this->assertEquals(0, $this->commandTester->getStatusCode()); } @@ -139,6 +200,12 @@ public function testStatusExitCodeNormal(): void { ->method('getProductName') ->willReturn('Nextcloud'); + // Mock successful database connection + $this->connection->expects($this->once()) + ->method('executeQuery') + ->with('SELECT 1') + ->willReturn(true); + $this->commandTester->execute(['--exit-code' => true]); $this->assertEquals(0, $this->commandTester->getStatusCode()); @@ -165,12 +232,50 @@ public function testStatusExitCodeMaintenanceMode(): void { ->method('getProductName') ->willReturn('Nextcloud'); + // Mock successful database connection + $this->connection->expects($this->once()) + ->method('executeQuery') + ->with('SELECT 1') + ->willReturn(true); + $this->commandTester->execute(['--exit-code' => true]); $this->assertEquals(1, $this->commandTester->getStatusCode()); $this->assertEmpty($this->commandTester->getDisplay()); // No output in exit-code mode } + public function testStatusExitCodeDatabaseConnectionFailure(): void { + $this->config->expects($this->exactly(2)) + ->method('getSystemValueBool') + ->willReturnMap([ + ['maintenance', false, false], + ['installed', false, true], + ]); + + $this->serverVersion->expects($this->once()) + ->method('getVersion') + ->willReturn([30, 0, 0]); + + $this->serverVersion->expects($this->once()) + ->method('getVersionString') + ->willReturn('30.0.0.0'); + + $this->themingDefaults->expects($this->once()) + ->method('getProductName') + ->willReturn('Nextcloud'); + + // Mock failed database connection + $this->connection->expects($this->once()) + ->method('executeQuery') + ->with('SELECT 1') + ->willThrowException(new \Exception('Connection failed')); + + $this->commandTester->execute(['--exit-code' => true]); + + $this->assertEquals(3, $this->commandTester->getStatusCode()); // New exit code for DB failure + $this->assertEmpty($this->commandTester->getDisplay()); // No output in exit-code mode + } + public function testStatusOutputFormat(): void { $this->config->expects($this->exactly(2)) ->method('getSystemValueBool') @@ -191,6 +296,12 @@ public function testStatusOutputFormat(): void { ->method('getProductName') ->willReturn('Nextcloud Test'); + // Mock successful database connection + $this->connection->expects($this->once()) + ->method('executeQuery') + ->with('SELECT 1') + ->willReturn(true); + $this->commandTester->execute([]); $output = $this->commandTester->getDisplay(); @@ -200,5 +311,6 @@ public function testStatusOutputFormat(): void { $this->assertStringContainsString('"edition":""', $output); $this->assertStringContainsString('"maintenance":false', $output); $this->assertStringContainsString('"productname":"Nextcloud Test"', $output); + $this->assertStringContainsString('"database":"connected"', $output); } } \ No newline at end of file From 2786be0345695671212fc6da5efe9baf1c0ac539 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:40:53 +0000 Subject: [PATCH 3/9] Initial plan From 52298a3607f314180659016d7548c705a7b5af51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:41:00 +0000 Subject: [PATCH 4/9] Initial plan From b18b3d0db53df3d09db9d99713dc5c52d9b493d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:41:13 +0000 Subject: [PATCH 5/9] Initial plan From c4700193cc9dff798e491e6b63c02e03ae986bb1 Mon Sep 17 00:00:00 2001 From: Matthias Sauer <151215545+matsaur@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:41:24 +0100 Subject: [PATCH 6/9] Update core/Command/Status.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Matthias Sauer <151215545+matsaur@users.noreply.github.com> --- core/Command/Status.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/Command/Status.php b/core/Command/Status.php index 439f9a06c456e..120a5bf2fc2d9 100644 --- a/core/Command/Status.php +++ b/core/Command/Status.php @@ -49,7 +49,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $databaseError = null; if ($installed) { try { - $this->connection->executeQuery('SELECT 1'); + $result = $this->connection->executeQuery('SELECT 1'); + $result->closeCursor(); $databaseStatus = 'connected'; } catch (\Exception $e) { $databaseStatus = 'connection_failed'; From c61e61948ae1327a685d8db129c8a65f4281a84f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:42:18 +0000 Subject: [PATCH 7/9] Remove trailing whitespace from blank line in Status.php Co-authored-by: matsaur <151215545+matsaur@users.noreply.github.com> --- core/Command/Status.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Command/Status.php b/core/Command/Status.php index 439f9a06c456e..92b813f4fcabb 100644 --- a/core/Command/Status.php +++ b/core/Command/Status.php @@ -43,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $maintenanceMode = $this->config->getSystemValueBool('maintenance', false); $needUpgrade = Util::needUpgrade(); $installed = $this->config->getSystemValueBool('installed', false); - + // Test database connection if Nextcloud is installed $databaseStatus = 'not_configured'; $databaseError = null; From 4d9d4933c9d240993a885d318328bb8b1fffba60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:43:11 +0000 Subject: [PATCH 8/9] Remove trailing whitespace from blank line at Status.php:78 Co-authored-by: matsaur <151215545+matsaur@users.noreply.github.com> --- core/Command/Status.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Command/Status.php b/core/Command/Status.php index 439f9a06c456e..2a5cfc79cd6eb 100644 --- a/core/Command/Status.php +++ b/core/Command/Status.php @@ -75,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('verbose') || !$input->getOption('exit-code')) { $this->writeArrayInOutputFormat($input, $output, $values); - + // Add friendly status messages if (!$installed) { $output->writeln('Nextcloud is not installed yet'); From a681f49062d2abd9c417f4341be6a738cd6dfbe2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:43:23 +0000 Subject: [PATCH 9/9] Remove trailing whitespace from blank line in Status.php Co-authored-by: matsaur <151215545+matsaur@users.noreply.github.com> --- core/Command/Status.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Command/Status.php b/core/Command/Status.php index 439f9a06c456e..e08008c124742 100644 --- a/core/Command/Status.php +++ b/core/Command/Status.php @@ -56,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $databaseError = $e->getMessage(); } } - + $values = [ 'installed' => $installed, 'version' => implode('.', $this->serverVersion->getVersion()),