Files
nextcloud-server-mirror/tests/lib/Files/NodeAttributes/NodeSerializerTest.php
John Molakvoæ 88526c69e0 refactor: track preloaded nodes in the node attribute registry
The serializer and the DAV plugin both preload through the registry,
which only passes the nodes a provider has not preloaded yet for the
same context and attributes. Providers no longer need to skip known
nodes or preload lazily from getAttributes().

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com>
2026-10-27 16:17:40 +01:00

176 lines
6.8 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\Files\NodeAttributes;
use NCU\Files\NodeAttributes\INodeAttributeProvider;
use NCU\Files\NodeAttributes\NodeAttributeContext;
use NCU\Files\NodeAttributes\NodeAttributeList;
use OC\Files\NodeAttributes\NodeAttributeRegistry;
use OC\Files\NodeAttributes\NodeSerializer;
use OCP\Files\File;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\IUserFolder;
use OCP\Files\Node;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Share\IShare;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
class NodeSerializerTest extends TestCase {
private const HAS_PREVIEW = '{http://nextcloud.org/ns}has-preview';
private const SHARE_TYPES = '{http://owncloud.org/ns}share-types';
private NodeAttributeRegistry&MockObject $registry;
private IRootFolder&MockObject $rootFolder;
private NodeSerializer $serializer;
private IUser&MockObject $user;
#[\Override]
protected function setUp(): void {
parent::setUp();
$this->registry = $this->createMock(NodeAttributeRegistry::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$urlGenerator = $this->createMock(IURLGenerator::class);
$urlGenerator->method('getAbsoluteURL')->willReturnCallback(static fn (string $url): string => 'https://cloud.example.com' . $url);
$this->user = $this->createMock(IUser::class);
$this->user->method('getUID')->willReturn('alice');
$userFolder = $this->createMock(IUserFolder::class);
$userFolder->method('getPath')->willReturn('/alice/files');
$this->rootFolder->method('getUserFolder')->with('alice')->willReturn($userFolder);
$this->serializer = new NodeSerializer($this->registry, $this->rootFolder, $urlGenerator, $this->createMock(LoggerInterface::class));
}
private function node(string $class, int $id, string $path, string $mime = 'text/plain'): Node&MockObject {
$node = $this->createMock($class);
$node->method('getId')->willReturn($id);
$node->method('getPath')->willReturn($path);
$node->method('getName')->willReturn(basename($path));
$node->method('getType')->willReturn($class === Folder::class ? FileInfo::TYPE_FOLDER : FileInfo::TYPE_FILE);
$node->method('getMimetype')->willReturn($mime);
$node->method('getMTime')->willReturn(1760000000);
$node->method('getCreationTime')->willReturn(0);
$node->method('getSize')->willReturn(42);
$node->method('getPermissions')->willReturn(31);
$node->method('getEtag')->willReturn('etag-' . $id);
$node->method('getOwner')->willReturn($this->user);
return $node;
}
public function testSerializeForUser(): void {
$file = $this->node(File::class, 11, '/alice/files/Photos/cat.jpg', 'image/jpeg');
$folder = $this->node(Folder::class, 12, '/alice/files/Photos', 'httpd/unix-directory');
$provider = $this->createMock(INodeAttributeProvider::class);
$this->registry->method('getAttributeNames')->willReturn([self::HAS_PREVIEW, self::SHARE_TYPES]);
$this->registry->method('getProvidersFor')
->with([self::HAS_PREVIEW, self::SHARE_TYPES])
->willReturn([['provider' => $provider, 'attributes' => [self::HAS_PREVIEW, self::SHARE_TYPES]]]);
// Everything is preloaded at once, before reading any node
$this->registry->expects(self::once())
->method('preload')
->with(self::isInstanceOf(NodeAttributeContext::class), [$file, $folder], [self::HAS_PREVIEW, self::SHARE_TYPES]);
$provider->expects(self::never())->method('preload');
$provider->method('getAttributes')->willReturnCallback(static fn (NodeAttributeContext $context, Node $node): array => [
self::HAS_PREVIEW => $node === $file,
self::SHARE_TYPES => new NodeAttributeList('share-type', [0, 3]),
'{DAV:}not-requested' => 'ignored',
]);
$result = $this->serializer->serialize(NodeAttributeContext::forUser($this->user), [$file, $folder]);
self::assertSame([
[
'id' => 11,
'source' => 'https://cloud.example.com/remote.php/dav/files/alice/Photos/cat.jpg',
'root' => '/files/alice',
'owner' => 'alice',
'mime' => 'image/jpeg',
'mtime' => 1760000000,
'crtime' => null,
'size' => 42,
'permissions' => 31,
'displayname' => 'cat.jpg',
'type' => 'file',
'attributes' => [
'etag' => 'etag-11',
'has-preview' => true,
'share-types' => ['share-type' => [0, 3]],
],
],
[
'id' => 12,
'source' => 'https://cloud.example.com/remote.php/dav/files/alice/Photos',
'root' => '/files/alice',
'owner' => 'alice',
'mime' => 'httpd/unix-directory',
'mtime' => 1760000000,
'crtime' => null,
'size' => 42,
'permissions' => 31,
'displayname' => 'Photos',
'type' => 'folder',
'attributes' => [
'etag' => 'etag-12',
'has-preview' => false,
'share-types' => ['share-type' => [0, 3]],
],
],
], $result);
}
public function testSerializeForShare(): void {
$sharedFolder = $this->node(Folder::class, 20, '/bob/files/Shared');
$file = $this->node(File::class, 21, '/bob/files/Shared/sub/doc.md');
$share = $this->createMock(IShare::class);
$share->method('getToken')->willReturn('AbCdEf');
$share->method('getNode')->willReturn($sharedFolder);
$this->registry->method('getAttributeNames')->willReturn([]);
$this->registry->method('getProvidersFor')->willReturn([]);
$result = $this->serializer->serialize(NodeAttributeContext::forShare($share), [$sharedFolder, $file]);
self::assertSame('/files/AbCdEf', $result[0]['root']);
self::assertSame('https://cloud.example.com/public.php/dav/files/AbCdEf', $result[0]['source']);
self::assertSame('https://cloud.example.com/public.php/dav/files/AbCdEf/sub/doc.md', $result[1]['source']);
}
public function testOnlyRequestedAttributes(): void {
$file = $this->node(File::class, 11, '/alice/files/cat.jpg');
$this->registry->expects(self::never())->method('getAttributeNames');
$this->registry->expects(self::once())
->method('getProvidersFor')
->with([self::HAS_PREVIEW])
->willReturn([]);
$result = $this->serializer->serialize(NodeAttributeContext::forUser($this->user), [$file], [self::HAS_PREVIEW]);
self::assertSame(['etag' => 'etag-11'], $result[0]['attributes']);
}
public function testNodesOutsideOfTheRootAreSkipped(): void {
$inside = $this->node(File::class, 11, '/alice/files/cat.jpg');
$outside = $this->node(File::class, 12, '/bob/files/cat.jpg');
$sibling = $this->node(File::class, 13, '/alice/files_trashbin/files/cat.jpg');
$this->registry->method('getAttributeNames')->willReturn([]);
$this->registry->method('getProvidersFor')->willReturn([]);
$result = $this->serializer->serialize(NodeAttributeContext::forUser($this->user), [$inside, $outside, $sibling]);
self::assertSame([11], array_column($result, 'id'));
}
}