Skip to content

Commit f2cd0a8

Browse files
committed
test(files_sharing): add tests for fetching remote avatars
Signed-off-by: Kent Delante <kent@delante.me> Assisted-by: ClaudeCode:claude-sonnet-5
1 parent e7da4fe commit f2cd0a8

2 files changed

Lines changed: 215 additions & 14 deletions

File tree

tests/lib/Avatar/AvatarManagerTest.php

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010

1111
use OC\Avatar\AvatarManager;
1212
use OC\Avatar\PlaceholderAvatar;
13+
use OC\Avatar\RemoteAvatar;
1314
use OC\Avatar\UserAvatar;
1415
use OC\KnownUser\KnownUserService;
1516
use OC\User\Manager;
1617
use OC\User\User;
1718
use OCP\Accounts\IAccount;
1819
use OCP\Accounts\IAccountManager;
1920
use OCP\Accounts\IAccountProperty;
21+
use OCP\Federation\ICloudId;
22+
use OCP\Federation\ICloudIdManager;
2023
use OCP\Files\IAppData;
2124
use OCP\Files\SimpleFS\ISimpleFolder;
2225
use OCP\IConfig;
@@ -47,6 +50,7 @@ class AvatarManagerTest extends \Test\TestCase {
4750
private $avatarManager;
4851
/** @var KnownUserService | \PHPUnit\Framework\MockObject\MockObject */
4952
private $knownUserService;
53+
private ICloudIdManager&\PHPUnit\Framework\MockObject\MockObject $cloudIdManager;
5054

5155
#[\Override]
5256
protected function setUp(): void {
@@ -60,6 +64,7 @@ protected function setUp(): void {
6064
$this->config = $this->createMock(IConfig::class);
6165
$this->accountManager = $this->createMock(IAccountManager::class);
6266
$this->knownUserService = $this->createMock(KnownUserService::class);
67+
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
6368

6469
$this->avatarManager = new AvatarManager(
6570
$this->userSession,
@@ -69,23 +74,11 @@ protected function setUp(): void {
6974
$this->logger,
7075
$this->config,
7176
$this->accountManager,
72-
$this->knownUserService
77+
$this->knownUserService,
78+
$this->cloudIdManager
7379
);
7480
}
7581

76-
public function testGetAvatarInvalidUser(): void {
77-
$this->expectException(\Exception::class);
78-
$this->expectExceptionMessage('user does not exist');
79-
80-
$this->userManager
81-
->expects($this->once())
82-
->method('get')
83-
->with('invalidUser')
84-
->willReturn(null);
85-
86-
$this->avatarManager->getAvatar('invalidUser');
87-
}
88-
8982
public function testGetAvatarForSelf(): void {
9083
$user = $this->createMock(User::class);
9184
$user
@@ -276,4 +269,67 @@ public function testGetAvatarScopes($avatarScope, $isPublicCall, $isKnownUser, $
276269
}
277270
$this->assertEquals($expected, $this->avatarManager->getAvatar('valid-user'));
278271
}
272+
273+
public function testGetAvatarInvalidUser(): void {
274+
$this->expectException(\Exception::class);
275+
$this->expectExceptionMessage('user does not exist');
276+
277+
$this->userManager
278+
->expects($this->once())
279+
->method('get')
280+
->with('invalidUser')
281+
->willReturn(null);
282+
283+
$this->avatarManager->getAvatar('invalidUser');
284+
}
285+
286+
public function testGetAvatarForRemoteUser(): void {
287+
$cloudId = 'user@https://remote.example.com';
288+
289+
$this->userManager
290+
->expects($this->never())
291+
->method('get');
292+
293+
$resolvedCloudId = $this->createMock(ICloudId::class);
294+
$resolvedCloudId->method('getUser')->willReturn('user');
295+
$resolvedCloudId->method('getRemote')->willReturn('https://remote.example.com');
296+
$resolvedCloudId->method('getDisplayId')->willReturn('user@remote.example.com');
297+
298+
$this->cloudIdManager->expects($this->once())
299+
->method('isValidCloudId')
300+
->with($cloudId)
301+
->willReturn(true);
302+
$this->cloudIdManager->method('resolveCloudId')
303+
->with($cloudId)
304+
->willReturn($resolvedCloudId);
305+
$this->overwriteService(ICloudIdManager::class, $this->cloudIdManager);
306+
307+
$this->appData->expects($this->once())->method('getFolder');
308+
$this->accountManager->expects($this->never())->method('getAccount');
309+
310+
$avatar = $this->avatarManager->getAvatar($cloudId);
311+
312+
$this->assertInstanceOf(RemoteAvatar::class, $avatar);
313+
$this->assertTrue($avatar->exists());
314+
$this->assertTrue($avatar->isCustomAvatar());
315+
$this->assertSame('user@remote.example.com', $avatar->getDisplayName());
316+
}
317+
318+
public function testGetAvatarThrowsForUnknownUserThatIsNotACloudId(): void {
319+
$this->expectException(\Exception::class);
320+
$this->expectExceptionMessage('user does not exist');
321+
322+
$this->userManager
323+
->expects($this->once())
324+
->method('get')
325+
->with('invalidUser')
326+
->willReturn(null);
327+
328+
$this->cloudIdManager->expects($this->once())
329+
->method('isValidCloudId')
330+
->with('invalidUser')
331+
->willReturn(false);
332+
333+
$this->avatarManager->getAvatar('invalidUser');
334+
}
279335
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Test\Avatar;
11+
12+
use OC\Avatar\RemoteAvatar;
13+
use OCP\Federation\ICloudId;
14+
use OCP\Federation\ICloudIdManager;
15+
use OCP\Files\SimpleFS\ISimpleFile;
16+
use OCP\Files\SimpleFS\ISimpleFolder;
17+
use OCP\Http\Client\IClient;
18+
use OCP\Http\Client\IClientService;
19+
use OCP\Http\Client\IResponse;
20+
use OCP\IConfig;
21+
use PHPUnit\Framework\MockObject\MockObject;
22+
use Psr\Log\LoggerInterface;
23+
use Test\TestCase;
24+
25+
class RemoteAvatarTest extends TestCase {
26+
private const CLOUD_ID = 'user@https://remote.example.com';
27+
28+
private ISimpleFolder&MockObject $folder;
29+
private IConfig&MockObject $config;
30+
private LoggerInterface&MockObject $logger;
31+
private ICloudIdManager&MockObject $cloudIdManager;
32+
private IClientService&MockObject $clientService;
33+
private RemoteAvatar $avatar;
34+
35+
#[\Override]
36+
protected function setUp(): void {
37+
parent::setUp();
38+
39+
$this->config = $this->createMock(IConfig::class);
40+
$this->logger = $this->createMock(LoggerInterface::class);
41+
42+
$cloudId = $this->createMock(ICloudId::class);
43+
$cloudId->method('getUser')->willReturn('user');
44+
$cloudId->method('getRemote')->willReturn('https://remote.example.com');
45+
$cloudId->method('getDisplayId')->willReturn('user@remote.example.com');
46+
47+
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
48+
$this->cloudIdManager->method('resolveCloudId')
49+
->with(self::CLOUD_ID)
50+
->willReturn($cloudId);
51+
$this->overwriteService(ICloudIdManager::class, $this->cloudIdManager);
52+
53+
$this->clientService = $this->createMock(IClientService::class);
54+
$this->overwriteService(IClientService::class, $this->clientService);
55+
56+
$this->folder = $this->createMock(ISimpleFolder::class);
57+
$this->avatar = new RemoteAvatar($this->folder, self::CLOUD_ID, $this->config, $this->logger);
58+
}
59+
60+
/**
61+
* Stubs the client returned by IClientService::newClient() to respond to
62+
* a single GET request, optionally asserting the requested URL/options.
63+
*
64+
* @param string|resource|false $body
65+
*/
66+
private function mockRemoteClient(string $contentType, string $body, ?string $expectedUrl = null, ?array $expectedOptions = null): void {
67+
$response = $this->createMock(IResponse::class);
68+
$response->method('getHeader')->with('Content-Type')->willReturn($contentType);
69+
$response->method('getBody')->willReturn($body);
70+
71+
$client = $this->createMock(IClient::class);
72+
$matcher = $client->expects($this->once())->method('get');
73+
if ($expectedUrl !== null) {
74+
$matcher->with($expectedUrl, $expectedOptions ?? $this->anything());
75+
}
76+
$matcher->willReturn($response);
77+
78+
$this->clientService->method('newClient')->willReturn($client);
79+
}
80+
81+
public function testExists(): void {
82+
$this->assertTrue($this->avatar->exists());
83+
}
84+
85+
public function testGetDisplayName(): void {
86+
$this->assertSame('user@remote.example.com', $this->avatar->getDisplayName());
87+
}
88+
89+
public function testGetFileFetchesAvatarFromRemoteInstance(): void {
90+
$this->config->method('getSystemValueBool')
91+
->with('sharing.federation.allowSelfSignedCertificates', false)
92+
->willReturn(false);
93+
94+
$fileContents = 'png-bytes';
95+
96+
$this->mockRemoteClient(
97+
'image/png',
98+
$fileContents,
99+
'https://remote.example.com/index.php/avatar/user/64',
100+
['verify' => true],
101+
);
102+
103+
$expectedFile = $this->createMock(ISimpleFile::class);
104+
$expectedFile->expects($this->once())->method('getContent')->willReturn($fileContents);
105+
106+
$this->folder->expects($this->once())->method('newFile')->willReturn($expectedFile);
107+
108+
$file = $this->avatar->getFile(64);
109+
$this->assertInstanceOf(ISimpleFile::class, $file);
110+
$this->assertSame($fileContents, $file->getContent());
111+
}
112+
113+
public function testGetFileFetchesAvatarFromCache(): void {
114+
$this->config->method('getSystemValueBool')
115+
->with('sharing.federation.allowSelfSignedCertificates', false)
116+
->willReturn(false);
117+
118+
$fileContents = 'png-bytes';
119+
120+
$expectedFile = $this->createMock(ISimpleFile::class);
121+
$expectedFile->expects($this->once())->method('getContent')->willReturn($fileContents);
122+
$expectedFile->expects($this->once())->method('getMTime')->willReturn(time() + (60 * 60 * 24));
123+
$expectedFile->expects($this->once())->method('getName')->willReturn('avatar.64.png');
124+
$this->folder->expects($this->once())->method('getDirectoryListing')->willReturn([$expectedFile]);
125+
126+
$this->clientService->expects($this->never())->method('newClient');
127+
128+
$file = $this->avatar->getFile(64);
129+
$this->assertInstanceOf(ISimpleFile::class, $file);
130+
$this->assertSame($fileContents, $file->getContent());
131+
}
132+
133+
public function testGetFileThrowsOnUnexpectedContentType(): void {
134+
$this->mockRemoteClient('text/html', '<html></html>');
135+
136+
$this->expectException(\Exception::class);
137+
$this->expectExceptionMessage('Unknown filetype');
138+
139+
$this->avatar->getFile(64);
140+
}
141+
142+
public function testIsCustomAvatar(): void {
143+
$this->assertTrue($this->avatar->isCustomAvatar());
144+
}
145+
}

0 commit comments

Comments
 (0)