mirror of
https://github.com/nextcloud/server.git
synced 2026-11-03 21:12:20 +01:00
248 lines
9.6 KiB
PHP
248 lines
9.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Files\Tests\Search;
|
|
|
|
use NCU\Search\AccountScopedSearchResult;
|
|
use OC\Files\Search\SearchComparison;
|
|
use OC\Files\Storage\Temporary;
|
|
use OC\Files\Storage\Wrapper\Jail;
|
|
use OCA\Files\Search\FileAccountScopedSearchProvider;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\Files\Search\ISearchComparison;
|
|
use OCP\FullTextSearch\IFullTextSearchManager;
|
|
use OCP\IAppConfig;
|
|
use OCP\IL10N;
|
|
use OCP\Server;
|
|
use OCP\Share\IManager as IShareManager;
|
|
use OCP\Share\IShare;
|
|
use OCP\SystemTag\ISystemTagManager;
|
|
use OCP\SystemTag\ISystemTagObjectMapper;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\Attributes\Group;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use Psr\Log\LoggerInterface;
|
|
use Test\TestCase;
|
|
use Test\Traits\MountProviderTrait;
|
|
use Test\Traits\UserTrait;
|
|
|
|
#[Group('DB')]
|
|
class FileAccountScopedSearchProviderTest extends TestCase {
|
|
use MountProviderTrait;
|
|
use UserTrait;
|
|
|
|
private string $userId;
|
|
private IShareManager&MockObject $shareManager;
|
|
private IFullTextSearchManager&MockObject $fullTextSearchManager;
|
|
private FileAccountScopedSearchProvider $provider;
|
|
|
|
#[\Override]
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->userId = $this->getUniqueID('user');
|
|
$this->createUser($this->userId, $this->userId);
|
|
|
|
$home = new Temporary([]);
|
|
$home->mkdir('Docs');
|
|
$home->file_put_contents('Docs/in-docs.txt', 'x');
|
|
$home->file_put_contents('root.txt', 'x');
|
|
$this->registerMount($this->userId, $home, '/' . $this->userId . '/files');
|
|
|
|
$external = new Temporary([]);
|
|
$external->file_put_contents('external.txt', 'x');
|
|
$this->registerMount($this->userId, $external, '/' . $this->userId . '/files/External');
|
|
|
|
$nested = new Temporary([]);
|
|
$nested->file_put_contents('nested.txt', 'x');
|
|
$this->registerMount($this->userId, $nested, '/' . $this->userId . '/files/Docs/Nested');
|
|
|
|
// A jail into a shared storage, as group folders and received shares are mounted.
|
|
$shared = new Temporary([]);
|
|
$shared->mkdir('__groupfolders');
|
|
$shared->mkdir('__groupfolders/1');
|
|
$shared->mkdir('__groupfolders/2');
|
|
$shared->file_put_contents('__groupfolders/1/team.txt', 'x');
|
|
$shared->file_put_contents('__groupfolders/2/other-team.txt', 'x');
|
|
$shared->getScanner()->scan('');
|
|
$this->registerMount($this->userId, new Jail(['storage' => $shared, 'root' => '__groupfolders/1']), '/' . $this->userId . '/files/Team');
|
|
|
|
$this->loginAsUser($this->userId);
|
|
|
|
$this->shareManager = $this->createMock(IShareManager::class);
|
|
$this->fullTextSearchManager = $this->createMock(IFullTextSearchManager::class);
|
|
|
|
$l10n = $this->createMock(IL10N::class);
|
|
$l10n->method('t')->willReturnArgument(0);
|
|
|
|
$this->provider = new FileAccountScopedSearchProvider(
|
|
$l10n,
|
|
Server::get(IRootFolder::class),
|
|
$this->shareManager,
|
|
$this->createMock(ISystemTagObjectMapper::class),
|
|
$this->createMock(ISystemTagManager::class),
|
|
$this->fullTextSearchManager,
|
|
$this->createMock(IAppConfig::class),
|
|
$this->createMock(LoggerInterface::class),
|
|
);
|
|
}
|
|
|
|
#[\Override]
|
|
protected function tearDown(): void {
|
|
$this->logout();
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function searchPaths(ISearchComparison $filter): array {
|
|
$paths = array_map(
|
|
static fn (AccountScopedSearchResult $result): string => $result->getMetadata()['path'],
|
|
iterator_to_array($this->provider->search($this->userId, $filter, 100), false),
|
|
);
|
|
sort($paths);
|
|
|
|
return $paths;
|
|
}
|
|
|
|
public static function pathProvider(): array {
|
|
return [
|
|
'home file' => [ISearchComparison::COMPARE_EQUAL, 'root.txt', ['/root.txt']],
|
|
'leading slash' => [ISearchComparison::COMPARE_EQUAL, '/Docs/in-docs.txt', ['/Docs/in-docs.txt']],
|
|
'external storage file' => [ISearchComparison::COMPARE_EQUAL, 'External/external.txt', ['/External/external.txt']],
|
|
'jailed file' => [ISearchComparison::COMPARE_EQUAL, 'Team/team.txt', ['/Team/team.txt']],
|
|
'missing file' => [ISearchComparison::COMPARE_EQUAL, 'missing.txt', []],
|
|
'in' => [
|
|
ISearchComparison::COMPARE_IN,
|
|
['External/external.txt', 'Team/team.txt', 'missing.txt'],
|
|
['/External/external.txt', '/Team/team.txt'],
|
|
],
|
|
'empty in' => [ISearchComparison::COMPARE_IN, [], []],
|
|
'folder with a mount inside' => [ISearchComparison::COMPARE_LIKE, 'Docs/%', ['/Docs/Nested/nested.txt', '/Docs/in-docs.txt']],
|
|
'external storage root' => [ISearchComparison::COMPARE_LIKE, 'External/%', ['/External/external.txt']],
|
|
'jail root excludes the rest of the storage' => [ISearchComparison::COMPARE_LIKE, 'Team/%', ['/Team/team.txt']],
|
|
'user folder' => [
|
|
ISearchComparison::COMPARE_LIKE,
|
|
'/%',
|
|
['/Docs/Nested/nested.txt', '/Docs/in-docs.txt', '/External/external.txt', '/Team/team.txt', '/root.txt'],
|
|
],
|
|
'escaped character in folder' => [ISearchComparison::COMPARE_LIKE, 'Doc\\s/%', ['/Docs/Nested/nested.txt', '/Docs/in-docs.txt']],
|
|
'missing folder' => [ISearchComparison::COMPARE_LIKE, 'Missing/%', []],
|
|
'file as folder' => [ISearchComparison::COMPARE_LIKE, 'root.txt/%', []],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('pathProvider')]
|
|
public function testSearchByPath(string $type, string|array $value, array $expected): void {
|
|
$this->assertSame($expected, $this->searchPaths(new SearchComparison($type, 'path', $value)));
|
|
}
|
|
|
|
public static function unsupportedPathProvider(): array {
|
|
return [
|
|
'suffix pattern' => [ISearchComparison::COMPARE_LIKE, '%.txt'],
|
|
'wildcard in folder' => [ISearchComparison::COMPARE_LIKE, 'Do_s/%'],
|
|
'partial name' => [ISearchComparison::COMPARE_LIKE, 'Do%'],
|
|
'single character wildcard' => [ISearchComparison::COMPARE_LIKE, 'Docs/_'],
|
|
'escaped trailing wildcard' => [ISearchComparison::COMPARE_LIKE, 'Docs/\\%'],
|
|
'wildcard after the folder wildcard' => [ISearchComparison::COMPARE_LIKE, 'Docs/%\\%'],
|
|
'escaped backslash before the wildcard' => [ISearchComparison::COMPARE_LIKE, 'Docs\\\\%'],
|
|
'ordering' => [ISearchComparison::COMPARE_GREATER_THAN, 'Docs'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('unsupportedPathProvider')]
|
|
public function testUnsupportedPathComparison(string $type, string $value): void {
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
$this->searchPaths(new SearchComparison($type, 'path', $value));
|
|
}
|
|
|
|
private function fileId(string $path): int {
|
|
return Server::get(IRootFolder::class)->getUserFolder($this->userId)->get($path)->getId();
|
|
}
|
|
|
|
private function share(int $type, int $nodeId): IShare&MockObject {
|
|
$share = $this->createMock(IShare::class);
|
|
$share->method('getShareType')->willReturn($type);
|
|
$share->method('getNodeId')->willReturn($nodeId);
|
|
$share->method('getSharedWith')->willReturn('recipient');
|
|
$share->method('getId')->willReturn('share-' . $type);
|
|
|
|
return $share;
|
|
}
|
|
|
|
public static function sharedExternallyProvider(): array {
|
|
return [
|
|
'shared' => [true, ['/External/external.txt', '/Team/team.txt']],
|
|
'not shared' => [false, ['/Docs/Nested/nested.txt', '/Docs/in-docs.txt', '/root.txt']],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('sharedExternallyProvider')]
|
|
public function testSearchSharedExternally(bool $value, array $expected): void {
|
|
$shares = [
|
|
IShare::TYPE_LINK => [$this->share(IShare::TYPE_LINK, $this->fileId('External/external.txt'))],
|
|
IShare::TYPE_EMAIL => [$this->share(IShare::TYPE_EMAIL, $this->fileId('Team/team.txt'))],
|
|
];
|
|
$this->shareManager->expects($this->exactly(4))
|
|
->method('getSharesBy')
|
|
->willReturnCallback(function (string $userId, int $type, $path, bool $reshares, int $limit) use ($shares): array {
|
|
$this->assertSame($this->userId, $userId);
|
|
$this->assertNull($path);
|
|
$this->assertSame(-1, $limit);
|
|
|
|
return $shares[$type] ?? [];
|
|
});
|
|
|
|
$this->assertSame(
|
|
$expected,
|
|
$this->searchPaths(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'shared_externally', $value)),
|
|
);
|
|
}
|
|
|
|
public function testGetShareStatus(): void {
|
|
$fileId = $this->fileId('root.txt');
|
|
$this->shareManager->expects($this->exactly(6))
|
|
->method('getSharesBy')
|
|
->willReturnCallback(function (string $owner, int $type, $node, bool $reshares, int $limit) use ($fileId): array {
|
|
$this->assertSame($fileId, $node->getId());
|
|
$this->assertSame(-1, $limit);
|
|
|
|
return in_array($type, [IShare::TYPE_LINK, IShare::TYPE_USER], true) ? [$this->share($type, $fileId)] : [];
|
|
});
|
|
|
|
$status = $this->provider->get($this->userId, (string)$fileId)->getMetadata()['share_status'];
|
|
|
|
$this->assertTrue($status['shared']);
|
|
$this->assertTrue($status['externally']);
|
|
$this->assertSame([IShare::TYPE_LINK, IShare::TYPE_USER], array_column($status['shares'], 'type'));
|
|
}
|
|
|
|
public static function contentTermProvider(): array {
|
|
return [
|
|
'equal' => [ISearchComparison::COMPARE_EQUAL, 'budget %', 'budget %'],
|
|
'surrounding wildcards' => [ISearchComparison::COMPARE_LIKE, '%budget%', 'budget'],
|
|
'wildcards separate words' => [ISearchComparison::COMPARE_LIKE, 'q1%report_2026', 'q1 report 2026'],
|
|
'escaped wildcards' => [ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE, '100\\% snake\\_case', '100% snake_case'],
|
|
'escaped backslash' => [ISearchComparison::COMPARE_LIKE, 'back\\\\slash', 'back\\slash'],
|
|
'escaped backslash before a wildcard' => [ISearchComparison::COMPARE_LIKE, 'a\\\\%', 'a\\'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('contentTermProvider')]
|
|
public function testContentTerm(string $type, string $value, string $expectedTerm): void {
|
|
$this->fullTextSearchManager->expects($this->once())
|
|
->method('search')
|
|
->with($this->callback(fn (array $request): bool => $request['search'] === $expectedTerm), $this->userId)
|
|
->willReturn([]);
|
|
|
|
$this->assertSame([], $this->searchPaths(new SearchComparison($type, 'content', $value)));
|
|
}
|
|
}
|