Merge pull request #45698 from nextcloud/fix/files-remote-shares

This commit is contained in:
John Molakvoæ 2024-06-12 13:02:48 +02:00 committed by GitHub
commit 4c32ab7b72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 290 additions and 82 deletions

View file

@ -7,6 +7,7 @@
*/
namespace OCA\DAV\Connector\Sabre;
use OC\Share20\Exception\BackendError;
use OCA\DAV\Connector\Sabre\Node as DavNode;
use OCP\Files\Folder;
use OCP\Files\Node;
@ -33,24 +34,19 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
* @var \Sabre\DAV\Server
*/
private $server;
private IManager $shareManager;
private Tree $tree;
private string $userId;
private Folder $userFolder;
/** @var IShare[][] */
private array $cachedShares = [];
/** @var string[] */
private array $cachedFolders = [];
public function __construct(
Tree $tree,
IUserSession $userSession,
Folder $userFolder,
IManager $shareManager
private Tree $tree,
private IUserSession $userSession,
private Folder $userFolder,
private IManager $shareManager,
) {
$this->tree = $tree;
$this->shareManager = $shareManager;
$this->userFolder = $userFolder;
$this->userId = $userSession->getUser()->getUID();
}
@ -91,18 +87,29 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
IShare::TYPE_DECK,
IShare::TYPE_SCIENCEMESH,
];
foreach ($requestedShareTypes as $requestedShareType) {
$shares = $this->shareManager->getSharesBy(
$result = array_merge($result, $this->shareManager->getSharesBy(
$this->userId,
$requestedShareType,
$node,
false,
-1
);
foreach ($shares as $share) {
$result[] = $share;
));
// Also check for shares where the user is the recipient
try {
$result = array_merge($result, $this->shareManager->getSharedWith(
$this->userId,
$requestedShareType,
$node,
-1
));
} catch (BackendError $e) {
// ignore
}
}
return $result;
}
@ -124,27 +131,29 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
*/
private function getShares(DavNode $sabreNode): array {
if (isset($this->cachedShares[$sabreNode->getId()])) {
$shares = $this->cachedShares[$sabreNode->getId()];
} else {
[$parentPath,] = \Sabre\Uri\split($sabreNode->getPath());
if ($parentPath === '') {
$parentPath = '/';
}
// if we already cached the folder this file is in we know there are no shares for this file
if (array_search($parentPath, $this->cachedFolders) === false) {
try {
$node = $sabreNode->getNode();
} catch (NotFoundException $e) {
return [];
}
$shares = $this->getShare($node);
$this->cachedShares[$sabreNode->getId()] = $shares;
} else {
return [];
}
return $this->cachedShares[$sabreNode->getId()];
}
return $shares;
[$parentPath,] = \Sabre\Uri\split($sabreNode->getPath());
if ($parentPath === '') {
$parentPath = '/';
}
// if we already cached the folder containing this file
// then we already know there are no shares here.
if (array_search($parentPath, $this->cachedFolders) === false) {
try {
$node = $sabreNode->getNode();
} catch (NotFoundException $e) {
return [];
}
$shares = $this->getShare($node);
$this->cachedShares[$sabreNode->getId()] = $shares;
return $shares;
}
return [];
}
/**
@ -161,7 +170,9 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
return;
}
// need prefetch ?
// If the node is a directory and we are requesting share types or sharees
// then we get all the shares in the folder and cache them.
// This is more performant than iterating each files afterwards.
if ($sabreNode instanceof Directory
&& $propFind->getDepth() !== 0
&& (

View file

@ -263,13 +263,15 @@ class Server {
$this->server->tree, \OC::$server->getTagManager()
)
);
// TODO: switch to LazyUserFolder
$userFolder = \OC::$server->getUserFolder();
$shareManager = \OCP\Server::get(\OCP\Share\IManager::class);
$this->server->addPlugin(new SharesPlugin(
$this->server->tree,
$userSession,
$userFolder,
\OC::$server->getShareManager()
$shareManager,
));
$this->server->addPlugin(new CommentPropertiesPlugin(
\OC::$server->getCommentsManager(),
@ -304,7 +306,7 @@ class Server {
$this->server->tree,
$user,
\OC::$server->getRootFolder(),
\OC::$server->getShareManager(),
$shareManager,
$view,
\OCP\Server::get(IFilesMetadataManager::class)
));

View file

@ -97,7 +97,7 @@ class SharesPluginTest extends \Test\TestCase {
->with(
$this->equalTo('user1'),
$this->anything(),
$this->anything(),
$this->equalTo($node),
$this->equalTo(false),
$this->equalTo(-1)
)
@ -111,6 +111,16 @@ class SharesPluginTest extends \Test\TestCase {
return [];
});
$this->shareManager->expects($this->any())
->method('getSharedWith')
->with(
$this->equalTo('user1'),
$this->anything(),
$this->equalTo($node),
$this->equalTo(-1)
)
->willReturn([]);
$propFind = new \Sabre\DAV\PropFind(
'/dummyPath',
[self::SHARETYPES_PROPERTYNAME],
@ -199,6 +209,16 @@ class SharesPluginTest extends \Test\TestCase {
return [];
});
$this->shareManager->expects($this->any())
->method('getSharedWith')
->with(
$this->equalTo('user1'),
$this->anything(),
$this->equalTo($node),
$this->equalTo(-1)
)
->willReturn([]);
$this->shareManager->expects($this->any())
->method('getSharesInFolder')
->with(

View file

@ -51,7 +51,8 @@ export const canDownload = (nodes: Node[]) => {
}
export const canCopy = (nodes: Node[]) => {
// For now the only restriction is that a shared file
// cannot be copied if the download is disabled
// a shared file cannot be copied if the download is disabled
// it can be copied if the user has at least read permissions
return canDownload(nodes)
&& !nodes.some(node => node.permissions === Permission.NONE)
}

View file

@ -65,7 +65,7 @@
class="files-list__row-mtime"
data-cy-files-list-row-mtime
@click="openDetailsIfAvailable">
<NcDateTime :timestamp="source.mtime" :ignore-seconds="true" />
<NcDateTime v-if="source.mtime" :timestamp="source.mtime" :ignore-seconds="true" />
</td>
<!-- View columns -->
@ -177,8 +177,8 @@ export default defineComponent({
},
size() {
const size = parseInt(this.source.size, 10) || 0
if (typeof size !== 'number' || size < 0) {
const size = parseInt(this.source.size, 10)
if (typeof size !== 'number' || isNaN(size) || size < 0) {
return this.t('files', 'Pending')
}
return formatFileSize(size, true)
@ -186,8 +186,8 @@ export default defineComponent({
sizeOpacity() {
const maxOpacitySize = 10 * 1024 * 1024
const size = parseInt(this.source.size, 10) || 0
if (!size || size < 0) {
const size = parseInt(this.source.size, 10)
if (!size || isNaN(size) || size < 0) {
return {}
}

View file

@ -17,14 +17,22 @@ import { getCurrentUser } from '@nextcloud/auth'
import './sharingStatusAction.scss'
const generateAvatarSvg = (userId: string) => {
const avatarUrl = generateUrl('/avatar/{userId}/32', { userId })
const isDarkMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches === true
|| document.querySelector('[data-themes*=dark]') !== null
const generateAvatarSvg = (userId: string, isGuest = false) => {
const url = isDarkMode ? '/avatar/{userId}/32/dark' : '/avatar/{userId}/32'
const avatarUrl = generateUrl(isGuest ? url : url + '?guestFallback=true', { userId })
return `<svg width="32" height="32" viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg" class="sharing-status__avatar">
<image href="${avatarUrl}" height="32" width="32" />
</svg>`
}
const isExternal = (node: Node) => {
return node.attributes.remote_id !== undefined
}
export const action = new FileAction({
id: 'sharing-status',
displayName(nodes: Node[]) {
@ -33,7 +41,7 @@ export const action = new FileAction({
const ownerId = node?.attributes?.['owner-id']
if (shareTypes.length > 0
|| (ownerId && ownerId !== getCurrentUser()?.uid)) {
|| (ownerId !== getCurrentUser()?.uid || isExternal(node))) {
return t('files_sharing', 'Shared')
}
@ -46,11 +54,11 @@ export const action = new FileAction({
const ownerDisplayName = node?.attributes?.['owner-display-name']
// Mixed share types
if (Array.isArray(node.attributes?.['share-types'])) {
if (Array.isArray(node.attributes?.['share-types']) && node.attributes?.['share-types'].length > 1) {
return t('files_sharing', 'Shared multiple times with different people')
}
if (ownerId && ownerId !== getCurrentUser()?.uid) {
if (ownerId && (ownerId !== getCurrentUser()?.uid || isExternal(node))) {
return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName })
}
@ -62,7 +70,7 @@ export const action = new FileAction({
const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[]
// Mixed share types
if (Array.isArray(node.attributes?.['share-types'])) {
if (Array.isArray(node.attributes?.['share-types']) && node.attributes?.['share-types'].length > 1) {
return AccountPlusSvg
}
@ -84,8 +92,8 @@ export const action = new FileAction({
}
const ownerId = node?.attributes?.['owner-id']
if (ownerId && ownerId !== getCurrentUser()?.uid) {
return generateAvatarSvg(ownerId)
if (ownerId && (ownerId !== getCurrentUser()?.uid || isExternal(node))) {
return generateAvatarSvg(ownerId, isExternal(node))
}
return AccountPlusSvg
@ -107,7 +115,7 @@ export const action = new FileAction({
}
// If the node is shared by someone else
if (ownerId && ownerId !== getCurrentUser()?.uid) {
if (ownerId && (ownerId !== getCurrentUser()?.uid || isExternal(node))) {
return true
}

View file

@ -336,12 +336,27 @@ describe('SharingService share to Node mapping', () => {
expect(folder.attributes.favorite).toBe(1)
})
test('Empty', async () => {
jest.spyOn(logger, 'error').mockImplementationOnce(() => {})
jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({
data: {
ocs: {
data: [],
},
},
}))
const shares = await getContents(false, true, false, false)
expect(shares.contents).toHaveLength(0)
expect(logger.error).toHaveBeenCalledTimes(0)
})
test('Error', async () => {
jest.spyOn(logger, 'error').mockImplementationOnce(() => {})
jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({
data: {
ocs: {
data: [{}],
data: [null],
},
},
}))

View file

@ -6,7 +6,7 @@
import type { AxiosPromise } from 'axios'
import type { OCSResponse } from '@nextcloud/typings/ocs'
import { Folder, File, type ContentsWithRoot } from '@nextcloud/files'
import { Folder, File, type ContentsWithRoot, Permission } from '@nextcloud/files'
import { generateOcsUrl, generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
@ -19,16 +19,34 @@ const headers = {
'Content-Type': 'application/json',
}
const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | null> {
try {
// Federated share handling
if (ocsEntry?.remote_id !== undefined) {
const mime = (await import('mime')).default
// This won't catch files without an extension, but this is the best we can do
ocsEntry.mimetype = mime.getType(ocsEntry.name)
ocsEntry.item_type = ocsEntry.mimetype ? 'file' : 'folder'
// Need to set permissions to NONE for federated shares
ocsEntry.item_permissions = Permission.NONE
ocsEntry.permissions = Permission.NONE
ocsEntry.uid_owner = ocsEntry.owner
// TODO: have the real display name stored somewhere
ocsEntry.displayname_owner = ocsEntry.owner
}
const isFolder = ocsEntry?.item_type === 'folder'
const hasPreview = ocsEntry?.has_preview === true
const Node = isFolder ? Folder : File
const fileid = ocsEntry.file_source
// If this is an external share that is not yet accepted,
// we don't have an id. We can fallback to the row id temporarily
const fileid = ocsEntry.file_source || ocsEntry.id
// Generate path and strip double slashes
const path = ocsEntry?.path || ocsEntry.file_target
const path = ocsEntry?.path || ocsEntry.file_target || ocsEntry.name
const source = generateRemoteUrl(`dav/${rootPath}/${path}`.replaceAll(/\/\//gm, '/'))
// Prefer share time if more recent than item mtime
@ -41,7 +59,7 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
id: fileid,
source,
owner: ocsEntry?.uid_owner,
mime: ocsEntry?.mimetype,
mime: ocsEntry?.mimetype || 'application/octet-stream',
mtime,
size: ocsEntry?.item_size,
permissions: ocsEntry?.item_permissions || ocsEntry?.permissions,
@ -150,7 +168,7 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true,
const responses = await Promise.all(promises)
const data = responses.map((response) => response.data.ocs.data).flat()
let contents = data.map(ocsEntryToNode)
let contents = (await Promise.all(data.map(ocsEntryToNode)))
.filter((node) => node !== null) as (Folder | File)[]
if (filterTypes.length > 0) {

View file

@ -14,6 +14,7 @@ use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IAvatarManager;
@ -40,6 +41,7 @@ class AvatarController extends Controller {
protected LoggerInterface $logger,
protected ?string $userId,
protected TimeFactory $timeFactory,
protected GuestAvatarController $guestAvatarController,
) {
parent::__construct($appName, $request);
}
@ -54,13 +56,15 @@ class AvatarController extends Controller {
*
* @param string $userId ID of the user
* @param int $size Size of the avatar
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @param bool $guestFallback Fallback to guest avatar if not found
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
*
* 200: Avatar returned
* 201: Avatar returned
* 404: Avatar not found
*/
#[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}/dark')]
public function getAvatarDark(string $userId, int $size) {
public function getAvatarDark(string $userId, int $size, bool $guestFallback = false) {
if ($size <= 64) {
if ($size !== 64) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
@ -82,6 +86,9 @@ class AvatarController extends Controller {
['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
);
} catch (\Exception $e) {
if ($guestFallback) {
return $this->guestAvatarController->getAvatarDark($userId, (string)$size);
}
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
@ -101,13 +108,15 @@ class AvatarController extends Controller {
*
* @param string $userId ID of the user
* @param int $size Size of the avatar
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @param bool $guestFallback Fallback to guest avatar if not found
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
*
* 200: Avatar returned
* 201: Avatar returned
* 404: Avatar not found
*/
#[FrontpageRoute(verb: 'GET', url: '/avatar/{userId}/{size}')]
public function getAvatar(string $userId, int $size) {
public function getAvatar(string $userId, int $size, bool $guestFallback = false) {
if ($size <= 64) {
if ($size !== 64) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
@ -129,6 +138,9 @@ class AvatarController extends Controller {
['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
);
} catch (\Exception $e) {
if ($guestFallback) {
return $this->guestAvatarController->getAvatar($userId, (string)$size);
}
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}

View file

@ -39,7 +39,7 @@ class GuestAvatarController extends Controller {
* @param string $guestName The guest name, e.g. "Albert"
* @param string $size The desired avatar size, e.g. 64 for 64x64px
* @param bool|null $darkTheme Return dark avatar
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
*
* 200: Custom avatar returned
* 201: Avatar returned
@ -68,7 +68,7 @@ class GuestAvatarController extends Controller {
$resp = new FileDisplayResponse(
$avatarFile,
$avatar->isCustomAvatar() ? Http::STATUS_OK : Http::STATUS_CREATED,
['Content-Type' => $avatarFile->getMimeType()]
['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
);
} catch (\Exception $e) {
$this->logger->error('error while creating guest avatar', [
@ -92,7 +92,7 @@ class GuestAvatarController extends Controller {
*
* @param string $guestName The guest name, e.g. "Albert"
* @param string $size The desired avatar size, e.g. 64 for 64x64px
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
*
* 200: Custom avatar returned
* 201: Avatar returned

View file

@ -7318,6 +7318,19 @@
}
],
"parameters": [
{
"name": "guestFallback",
"in": "query",
"description": "Fallback to guest avatar if not found",
"schema": {
"type": "integer",
"default": 0,
"enum": [
0,
1
]
}
},
{
"name": "userId",
"in": "path",
@ -7358,6 +7371,25 @@
}
}
},
"201": {
"description": "Avatar returned",
"headers": {
"X-NC-IsCustomAvatar": {
"schema": {
"type": "integer",
"format": "int64"
}
}
},
"content": {
"*/*": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"404": {
"description": "Avatar not found",
"content": {
@ -7365,6 +7397,9 @@
"schema": {}
}
}
},
"500": {
"description": ""
}
}
}
@ -7386,6 +7421,19 @@
}
],
"parameters": [
{
"name": "guestFallback",
"in": "query",
"description": "Fallback to guest avatar if not found",
"schema": {
"type": "integer",
"default": 0,
"enum": [
0,
1
]
}
},
{
"name": "userId",
"in": "path",
@ -7426,6 +7474,25 @@
}
}
},
"201": {
"description": "Avatar returned",
"headers": {
"X-NC-IsCustomAvatar": {
"schema": {
"type": "integer",
"format": "int64"
}
}
},
"content": {
"*/*": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"404": {
"description": "Avatar not found",
"content": {
@ -7433,6 +7500,9 @@
"schema": {}
}
}
},
"500": {
"description": ""
}
}
}
@ -7569,6 +7639,14 @@
"responses": {
"200": {
"description": "Custom avatar returned",
"headers": {
"X-NC-IsCustomAvatar": {
"schema": {
"type": "integer",
"format": "int64"
}
}
},
"content": {
"*/*": {
"schema": {
@ -7580,6 +7658,14 @@
},
"201": {
"description": "Avatar returned",
"headers": {
"X-NC-IsCustomAvatar": {
"schema": {
"type": "integer",
"format": "int64"
}
}
},
"content": {
"*/*": {
"schema": {
@ -7634,6 +7720,14 @@
"responses": {
"200": {
"description": "Custom avatar returned",
"headers": {
"X-NC-IsCustomAvatar": {
"schema": {
"type": "integer",
"format": "int64"
}
}
},
"content": {
"*/*": {
"schema": {
@ -7645,6 +7739,14 @@
},
"201": {
"description": "Avatar returned",
"headers": {
"X-NC-IsCustomAvatar": {
"schema": {
"type": "integer",
"format": "int64"
}
}
},
"content": {
"*/*": {
"schema": {

2
dist/857-857.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/857-857.js.map vendored Normal file

File diff suppressed because one or more lines are too long

6
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -97,3 +97,8 @@
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

File diff suppressed because one or more lines are too long

View file

@ -1205,7 +1205,7 @@ class Manager implements IManager {
throw new \Exception("non-shallow getSharesInFolder is no longer supported");
}
return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares, $shallow) {
return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares) {
$newShares = $provider->getSharesInFolder($userId, $node, $reshares);
foreach ($newShares as $fid => $data) {
if (!isset($shares[$fid])) {

View file

@ -19,6 +19,7 @@ namespace Tests\Core\Controller;
use OC\AppFramework\Utility\TimeFactory;
use OC\Core\Controller\AvatarController;
use OC\Core\Controller\GuestAvatarController;
use OCP\AppFramework\Http;
use OCP\Files\File;
use OCP\Files\IRootFolder;
@ -42,13 +43,15 @@ use Psr\Log\LoggerInterface;
class AvatarControllerTest extends \Test\TestCase {
/** @var AvatarController */
private $avatarController;
/** @var GuestAvatarController */
private $guestAvatarController;
/** @var IAvatar|\PHPUnit\Framework\MockObject\MockObject */
private $avatarMock;
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject */
private $userMock;
/** @var ISimpleFile|\PHPUnit\Framework\MockObject\MockObject */
private $avatarFile;
/** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */
private $avatarManager;
/** @var ICache|\PHPUnit\Framework\MockObject\MockObject */
@ -83,6 +86,13 @@ class AvatarControllerTest extends \Test\TestCase {
$this->avatarMock = $this->getMockBuilder('OCP\IAvatar')->getMock();
$this->userMock = $this->getMockBuilder(IUser::class)->getMock();
$this->guestAvatarController = new GuestAvatarController(
'core',
$this->request,
$this->avatarManager,
$this->logger
);
$this->avatarController = new AvatarController(
'core',
$this->request,
@ -93,7 +103,8 @@ class AvatarControllerTest extends \Test\TestCase {
$this->rootFolder,
$this->logger,
'userid',
$this->timeFactory
$this->timeFactory,
$this->guestAvatarController,
);
// Configure userMock