mirror of
https://github.com/nextcloud/server.git
synced 2026-10-31 18:15:57 +01:00
364 lines
14 KiB
PHP
364 lines
14 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\FederatedFileSharing\Tests\OCM;
|
|
|
|
use OC\OCM\OCMSignatoryManager;
|
|
use OCA\FederatedFileSharing\AddressHandler;
|
|
use OCA\FederatedFileSharing\FederatedShareProvider;
|
|
use OCA\FederatedFileSharing\OCM\CloudFederationProviderFiles;
|
|
use OCA\Files_Sharing\External\ExternalShareMapper;
|
|
use OCA\Files_Sharing\External\Manager;
|
|
use OCP\Activity\IManager as IActivityManager;
|
|
use OCP\App\IAppManager;
|
|
use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
|
|
use OCP\Federation\ICloudFederationFactory;
|
|
use OCP\Federation\ICloudFederationProviderManager;
|
|
use OCP\Federation\ICloudFederationShare;
|
|
use OCP\Federation\ICloudIdManager;
|
|
use OCP\Files\IFilenameValidator;
|
|
use OCP\Files\ISetupManager;
|
|
use OCP\Http\Client\IClient;
|
|
use OCP\Http\Client\IClientService;
|
|
use OCP\Http\Client\IResponse;
|
|
use OCP\IAppConfig;
|
|
use OCP\IConfig;
|
|
use OCP\IGroupManager;
|
|
use OCP\IURLGenerator;
|
|
use OCP\IUserManager;
|
|
use OCP\Notification\IManager as INotificationManager;
|
|
use OCP\OCM\IOCMDiscoveryService;
|
|
use OCP\OCM\IOCMProvider;
|
|
use OCP\OCM\OCMCapabilities;
|
|
use OCP\Security\Signature\ISignatureManager;
|
|
use OCP\Share\IManager;
|
|
use OCP\Share\IProviderFactory;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use Psr\Log\LoggerInterface;
|
|
use Test\TestCase;
|
|
|
|
class CloudFederationProviderFilesTest extends TestCase {
|
|
private IAppManager&MockObject $appManager;
|
|
private FederatedShareProvider&MockObject $federatedShareProvider;
|
|
private AddressHandler&MockObject $addressHandler;
|
|
private IUserManager&MockObject $userManager;
|
|
private IManager&MockObject $shareManager;
|
|
private ICloudIdManager&MockObject $cloudIdManager;
|
|
private IActivityManager&MockObject $activityManager;
|
|
private INotificationManager&MockObject $notificationManager;
|
|
private IURLGenerator&MockObject $urlGenerator;
|
|
private ICloudFederationFactory&MockObject $cloudFederationFactory;
|
|
private ICloudFederationProviderManager&MockObject $cloudFederationProviderManager;
|
|
private IGroupManager&MockObject $groupManager;
|
|
private IConfig&MockObject $config;
|
|
private Manager&MockObject $externalShareManager;
|
|
private LoggerInterface&MockObject $logger;
|
|
private IFilenameValidator&MockObject $filenameValidator;
|
|
private IProviderFactory&MockObject $shareProviderFactory;
|
|
private ISetupManager&MockObject $setupManager;
|
|
private ExternalShareMapper&MockObject $externalShareMapper;
|
|
private IOCMDiscoveryService&MockObject $discoveryService;
|
|
private IClientService&MockObject $clientService;
|
|
private ISignatureManager&MockObject $signatureManager;
|
|
private OCMSignatoryManager&MockObject $signatoryManager;
|
|
private IAppConfig&MockObject $appConfig;
|
|
|
|
private CloudFederationProviderFiles $provider;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->appManager = $this->createMock(IAppManager::class);
|
|
$this->federatedShareProvider = $this->createMock(FederatedShareProvider::class);
|
|
$this->addressHandler = $this->createMock(AddressHandler::class);
|
|
$this->userManager = $this->createMock(IUserManager::class);
|
|
$this->shareManager = $this->createMock(IManager::class);
|
|
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
|
|
$this->activityManager = $this->createMock(IActivityManager::class);
|
|
$this->notificationManager = $this->createMock(INotificationManager::class);
|
|
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
|
$this->cloudFederationFactory = $this->createMock(ICloudFederationFactory::class);
|
|
$this->cloudFederationProviderManager = $this->createMock(ICloudFederationProviderManager::class);
|
|
$this->groupManager = $this->createMock(IGroupManager::class);
|
|
$this->config = $this->createMock(IConfig::class);
|
|
$this->externalShareManager = $this->createMock(Manager::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
$this->filenameValidator = $this->createMock(IFilenameValidator::class);
|
|
$this->shareProviderFactory = $this->createMock(IProviderFactory::class);
|
|
$this->setupManager = $this->createMock(ISetupManager::class);
|
|
$this->externalShareMapper = $this->createMock(ExternalShareMapper::class);
|
|
$this->discoveryService = $this->createMock(IOCMDiscoveryService::class);
|
|
$this->clientService = $this->createMock(IClientService::class);
|
|
$this->signatureManager = $this->createMock(ISignatureManager::class);
|
|
$this->signatoryManager = $this->createMock(OCMSignatoryManager::class);
|
|
$this->appConfig = $this->createMock(IAppConfig::class);
|
|
|
|
$this->provider = new CloudFederationProviderFiles(
|
|
$this->appManager,
|
|
$this->federatedShareProvider,
|
|
$this->addressHandler,
|
|
$this->userManager,
|
|
$this->shareManager,
|
|
$this->cloudIdManager,
|
|
$this->activityManager,
|
|
$this->notificationManager,
|
|
$this->urlGenerator,
|
|
$this->cloudFederationFactory,
|
|
$this->cloudFederationProviderManager,
|
|
$this->groupManager,
|
|
$this->config,
|
|
$this->externalShareManager,
|
|
$this->logger,
|
|
$this->filenameValidator,
|
|
$this->shareProviderFactory,
|
|
$this->setupManager,
|
|
$this->externalShareMapper,
|
|
$this->discoveryService,
|
|
$this->clientService,
|
|
$this->signatureManager,
|
|
$this->signatoryManager,
|
|
$this->appConfig,
|
|
);
|
|
}
|
|
|
|
private function enableS2S(): void {
|
|
$this->appManager->method('isEnabledForUser')
|
|
->with('files_sharing')
|
|
->willReturn(true);
|
|
$this->federatedShareProvider->method('isIncomingServer2serverShareEnabled')
|
|
->willReturn(true);
|
|
}
|
|
|
|
private function buildShare(array $requirements = []): ICloudFederationShare&MockObject {
|
|
$share = $this->createMock(ICloudFederationShare::class);
|
|
$share->method('getProtocol')->willReturn([
|
|
'name' => 'webdav',
|
|
'webdav' => ['requirements' => $requirements],
|
|
]);
|
|
$share->method('getOwner')->willReturn('owner@example.com');
|
|
$share->method('getOwnerDisplayName')->willReturn('Owner Name');
|
|
$share->method('getShareSecret')->willReturn('refresh-token-abc');
|
|
$share->method('getResourceName')->willReturn('/SharedFolder');
|
|
$share->method('getShareWith')->willReturn('localuser');
|
|
$share->method('getProviderId')->willReturn('42');
|
|
$share->method('getSharedBy')->willReturn('owner@example.com');
|
|
$share->method('getShareType')->willReturn('user');
|
|
return $share;
|
|
}
|
|
|
|
private function buildMultiShare(array $requirements = []): ICloudFederationShare&MockObject {
|
|
$share = $this->createMock(ICloudFederationShare::class);
|
|
// OCM multi-protocol envelope: name => 'multi' with the webdav entry as a
|
|
// sibling, alongside other protocols (e.g. webapp) the files provider ignores.
|
|
$share->method('getProtocol')->willReturn([
|
|
'name' => 'multi',
|
|
'webdav' => ['sharedSecret' => 'refresh-token-abc', 'requirements' => $requirements],
|
|
'webapp' => ['sharedSecret' => 'refresh-token-abc', 'uri' => 'https://app.example/open'],
|
|
]);
|
|
$share->method('getOwner')->willReturn('owner@example.com');
|
|
$share->method('getOwnerDisplayName')->willReturn('Owner Name');
|
|
$share->method('getShareSecret')->willReturn('refresh-token-abc');
|
|
$share->method('getResourceName')->willReturn('/SharedFolder');
|
|
$share->method('getShareWith')->willReturn('localuser');
|
|
$share->method('getProviderId')->willReturn('42');
|
|
$share->method('getSharedBy')->willReturn('owner@example.com');
|
|
$share->method('getShareType')->willReturn('user');
|
|
return $share;
|
|
}
|
|
|
|
/**
|
|
* A multi-protocol envelope (name => 'multi') must be accepted by serving its
|
|
* webdav entry. We drive through to the user lookup guard to prove the protocol
|
|
* check passed rather than rejecting the share as unsupported.
|
|
*/
|
|
public function testShareReceivedAcceptsMultiProtocolEnvelope(): void {
|
|
$this->enableS2S();
|
|
|
|
$this->addressHandler->method('splitUserRemote')
|
|
->with('owner@example.com')
|
|
->willReturn(['owner', 'https://example.com/']);
|
|
|
|
$share = $this->buildMultiShare();
|
|
|
|
$this->discoveryService->method('discover')
|
|
->willThrowException(new \Exception('network error'));
|
|
|
|
$this->userManager->method('getUserNameFromLoginName')->with('localuser')->willReturn('localuser');
|
|
$this->userManager->method('get')->with('localuser')->willReturn(null);
|
|
$this->filenameValidator->method('isFilenameValid')->willReturn(true);
|
|
|
|
$this->expectException(ProviderCouldNotAddShareException::class);
|
|
$this->expectExceptionMessage('User does not exists');
|
|
|
|
$this->provider->shareReceived($share);
|
|
}
|
|
|
|
/**
|
|
* When must-exchange-token is required but the remote has no token endpoint,
|
|
* shareReceived must throw rather than silently accept the share.
|
|
*/
|
|
public function testShareReceivedMustExchangeTokenThrowsWhenExchangeFails(): void {
|
|
$this->enableS2S();
|
|
|
|
$this->addressHandler->method('splitUserRemote')
|
|
->with('owner@example.com')
|
|
->willReturn(['owner', 'https://example.com/']);
|
|
|
|
$share = $this->buildShare(['must-exchange-token']);
|
|
|
|
$ocmProvider = $this->createMock(IOCMProvider::class);
|
|
$ocmProvider->method('getTokenEndPoint')->willReturn('');
|
|
|
|
$this->discoveryService->method('discover')
|
|
->willReturn($ocmProvider);
|
|
|
|
$this->expectException(ProviderCouldNotAddShareException::class);
|
|
|
|
$this->provider->shareReceived($share);
|
|
}
|
|
|
|
/**
|
|
* When must-exchange-token is required and the token exchange succeeds,
|
|
* the access token is stored on the share (we drive through share creation
|
|
* up to the "user does not exist" guard to avoid a full integration setup).
|
|
*/
|
|
public function testShareReceivedMustExchangeTokenStoresAccessToken(): void {
|
|
$this->enableS2S();
|
|
|
|
$this->addressHandler->method('splitUserRemote')
|
|
->with('owner@example.com')
|
|
->willReturn(['owner', 'https://example.com/']);
|
|
|
|
$share = $this->buildShare(['must-exchange-token']);
|
|
|
|
$tokenEndpoint = 'https://example.com/index.php/ocm/token';
|
|
|
|
$ocmProvider = $this->createMock(IOCMProvider::class);
|
|
$ocmProvider->method('getTokenEndPoint')->willReturn($tokenEndpoint);
|
|
$ocmProvider->method('getCapabilities')->willReturn(new OCMCapabilities([]));
|
|
|
|
$this->discoveryService->method('discover')->willReturn($ocmProvider);
|
|
|
|
$this->urlGenerator->method('getAbsoluteURL')->willReturn('https://local.example/');
|
|
|
|
$signedOptions = [
|
|
'body' => 'grant_type=authorization_code&client_id=local.example&code=refresh-token-abc',
|
|
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded', 'Signature' => 'sig'],
|
|
'timeout' => 10,
|
|
'connect_timeout' => 10,
|
|
];
|
|
$this->signatureManager->method('signOutgoingRequestIClientPayload')
|
|
->willReturn($signedOptions);
|
|
|
|
$response = $this->createMock(IResponse::class);
|
|
$response->method('getStatusCode')->willReturn(200);
|
|
$response->method('getBody')->willReturn(json_encode([
|
|
'access_token' => 'access-token-xyz',
|
|
'token_type' => 'Bearer',
|
|
]));
|
|
|
|
$httpClient = $this->createMock(IClient::class);
|
|
$httpClient->method('post')->willReturn($response);
|
|
$this->clientService->method('newClient')->willReturn($httpClient);
|
|
|
|
// Exchange succeeds → share creation continues; we stop it at the user
|
|
// lookup stage to avoid a full integration setup.
|
|
$this->userManager->method('getUserNameFromLoginName')->with('localuser')->willReturn('localuser');
|
|
$this->userManager->method('get')->with('localuser')->willReturn(null);
|
|
$this->filenameValidator->method('isFilenameValid')->willReturn(true);
|
|
|
|
$this->expectException(ProviderCouldNotAddShareException::class);
|
|
$this->expectExceptionMessage('User does not exists');
|
|
|
|
$this->provider->shareReceived($share);
|
|
}
|
|
|
|
/**
|
|
* When exchange-token capability is present but the discovery service throws,
|
|
* shareReceived must not propagate the exception — the token exchange is optional.
|
|
*/
|
|
public function testShareReceivedOptionalExchangeGracefulOnDiscoveryFailure(): void {
|
|
$this->enableS2S();
|
|
|
|
$this->addressHandler->method('splitUserRemote')
|
|
->with('owner@example.com')
|
|
->willReturn(['owner', 'https://example.com/']);
|
|
|
|
// Build a share with no must-exchange-token requirement
|
|
$share = $this->buildShare();
|
|
|
|
$this->discoveryService->method('discover')
|
|
->willThrowException(new \Exception('network error'));
|
|
|
|
// Discovery failure is caught and logged; share creation continues.
|
|
// We stop it at the user lookup stage.
|
|
$this->userManager->method('getUserNameFromLoginName')->with('localuser')->willReturn('localuser');
|
|
$this->userManager->method('get')->with('localuser')->willReturn(null);
|
|
$this->filenameValidator->method('isFilenameValid')->willReturn(true);
|
|
|
|
$this->expectException(ProviderCouldNotAddShareException::class);
|
|
$this->expectExceptionMessage('User does not exists');
|
|
|
|
$this->provider->shareReceived($share);
|
|
}
|
|
|
|
/**
|
|
* When exchange-token capability is present and the exchange succeeds,
|
|
* the access token is set (we stop at user-not-found to avoid full setup).
|
|
*/
|
|
public function testShareReceivedOptionalExchangeStoresAccessTokenOnSuccess(): void {
|
|
$this->enableS2S();
|
|
|
|
$this->addressHandler->method('splitUserRemote')
|
|
->with('owner@example.com')
|
|
->willReturn(['owner', 'https://example.com/']);
|
|
|
|
$share = $this->buildShare();
|
|
|
|
$tokenEndpoint = 'https://example.com/index.php/ocm/token';
|
|
|
|
$ocmProvider = $this->createMock(IOCMProvider::class);
|
|
$ocmProvider->method('getTokenEndPoint')->willReturn($tokenEndpoint);
|
|
$ocmProvider->method('getCapabilities')->willReturn(new OCMCapabilities(['exchange-token']));
|
|
|
|
$this->discoveryService->method('discover')->willReturn($ocmProvider);
|
|
|
|
$this->urlGenerator->method('getAbsoluteURL')->willReturn('https://local.example/');
|
|
|
|
$signedOptions = [
|
|
'body' => 'grant_type=authorization_code&client_id=local.example&code=refresh-token-abc',
|
|
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded', 'Signature' => 'sig'],
|
|
'timeout' => 10,
|
|
'connect_timeout' => 10,
|
|
];
|
|
$this->signatureManager->method('signOutgoingRequestIClientPayload')
|
|
->willReturn($signedOptions);
|
|
|
|
$response = $this->createMock(IResponse::class);
|
|
$response->method('getStatusCode')->willReturn(200);
|
|
$response->method('getBody')->willReturn(json_encode([
|
|
'access_token' => 'access-token-xyz',
|
|
'token_type' => 'Bearer',
|
|
]));
|
|
|
|
$httpClient = $this->createMock(IClient::class);
|
|
$httpClient->method('post')->willReturn($response);
|
|
$this->clientService->method('newClient')->willReturn($httpClient);
|
|
|
|
$this->userManager->method('getUserNameFromLoginName')->with('localuser')->willReturn('localuser');
|
|
$this->userManager->method('get')->with('localuser')->willReturn(null);
|
|
$this->filenameValidator->method('isFilenameValid')->willReturn(true);
|
|
|
|
$this->expectException(ProviderCouldNotAddShareException::class);
|
|
$this->expectExceptionMessage('User does not exists');
|
|
|
|
$this->provider->shareReceived($share);
|
|
}
|
|
}
|