From a09562b0bfc7f51c4ffeb593881ab5bf0af0c115 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 Signed-off-by: Daniel Kesselberg <3902676+kesselb@users.noreply.github.com> --- 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 8db1b7b32..8d2c1370a 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,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 @@ -307,6 +314,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 runs system commands (e.g. df on Linux) without a shell and with a time limit of a few seconds each. diff --git a/lib/Config/ConfigLexicon.php b/lib/Config/ConfigLexicon.php index 2a181d1c5..be2dfc5cf 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 568b6de5b..9d22c94e1 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 62dc8eea7..5dbe8f01c 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 SubscriptionStatistics $subscriptionStatistics, private SystemStatistics $systemStatistics, private IAppConfig $appConfig, @@ -37,6 +34,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(), @@ -63,13 +61,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'], 'subscriptionUsers' => $this->subscriptionStatistics->getSubscriptionUsers(), 'freeSpace' => $this->systemStatistics->getFreeSpace(), 'memTotal' => $memory->getMemTotal(), diff --git a/lib/StatisticsCache.php b/lib/StatisticsCache.php new file mode 100644 index 000000000..7c38af352 --- /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 7635d2ae3..f4b78b8db 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 000000000..9926ade51 --- /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 000000000..a1fc2a296 --- /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']); + } +}