From 8ff02b54c9454910cbd71828fa43226c0b84c741 Mon Sep 17 00:00:00 2001 From: Frank Karlitschek Date: Sat, 24 Oct 2026 22:35:15 +0200 Subject: [PATCH] perf(api): cache the database statistics for the api and the page The OCS info endpoint and the admin page's /data each ran about 25 database queries on every call: storage and user counts, ten share counts, eight active-user windows, and the database version and size, which on MySQL is SHOW TABLE STATUS and on PostgreSQL pg_database_size(). Monitoring systems scrape the endpoint every 15 to 60 seconds, so these queries ran thousands of times a day for values that change on a scale of minutes. StatisticsCache computes that block once and keeps it in the distributed cache under one key, which both callers read, so they never compute it twice. The block is stored as JSON with zero fractions preserved, so a float such as size_appdata_storage keeps its type on the Redis backend too and callers get exactly what a fresh computation returns. The lifetime comes from the new app config key statistics_cache_ttl: 300 seconds by default, clamped to 0..3600, with 0 turning the cache off. CPU, memory, PHP, PHP-FPM, OPcache and the web server stay live. The skipApps, skipUpdate and skipOs parts are outside the cached block and keep their own behaviour, so the cache needs no per-parameter keys. The statistics queries themselves are unchanged. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Frank Karlitschek --- README.md | 23 ++ lib/Config/ConfigLexicon.php | 8 + lib/Controller/ApiController.php | 19 +- lib/StaticData.php | 14 +- lib/StatisticsCache.php | 117 ++++++++++ tests/lib/ApiControllerTest.php | 49 ++--- tests/lib/StatisticsCacheDatabaseTest.php | 71 +++++++ tests/lib/StatisticsCacheTest.php | 246 ++++++++++++++++++++++ 8 files changed, 499 insertions(+), 48 deletions(-) create mode 100644 lib/StatisticsCache.php create mode 100644 tests/lib/StatisticsCacheDatabaseTest.php create mode 100644 tests/lib/StatisticsCacheTest.php diff --git a/README.md b/README.md index bd3b1faf..4fc88e20 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,13 @@ https:///ocs/v2.php/apps/serverinfo/api/v1/info - Use the url parameter `skipApps=true` to omit app updates (including available app updates will send an external request to the app store). - Use the url parameter `skipOs=false` to include the operating system status. +The values that come from database queries (`nextcloud.storage`, +`nextcloud.shares`, `server.database` and `activeUsers`) are cached for five +minutes by default, so they can be up to that old; see +[Caching of the database statistics](#caching-of-the-database-statistics). +CPU, memory, PHP, PHP-FPM and the web server are read on every request; app +and server updates and the operating system status are not part of this cache. + The operating system status in `server.os` holds counts and flags only, never package names, package or kernel versions, or advisories, so a leaked token does not reveal what is unpatched. Values that could not be determined are @@ -293,6 +300,22 @@ php occ serverinfo:update-storage-statistics -v --output=json_pretty } ``` +##### Caching of the database statistics + +The storage and user counts, share counts, active users and the database type, +version and size take about 25 database queries, on MySQL including +`SHOW TABLE STATUS` and on PostgreSQL `pg_database_size()`. The API and the +admin page share one copy of them in the distributed memory cache, so a +monitoring system that polls every few seconds runs those queries at most once +per cache period. The values can therefore be up to that period old. + +The period is 300 seconds by default and at most 3600; 0 turns the cache off: + +``php occ config:app:set serverinfo statistics_cache_ttl --type=integer --value=60`` + +Without a memory cache (`memcache.local` or `memcache.distributed` in +`config.php`) nothing is cached. + ##### Restricted mode (>= Nextcloud 28) To obtain information about your server, the serverinfo app reads files outside the application directory (e.g. /proc on Linux) or executes shell commands (e.g. df on Linux). diff --git a/lib/Config/ConfigLexicon.php b/lib/Config/ConfigLexicon.php index 2a181d1c..be2dfc5c 100644 --- a/lib/Config/ConfigLexicon.php +++ b/lib/Config/ConfigLexicon.php @@ -28,6 +28,8 @@ class ConfigLexicon implements ILexicon { public const CACHED_DB_CHECK = 'cached_db_check'; + public const STATISTICS_CACHE_TTL = 'statistics_cache_ttl'; + public const CACHED_OS_FIRST_SEEN = 'cached_os_first_seen'; public const CACHED_OS_PHP_PROCESS = 'cached_os_php_process'; public const CACHED_OS_PHP_OUTDATED_SINCE = 'cached_os_php_outdated_since'; @@ -89,6 +91,12 @@ public function getAppConfigs(): array { 'Outcome of the last database check run, read by the setup check on Settings > Overview', lazy: true, ), + new Entry( + self::STATISTICS_CACHE_TTL, + ValueType::INT, + 300, + 'How long the database statistics of the monitoring API and the admin page are cached, in seconds; 0 turns the cache off, at most 3600', + ), new Entry( self::CACHED_OS_FIRST_SEEN, ValueType::ARRAY, diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 568b6de5..9d22c94e 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -9,14 +9,11 @@ namespace OCA\ServerInfo\Controller; -use OCA\ServerInfo\DatabaseStatistics; use OCA\ServerInfo\FpmStatistics; use OCA\ServerInfo\Os; use OCA\ServerInfo\OsStatus\OsStatusService; use OCA\ServerInfo\PhpStatistics; -use OCA\ServerInfo\SessionStatistics; -use OCA\ServerInfo\ShareStatistics; -use OCA\ServerInfo\StorageStatistics; +use OCA\ServerInfo\StatisticsCache; use OCA\ServerInfo\SystemStatistics; use OCA\ServerInfo\UptimeFormatter; use OCP\AppFramework\Http; @@ -36,12 +33,9 @@ public function __construct( private ?IUserSession $userSession, private Os $os, private SystemStatistics $systemStatistics, - private StorageStatistics $storageStatistics, + private StatisticsCache $statisticsCache, private PhpStatistics $phpStatistics, private FpmStatistics $fpmStatistics, - private DatabaseStatistics $databaseStatistics, - private ShareStatistics $shareStatistics, - private SessionStatistics $sessionStatistics, private UptimeFormatter $uptimeFormatter, private OsStatusService $osStatus, ) { @@ -88,19 +82,20 @@ public function info(bool $skipApps = true, bool $skipUpdate = true, bool $skipO $response->setStatus(Http::STATUS_UNAUTHORIZED); return $response; } + $statistics = $this->statisticsCache->get(); $data = [ 'nextcloud' => [ 'system' => $this->systemStatistics->getSystemStatistics($skipApps, $skipUpdate), - 'storage' => $this->storageStatistics->getStorageStatistics(), - 'shares' => $this->shareStatistics->getShareStatistics() + 'storage' => $statistics['storage'], + 'shares' => $statistics['shares'] ], 'server' => [ 'webserver' => $this->getWebserver(), 'php' => $this->phpStatistics->getPhpStatistics(), 'fpm' => $this->fpmStatistics->getFpmStatistics(), - 'database' => $this->databaseStatistics->getDatabaseStatistics() + 'database' => $statistics['database'] ], - 'activeUsers' => $this->sessionStatistics->getSessionStatistics() + 'activeUsers' => $statistics['activeUsers'] ]; if (!$skipOs) { $data['server']['os'] = $this->osStatus->monitoringSummary(); diff --git a/lib/StaticData.php b/lib/StaticData.php index 6e869b72..2aba405d 100644 --- a/lib/StaticData.php +++ b/lib/StaticData.php @@ -19,13 +19,10 @@ class StaticData { public function __construct( private Os $os, private IURLGenerator $urlGenerator, - private StorageStatistics $storageStatistics, + private StatisticsCache $statisticsCache, private Php $php, private Opcache $opcache, private FpmStatistics $fpmStatistics, - private DatabaseStatistics $databaseStatistics, - private ShareStatistics $shareStatistics, - private SessionStatistics $sessionStatistics, private SystemStatistics $systemStatistics, private IAppConfig $appConfig, private OsStatusService $osStatus, @@ -36,6 +33,7 @@ public function getData(): array { $diskinfo = $this->os->getDiskInfo(); $interfaces = $this->os->getNetworkInterfaces(); $memory = $this->os->getMemory(); + $statistics = $this->statisticsCache->get(); return [ 'hostname' => $this->os->getHostname(), @@ -62,13 +60,13 @@ public function getData(): array { 'loopback' => $i->isLoopback(), ], $interfaces), 'ocs' => $this->urlGenerator->getAbsoluteURL('ocs/v2.php/apps/serverinfo/api/v1/info'), - 'storage' => $this->storageStatistics->getStorageStatistics(), - 'shares' => $this->shareStatistics->getShareStatistics(), + 'storage' => $statistics['storage'], + 'shares' => $statistics['shares'], 'php' => $this->php->getData(), 'opcache' => $this->opcache->getData(), 'fpm' => $this->fpmStatistics->getFpmStatistics(), - 'database' => $this->databaseStatistics->getDatabaseStatistics(), - 'activeUsers' => $this->sessionStatistics->getSessionStatistics(), + 'database' => $statistics['database'], + 'activeUsers' => $statistics['activeUsers'], 'freeSpace' => $this->systemStatistics->getFreeSpace(), 'memTotal' => $memory->getMemTotal(), 'phpinfo' => $this->appConfig->getAppValueBool('phpinfo', false), diff --git a/lib/StatisticsCache.php b/lib/StatisticsCache.php new file mode 100644 index 00000000..7c38af35 --- /dev/null +++ b/lib/StatisticsCache.php @@ -0,0 +1,117 @@ +cache = $cacheFactory->createDistributed('serverinfo'); + } + + /** + * The statistics, at most ttl() seconds old. + * + * @return StatisticsBlock + */ + public function get(): array { + $ttl = $this->ttl(); + if ($ttl === 0) { + return $this->compute(); + } + + $cached = $this->decode($this->cache->get(self::CACHE_KEY)); + if ($cached !== null) { + return $cached; + } + + $statistics = $this->compute(); + try { + // Encoded here so that floats keep their type on every cache backend + $json = json_encode($statistics, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION); + $this->cache->set(self::CACHE_KEY, $json, $ttl); + } catch (\JsonException) { + // Served uncached: invalid UTF-8, for example in a database version string + } + return $statistics; + } + + /** + * How long the statistics are cached, in seconds; 0 when caching is off. + */ + public function ttl(): int { + $ttl = $this->appConfig->getAppValueInt(ConfigLexicon::STATISTICS_CACHE_TTL); + return max(0, min(self::MAX_TTL, $ttl)); + } + + /** + * @return StatisticsBlock + */ + private function compute(): array { + return [ + 'storage' => $this->storageStatistics->getStorageStatistics(), + 'shares' => $this->shareStatistics->getShareStatistics(), + 'database' => $this->databaseStatistics->getDatabaseStatistics(), + 'activeUsers' => $this->sessionStatistics->getSessionStatistics(), + ]; + } + + /** + * @return StatisticsBlock|null + */ + private function decode(mixed $json): ?array { + if (!is_string($json)) { + return null; + } + try { + $cached = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return null; + } + if (!is_array($cached)) { + return null; + } + foreach (self::BLOCKS as $block) { + if (!isset($cached[$block]) || !is_array($cached[$block])) { + return null; + } + } + /** @var StatisticsBlock $cached */ + return $cached; + } +} diff --git a/tests/lib/ApiControllerTest.php b/tests/lib/ApiControllerTest.php index 7635d2ae..f4b78b8d 100644 --- a/tests/lib/ApiControllerTest.php +++ b/tests/lib/ApiControllerTest.php @@ -10,14 +10,11 @@ namespace OCA\ServerInfo\Tests; use OCA\ServerInfo\Controller\ApiController; -use OCA\ServerInfo\DatabaseStatistics; use OCA\ServerInfo\FpmStatistics; use OCA\ServerInfo\Os; use OCA\ServerInfo\OsStatus\OsStatusService; use OCA\ServerInfo\PhpStatistics; -use OCA\ServerInfo\SessionStatistics; -use OCA\ServerInfo\ShareStatistics; -use OCA\ServerInfo\StorageStatistics; +use OCA\ServerInfo\StatisticsCache; use OCA\ServerInfo\SystemStatistics; use OCA\ServerInfo\UptimeFormatter; use OCP\AppFramework\Http; @@ -34,12 +31,9 @@ class ApiControllerTest extends \Test\TestCase { private IConfig&MockObject $config; private IGroupManager&MockObject $groupManager; private SystemStatistics&MockObject $systemStatistics; - private StorageStatistics&MockObject $storageStatistics; + private StatisticsCache&MockObject $statisticsCache; private PhpStatistics&MockObject $phpStatistics; private FpmStatistics&MockObject $fpmStatistics; - private DatabaseStatistics&MockObject $databaseStatistics; - private ShareStatistics&MockObject $shareStatistics; - private SessionStatistics&MockObject $sessionStatistics; private UptimeFormatter&MockObject $uptimeFormatter; private OsStatusService&MockObject $osStatus; @@ -51,12 +45,15 @@ protected function setUp(): void { $this->groupManager = $this->createMock(IGroupManager::class); $this->os = $this->createMock(Os::class); $this->systemStatistics = $this->createMock(SystemStatistics::class); - $this->storageStatistics = $this->createMock(StorageStatistics::class); + $this->statisticsCache = $this->createMock(StatisticsCache::class); + $this->statisticsCache->method('get')->willReturn([ + 'storage' => [], + 'shares' => [], + 'database' => [], + 'activeUsers' => [], + ]); $this->phpStatistics = $this->createMock(PhpStatistics::class); $this->fpmStatistics = $this->createMock(FpmStatistics::class); - $this->databaseStatistics = $this->createMock(DatabaseStatistics::class); - $this->shareStatistics = $this->createMock(ShareStatistics::class); - $this->sessionStatistics = $this->createMock(SessionStatistics::class); $this->uptimeFormatter = $this->createMock(UptimeFormatter::class); $this->osStatus = $this->createMock(OsStatusService::class); } @@ -70,18 +67,17 @@ private function getController($userSession) { $userSession, $this->os, $this->systemStatistics, - $this->storageStatistics, + $this->statisticsCache, $this->phpStatistics, $this->fpmStatistics, - $this->databaseStatistics, - $this->shareStatistics, - $this->sessionStatistics, $this->uptimeFormatter, $this->osStatus, ); } public function testAuthFailureNoSession() { + $this->statisticsCache->expects($this->never())->method('get'); + $response = $this->getController(null)->info(); $this->assertEquals(['message' => 'Unauthorized'], $response->getData()); @@ -188,24 +184,21 @@ public function testInfo() { $this->systemStatistics->expects($this->once()) ->method('getSystemStatistics') ->willReturn(['some_system' => 'some_value']); - $this->storageStatistics->expects($this->once()) - ->method('getStorageStatistics') - ->willReturn(['some_storage' => 'some_value']); - $this->shareStatistics->expects($this->once()) - ->method('getShareStatistics') - ->willReturn(['some_shares' => 'some_value']); $this->phpStatistics->expects($this->once()) ->method('getPhpStatistics') ->willReturn(['some_php' => 'some_value']); $this->fpmStatistics->expects($this->once()) ->method('getFpmStatistics') ->willReturn(['some_fpm' => 'some_value']); - $this->databaseStatistics->expects($this->once()) - ->method('getDatabaseStatistics') - ->willReturn(['some_database' => 'some_value']); - $this->sessionStatistics->expects($this->once()) - ->method('getSessionStatistics') - ->willReturn(['some_user' => 'some_value']); + $this->statisticsCache = $this->createMock(StatisticsCache::class); + $this->statisticsCache->expects($this->once()) + ->method('get') + ->willReturn([ + 'storage' => ['some_storage' => 'some_value'], + 'shares' => ['some_shares' => 'some_value'], + 'database' => ['some_database' => 'some_value'], + 'activeUsers' => ['some_user' => 'some_value'], + ]); $this->osStatus->expects($this->never())->method('monitoringSummary'); $response = $this->getController($userSession)->info(); diff --git a/tests/lib/StatisticsCacheDatabaseTest.php b/tests/lib/StatisticsCacheDatabaseTest.php new file mode 100644 index 00000000..9926ade5 --- /dev/null +++ b/tests/lib/StatisticsCacheDatabaseTest.php @@ -0,0 +1,71 @@ +createMock(ICache::class); + $cache->method('get') + ->willReturnCallback(function (string $key) use (&$stored) { + return isset($stored[$key]) ? json_decode($stored[$key], true) : null; + }); + $cache->expects($this->once()) + ->method('set') + ->willReturnCallback(function (string $key, mixed $value) use (&$stored): bool { + $stored[$key] = json_encode($value, JSON_THROW_ON_ERROR); + return true; + }); + $cacheFactory = $this->createMock(ICacheFactory::class); + $cacheFactory->method('createDistributed')->willReturn($cache); + $appConfig = $this->createMock(IAppConfig::class); + $appConfig->method('getAppValueInt')->willReturn(300); + + $statisticsCache = new StatisticsCache( + $storageStatistics, + $shareStatistics, + $sessionStatistics, + $databaseStatistics, + $appConfig, + $cacheFactory, + ); + + $computed = $statisticsCache->get(); + $cached = $statisticsCache->get(); + + $this->assertSame([ + 'storage' => $storageStatistics->getStorageStatistics(), + 'shares' => $shareStatistics->getShareStatistics(), + 'database' => $databaseStatistics->getDatabaseStatistics(), + 'activeUsers' => $sessionStatistics->getSessionStatistics(), + ], $computed); + $this->assertSame($computed, $cached); + } +} diff --git a/tests/lib/StatisticsCacheTest.php b/tests/lib/StatisticsCacheTest.php new file mode 100644 index 00000000..a1fc2a29 --- /dev/null +++ b/tests/lib/StatisticsCacheTest.php @@ -0,0 +1,246 @@ + 7, 'num_files' => 708860, 'size_appdata_storage' => 1024.0]; + private const SHARES = ['num_shares' => 1, 'permissions_3_1' => '1']; + private const DATABASE = ['type' => 'pgsql', 'version' => '16.4', 'size' => 331382784]; + private const ACTIVE_USERS = ['last5minutes' => 2, 'last1hour' => 3]; + + private StorageStatistics&MockObject $storageStatistics; + private ShareStatistics&MockObject $shareStatistics; + private SessionStatistics&MockObject $sessionStatistics; + private DatabaseStatistics&MockObject $databaseStatistics; + private IAppConfig&MockObject $appConfig; + private ICache&MockObject $cache; + private ICacheFactory&MockObject $cacheFactory; + /** @var array */ + private array $stored = []; + /** @var list */ + private array $storedTtls = []; + + protected function setUp(): void { + parent::setUp(); + + $this->storageStatistics = $this->createMock(StorageStatistics::class); + $this->storageStatistics->method('getStorageStatistics')->willReturn(self::STORAGE); + $this->shareStatistics = $this->createMock(ShareStatistics::class); + $this->shareStatistics->method('getShareStatistics')->willReturn(self::SHARES); + $this->sessionStatistics = $this->createMock(SessionStatistics::class); + $this->sessionStatistics->method('getSessionStatistics')->willReturn(self::ACTIVE_USERS); + $this->databaseStatistics = $this->createMock(DatabaseStatistics::class); + $this->databaseStatistics->method('getDatabaseStatistics')->willReturn(self::DATABASE); + $this->appConfig = $this->createMock(IAppConfig::class); + + // Stores values as JSON, the way the Redis cache does. + $this->cache = $this->createMock(ICache::class); + $this->cache->method('get') + ->willReturnCallback(fn (string $key) => isset($this->stored[$key]) ? json_decode($this->stored[$key], true) : null); + $this->cache->method('set') + ->willReturnCallback(function (string $key, mixed $value, int $ttl = 0): bool { + $this->stored[$key] = json_encode($value, JSON_THROW_ON_ERROR); + $this->storedTtls[] = $ttl; + return true; + }); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->cacheFactory->method('createDistributed')->with('serverinfo')->willReturn($this->cache); + } + + private function statisticsCache(int $ttl = 300): StatisticsCache { + $this->appConfig->method('getAppValueInt') + ->with(ConfigLexicon::STATISTICS_CACHE_TTL) + ->willReturn($ttl); + + return new StatisticsCache( + $this->storageStatistics, + $this->shareStatistics, + $this->sessionStatistics, + $this->databaseStatistics, + $this->appConfig, + $this->cacheFactory, + ); + } + + private function expected(): array { + return [ + 'storage' => self::STORAGE, + 'shares' => self::SHARES, + 'database' => self::DATABASE, + 'activeUsers' => self::ACTIVE_USERS, + ]; + } + + public function testMissComputesAndStoresForTheTtl(): void { + $this->storageStatistics->expects($this->once())->method('getStorageStatistics'); + $this->shareStatistics->expects($this->once())->method('getShareStatistics'); + $this->sessionStatistics->expects($this->once())->method('getSessionStatistics'); + $this->databaseStatistics->expects($this->once())->method('getDatabaseStatistics'); + + $this->assertSame($this->expected(), $this->statisticsCache(120)->get()); + $this->assertSame([120], $this->storedTtls); + } + + public function testHitServesTheSameValuesWithoutQuerying(): void { + $this->storageStatistics->expects($this->once())->method('getStorageStatistics'); + $this->shareStatistics->expects($this->once())->method('getShareStatistics'); + $this->sessionStatistics->expects($this->once())->method('getSessionStatistics'); + $this->databaseStatistics->expects($this->once())->method('getDatabaseStatistics'); + $statisticsCache = $this->statisticsCache(); + + $first = $statisticsCache->get(); + $second = $statisticsCache->get(); + + $this->assertSame($first, $second); + $this->assertSame($this->expected(), $second); + } + + public function testZeroTtlTurnsTheCacheOff(): void { + $this->storageStatistics->expects($this->exactly(2))->method('getStorageStatistics'); + $this->databaseStatistics->expects($this->exactly(2))->method('getDatabaseStatistics'); + $this->cache->expects($this->never())->method('get'); + $this->cache->expects($this->never())->method('set'); + $statisticsCache = $this->statisticsCache(0); + + $this->assertSame($this->expected(), $statisticsCache->get()); + $this->assertSame($this->expected(), $statisticsCache->get()); + } + + public static function dataTtl(): array { + return [ + 'default' => [300, 300], + 'off' => [0, 0], + 'negative' => [-5, 0], + 'longest' => [StatisticsCache::MAX_TTL, StatisticsCache::MAX_TTL], + 'too long' => [86400, StatisticsCache::MAX_TTL], + ]; + } + + #[DataProvider('dataTtl')] + public function testTtlIsClamped(int $configured, int $expected): void { + $this->assertSame($expected, $this->statisticsCache($configured)->ttl()); + } + + public function testNegativeTtlTurnsTheCacheOff(): void { + $this->cache->expects($this->never())->method('set'); + + $this->assertSame($this->expected(), $this->statisticsCache(-1)->get()); + } + + public static function dataInvalidEntry(): array { + $complete = ['storage' => [], 'shares' => [], 'database' => [], 'activeUsers' => []]; + return [ + 'not a string' => [$complete], + 'not JSON' => ['{"storage":'], + 'not an object' => ['"stale"'], + 'block missing' => [json_encode(['storage' => [], 'shares' => [], 'database' => []])], + 'block not an array' => [json_encode(['activeUsers' => 3] + $complete)], + ]; + } + + #[DataProvider('dataInvalidEntry')] + public function testInvalidEntryIsRecomputed(mixed $entry): void { + $this->stored['statistics'] = json_encode($entry, JSON_THROW_ON_ERROR); + $this->storageStatistics->expects($this->once())->method('getStorageStatistics'); + + $this->assertSame($this->expected(), $this->statisticsCache()->get()); + $this->assertSame($this->expected(), json_decode(json_decode($this->stored['statistics'], true), true)); + } + + public function testUnencodableStatisticsAreServedUncached(): void { + $database = ['type' => 'mysql', 'version' => "\xB1\x31", 'size' => 1]; + $this->databaseStatistics = $this->createMock(DatabaseStatistics::class); + $this->databaseStatistics->expects($this->exactly(2)) + ->method('getDatabaseStatistics') + ->willReturn($database); + $this->cache->expects($this->never())->method('set'); + $statisticsCache = $this->statisticsCache(); + + $this->assertSame($database, $statisticsCache->get()['database']); + $this->assertSame($database, $statisticsCache->get()['database']); + } + + public function testApiAndPageShareOneEntry(): void { + $this->storageStatistics->expects($this->once())->method('getStorageStatistics'); + $this->shareStatistics->expects($this->once())->method('getShareStatistics'); + $this->sessionStatistics->expects($this->once())->method('getSessionStatistics'); + $this->databaseStatistics->expects($this->once())->method('getDatabaseStatistics'); + $statisticsCache = $this->statisticsCache(); + + $request = $this->createMock(IRequest::class); + $request->method('getHeader')->with('NC-Token')->willReturn('token'); + $config = $this->createMock(IConfig::class); + $config->method('getAppValue')->with('serverinfo', 'token', '')->willReturn('token'); + $api = new ApiController( + 'serverinfo', + $request, + $config, + $this->createMock(IGroupManager::class), + null, + $this->createMock(Os::class), + $this->createMock(SystemStatistics::class), + $statisticsCache, + $this->createMock(PhpStatistics::class), + $this->createMock(FpmStatistics::class), + $this->createMock(UptimeFormatter::class), + $this->createMock(OsStatusService::class), + ); + $page = new StaticData( + $this->createMock(Os::class), + $this->createMock(IURLGenerator::class), + $statisticsCache, + $this->createMock(Php::class), + $this->createMock(Opcache::class), + $this->createMock(FpmStatistics::class), + $this->createMock(SystemStatistics::class), + $this->createMock(IAppConfig::class), + $this->createMock(OsStatusService::class), + ); + + $info = $api->info()->getData(); + $data = $page->getData(); + + $this->assertSame(self::STORAGE, $info['nextcloud']['storage']); + $this->assertSame(self::SHARES, $info['nextcloud']['shares']); + $this->assertSame(self::DATABASE, $info['server']['database']); + $this->assertSame(self::ACTIVE_USERS, $info['activeUsers']); + $this->assertSame(self::STORAGE, $data['storage']); + $this->assertSame(self::SHARES, $data['shares']); + $this->assertSame(self::DATABASE, $data['database']); + $this->assertSame(self::ACTIVE_USERS, $data['activeUsers']); + } +}