libresign/tests/php/Unit/Service/SignFileServiceTest.php
Vitor Mattos 54f73ed475
refactor: add centralized reset in getMockAppConfig for test isolation
- Add reset() method to AppConfigOverwrite that clears overWrite and deleted arrays and returns self
- Integrate reset() directly into getMockAppConfig() to ensure clean state on every call
- All tests now automatically get clean AppConfig state without explicit reset calls
- Prevents state pollution across test suites by resetting at the source
- Simplifies test code by removing need for separate reset wrapper method

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
2025-12-15 19:11:06 -03:00

1321 lines
42 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
use bovigo\vfs\vfsStream;
use OC\User\NoUserException;
use OCA\Libresign\Db\File;
use OCA\Libresign\Db\FileElement;
use OCA\Libresign\Db\FileElementMapper;
use OCA\Libresign\Db\FileMapper;
use OCA\Libresign\Db\IdDocsMapper;
use OCA\Libresign\Db\IdentifyMethod;
use OCA\Libresign\Db\IdentifyMethodMapper;
use OCA\Libresign\Db\SignRequest;
use OCA\Libresign\Db\SignRequestMapper;
use OCA\Libresign\Db\UserElement;
use OCA\Libresign\Db\UserElementMapper;
use OCA\Libresign\Events\SignedEvent;
use OCA\Libresign\Events\SignedEventFactory;
use OCA\Libresign\Exception\LibresignException;
use OCA\Libresign\Handler\DocMdpHandler;
use OCA\Libresign\Handler\FooterHandler;
use OCA\Libresign\Handler\PdfTk\Pdf;
use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
use OCA\Libresign\Handler\SignEngine\Pkcs7Handler;
use OCA\Libresign\Handler\SignEngine\SignEngineFactory;
use OCA\Libresign\Helper\JavaHelper;
use OCA\Libresign\Helper\ValidateHelper;
use OCA\Libresign\Service\FolderService;
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
use OCA\Libresign\Service\IdentifyMethodService;
use OCA\Libresign\Service\PdfSignatureDetectionService;
use OCA\Libresign\Service\SignerElementsService;
use OCA\Libresign\Service\SignFileService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use OCP\IDateTimeZone;
use OCP\IL10N;
use OCP\ITempManager;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
/**
* @group DB
*/
final class SignFileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase {
private IL10N&MockObject $l10n;
private FooterHandler&MockObject $footerHandler;
private FileMapper&MockObject $fileMapper;
private SignRequestMapper&MockObject $signRequestMapper;
private IdDocsMapper&MockObject $idDocsMapper;
private IClientService&MockObject $clientService;
private IUserManager&MockObject $userManager;
private FolderService&MockObject $folderService;
private LoggerInterface&MockObject $logger;
private IAppConfig $appConfig;
private ValidateHelper&MockObject $validateHelper;
private SignerElementsService&MockObject $signerElementsService;
private IRootFolder&MockObject $root;
private IUserSession&MockObject $userSession;
private IDateTimeZone $dateTimeZone;
private FileElementMapper&MockObject $fileElementMapper;
private UserElementMapper&MockObject $userElementMapper;
private IEventDispatcher&MockObject $eventDispatcher;
private ISecureRandom $secureRandom;
private IURLGenerator&MockObject $urlGenerator;
private IdentifyMethodMapper&MockObject $identifyMethodMapper;
private ITempManager|MockObject $tempManager;
private IdentifyMethodService&MockObject $identifyMethodService;
private ITimeFactory&MockObject $timeFactory;
private JavaHelper&MockObject $javaHelper;
private SignEngineFactory $signEngineFactory;
private SignedEventFactory&MockObject $signedEventFactory;
private Pdf&MockObject $pdf;
private DocMdpHandler $docMdpHandler;
private PdfSignatureDetectionService&MockObject $pdfSignatureDetectionService;
private \OCA\Libresign\Service\SequentialSigningService&MockObject $sequentialSigningService;
public function setUp(): void {
parent::setUp();
$this->l10n = $this->createMock(IL10N::class);
$this->l10n
->method('t')
->willReturnArgument(0);
$this->fileMapper = $this->createMock(FileMapper::class);
$this->signRequestMapper = $this->createMock(SignRequestMapper::class);
$this->idDocsMapper = $this->createMock(IdDocsMapper::class);
$this->footerHandler = $this->createMock(FooterHandler::class);
$this->clientService = $this->createMock(IClientService::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->folderService = $this->createMock(FolderService::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->appConfig = $this->getMockAppConfigWithReset();
$this->validateHelper = $this->createMock(\OCA\Libresign\Helper\ValidateHelper::class);
$this->signerElementsService = $this->createMock(SignerElementsService::class);
$this->root = $this->createMock(\OCP\Files\IRootFolder::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->dateTimeZone = \OCP\Server::get(IDateTimeZone::class);
$this->fileElementMapper = $this->createMock(FileElementMapper::class);
$this->userElementMapper = $this->createMock(UserElementMapper::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->secureRandom = \OCP\Server::get(\OCP\Security\ISecureRandom::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->identifyMethodMapper = $this->createMock(IdentifyMethodMapper::class);
$this->tempManager = $this->createMock(ITempManager::class);
$this->identifyMethodService = $this->createMock(IdentifyMethodService::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->javaHelper = $this->createMock(JavaHelper::class);
$this->signEngineFactory = \OCP\Server::get(SignEngineFactory::class);
$this->signedEventFactory = $this->createMock(SignedEventFactory::class);
$this->pdf = $this->createMock(Pdf::class);
$this->docMdpHandler = new DocMdpHandler($this->l10n);
$this->pdfSignatureDetectionService = $this->createMock(PdfSignatureDetectionService::class);
$this->sequentialSigningService = $this->createMock(\OCA\Libresign\Service\SequentialSigningService::class);
}
private function getService(array $methods = []): SignFileService|MockObject {
if ($methods) {
return $this->getMockBuilder(SignFileService::class)
->setConstructorArgs([
$this->l10n,
$this->fileMapper,
$this->signRequestMapper,
$this->idDocsMapper,
$this->footerHandler,
$this->folderService,
$this->clientService,
$this->userManager,
$this->logger,
$this->appConfig,
$this->validateHelper,
$this->signerElementsService,
$this->root,
$this->userSession,
$this->dateTimeZone,
$this->fileElementMapper,
$this->userElementMapper,
$this->eventDispatcher,
$this->secureRandom,
$this->urlGenerator,
$this->identifyMethodMapper,
$this->tempManager,
$this->identifyMethodService,
$this->timeFactory,
$this->signEngineFactory,
$this->signedEventFactory,
$this->pdf,
$this->docMdpHandler,
$this->pdfSignatureDetectionService,
$this->sequentialSigningService,
])
->onlyMethods($methods)
->getMock();
}
return new SignFileService(
$this->l10n,
$this->fileMapper,
$this->signRequestMapper,
$this->idDocsMapper,
$this->footerHandler,
$this->folderService,
$this->clientService,
$this->userManager,
$this->logger,
$this->appConfig,
$this->validateHelper,
$this->signerElementsService,
$this->root,
$this->userSession,
$this->dateTimeZone,
$this->fileElementMapper,
$this->userElementMapper,
$this->eventDispatcher,
$this->secureRandom,
$this->urlGenerator,
$this->identifyMethodMapper,
$this->tempManager,
$this->identifyMethodService,
$this->timeFactory,
$this->signEngineFactory,
$this->signedEventFactory,
$this->pdf,
$this->docMdpHandler,
$this->pdfSignatureDetectionService,
$this->sequentialSigningService,
);
}
public function testCanDeleteRequestSignatureWhenDocumentAlreadySigned():void {
$file = $this->createMock(\OCA\Libresign\Db\File::class);
$file->method('__call')->with($this->equalTo('getId'))->willReturn(1);
$this->fileMapper->method('getByUuid')->willReturn($file);
$signRequest = $this->createMock(\OCA\Libresign\Db\SignRequest::class);
$signRequest
->method('__call')
->willReturnCallback(fn (string $method)
=> match ($method) {
'getSigned' => '2021-01-01 01:01:01',
}
);
$this->signRequestMapper->method('getByFileUuid')->willReturn([$signRequest]);
$this->expectExceptionMessage('Document already signed');
$this->getService()->canDeleteRequestSignature(['uuid' => 'valid']);
}
public function testCanDeleteRequestSignatureWhenNoSignatureWasRequested():void {
$file = $this->createMock(\OCA\Libresign\Db\File::class);
$file->method('__call')->with($this->equalTo('getId'))->willReturn(1);
$this->fileMapper->method('getByUuid')->willReturn($file);
$signRequest = $this->createMock(\OCA\Libresign\Db\SignRequest::class);
$signRequest
->method('__call')
->willReturnCallback(fn (string $method)
=> match ($method) {
'getSigned' => null,
'getId' => 171,
}
);
$this->signRequestMapper->method('getByFileUuid')->willReturn([$signRequest]);
$this->expectExceptionMessage('No signature was requested to %');
$this->getService()->canDeleteRequestSignature([
'uuid' => 'valid',
'users' => [
[
'email' => 'test@test.coop'
]
]
]);
}
public function testNotifyCallback():void {
$libreSignFile = new \OCA\Libresign\Db\File();
$libreSignFile->setCallback('https://test.coop');
$service = $this->getService();
$service->setLibreSignFile($libreSignFile);
$file = $this->createMock(\OCP\Files\File::class);
$actual = $service->notifyCallback($file);
$this->assertNull($actual);
}
public function testSignWithFileNotFound():void {
$this->expectExceptionMessage('File not found');
$file = new \OCA\Libresign\Db\File();
$file->setUserId('username');
$this->root->method('getUserFolder')
->willReturn($this->root);
$signRequest = new \OCA\Libresign\Db\SignRequest();
$this->getService()
->setLibreSignFile($file)
->setSignRequest($signRequest)
->setPassword('password')
->sign();
}
#[DataProvider('dataSignGenerateASha256OfSignedFile')]
public function testSignGenerateASha256OfSignedFile(string $signedContent):void {
$service = $this->getService([
'getEngine',
'setNewStatusIfNecessary',
'getNextcloudFile',
'validateDocMdpAllowsSignatures',
]);
$nextcloudFile = $this->createMock(\OCP\Files\File::class);
$nextcloudFile->method('getContent')->willReturn($signedContent);
$service->method('getNextcloudFile')->willReturn($nextcloudFile);
$service->method('validateDocMdpAllowsSignatures');
$pkcs12Handler = $this->createMock(Pkcs12Handler::class);
$pkcs12Handler->method('sign')->willReturn($nextcloudFile);
$service->method('getEngine')->willReturn($pkcs12Handler);
$expectedHash = hash('sha256', $signedContent);
$totalCalls = 0;
$hashCallback = function ($method, $args) use ($expectedHash, &$totalCalls) {
switch ($method) {
case 'setSignedHash':
$this->assertEquals($expectedHash, $args[0], 'Hash of signed file should match expected SHA-256 value');
$totalCalls++;
break;
case 'getFileId':
return 1;
case 'getSigningOrder':
return 1;
case 'getDocmdpLevelEnum':
return \OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED;
default: return null;
}
};
$signRequest = $this->createMock(SignRequest::class);
$signRequest->method('__call')->willReturnCallback($hashCallback);
$libreSignFile = $this->createMock(\OCA\Libresign\Db\File::class);
$libreSignFile->method('__call')->willReturnCallback($hashCallback);
$service
->setSignRequest($signRequest)
->setLibreSignFile($libreSignFile)
->sign();
$this->assertEquals(2, $totalCalls, 'setSignedHash should be called twice');
}
public static function dataSignGenerateASha256OfSignedFile(): array {
return [
['signed content'],
['another signed content'],
];
}
public function testUpdateDatabaseWhenSign(): void {
$service = $this->getService([
'getEngine',
'setNewStatusIfNecessary',
'computeHash',
'getNextcloudFile',
'validateDocMdpAllowsSignatures',
]);
$nextcloudFile = $this->createMock(\OCP\Files\File::class);
$nextcloudFile->method('getContent')->willReturn('pdf content');
$service->method('getNextcloudFile')->willReturn($nextcloudFile);
$service->method('validateDocMdpAllowsSignatures');
$this->fileMapper->expects($this->once())->method('update');
$this->signRequestMapper->expects($this->once())->method('update');
$signRequest = $this->createMock(SignRequest::class);
$signRequest->method('__call')->willReturnCallback(function ($method, $args) {
switch ($method) {
case 'getFileId':
return 1;
case 'getSigningOrder':
return 1;
default: return null;
}
});
$libreSignFile = $this->createMock(\OCA\Libresign\Db\File::class);
$libreSignFile->method('__call')->willReturnCallback(function ($method) {
if ($method === 'getDocmdpLevelEnum') {
return \OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED;
}
return null;
});
$service
->setSignRequest($signRequest)
->setLibreSignFile($libreSignFile)
->sign();
}
public function testDispatchEventWhenSign(): void {
$service = $this->getService([
'getEngine',
'setNewStatusIfNecessary',
'computeHash',
'getNextcloudFile',
'validateDocMdpAllowsSignatures',
]);
$nextcloudFile = $this->createMock(\OCP\Files\File::class);
$nextcloudFile->method('getContent')->willReturn('pdf content');
$service->method('getNextcloudFile')->willReturn($nextcloudFile);
$service->method('validateDocMdpAllowsSignatures');
$this->eventDispatcher
->expects($this->once())
->method('dispatchTyped')
->with($this->isInstanceOf(SignedEvent::class));
$signRequest = $this->createMock(SignRequest::class);
$signRequest->method('__call')->willReturnCallback(function ($method, $args) {
switch ($method) {
case 'getFileId':
return 1;
case 'getSigningOrder':
return 1;
default: return null;
}
});
$libreSignFile = $this->createMock(\OCA\Libresign\Db\File::class);
$libreSignFile->method('__call')->willReturnCallback(function ($method) {
if ($method === 'getDocmdpLevelEnum') {
return \OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED;
}
return null;
});
$service
->setSignRequest($signRequest)
->setLibreSignFile($libreSignFile)
->sign();
}
#[DataProvider('providerCheckStatusAfterSign')]
public function testCheckStatusAfterSign(array $inputSigners, int $fileStatus, int $finalStatus): void {
$service = $this->getService([
'getEngine',
'computeHash',
'getSigners',
'getNextcloudFile',
]);
$nextcloudFile = $this->createMock(\OCP\Files\File::class);
$nextcloudFile->method('getContent')->willReturn('pdf content');
$service->method('getNextcloudFile')->willReturn($nextcloudFile);
$service->method('getSigners')->willReturn($inputSigners);
$signRequestCallback = function ($method, $args) {
switch ($method) {
case 'getFileId':
return 1;
case 'getSigningOrder':
return 1;
default: return null;
}
};
$signRequest = $this->createMock(SignRequest::class);
$signRequest->method('__call')->willReturnCallback($signRequestCallback);
$libreSignFile = new \OCA\Libresign\Db\File();
$libreSignFile->setStatus($fileStatus);
$libreSignFile->resetUpdatedFields();
$service
->setSignRequest($signRequest)
->setLibreSignFile($libreSignFile)
->sign();
$this->assertEquals($finalStatus, $libreSignFile->getStatus());
$updatedFields = $libreSignFile->getUpdatedFields();
if ($fileStatus !== $finalStatus) {
$this->assertArrayHasKey('status', $updatedFields);
$this->assertTrue($updatedFields['status']);
} else {
$this->assertArrayNotHasKey('status', $updatedFields);
}
}
public static function providerCheckStatusAfterSign(): array {
return [
[self::generateSigners(5, 1), File::STATUS_ABLE_TO_SIGN, File::STATUS_PARTIAL_SIGNED],
[self::generateSigners(5, 1), File::STATUS_PARTIAL_SIGNED, File::STATUS_PARTIAL_SIGNED],
[self::generateSigners(5, 5), File::STATUS_ABLE_TO_SIGN, File::STATUS_SIGNED],
[self::generateSigners(3, 0), File::STATUS_ABLE_TO_SIGN, File::STATUS_ABLE_TO_SIGN],
[self::generateSigners(3, 3), File::STATUS_PARTIAL_SIGNED, File::STATUS_SIGNED],
[self::generateSigners(2, 2), File::STATUS_SIGNED, File::STATUS_SIGNED],
[self::generateSigners(4, 3), File::STATUS_ABLE_TO_SIGN, File::STATUS_PARTIAL_SIGNED],
[self::generateSigners(4, 4), File::STATUS_PARTIAL_SIGNED, File::STATUS_SIGNED],
[self::generateSigners(1, 1), File::STATUS_ABLE_TO_SIGN, File::STATUS_SIGNED],
[self::generateSigners(0, 0), File::STATUS_ABLE_TO_SIGN, File::STATUS_ABLE_TO_SIGN],
];
}
private static function generateSigners(int $total, int $signed): array {
$signers = [];
for ($i = 0; $i < $total; $i ++) {
$signers[] = new SignRequest();
}
for ($i = 0; $i < $signed; $i ++) {
$signers[$i]->setSigned(new DateTime());
}
return $signers;
}
#[DataProvider('providerGetEngineWillWorkWithLazyLoadedEngine')]
public function testGetEngineWillWorkWithLazyLoadedEngine(string $extension, string $instanceOf): void {
$service = $this->getService([
'updateSignRequest',
'updateLibreSignFile',
'dispatchSignedEvent',
'getFileToSign',
'configureEngine',
'getSignatureParams',
]);
$file = $this->createMock(\OCP\Files\File::class);
$file->method('getExtension')->willReturn($extension);
$service->method('getFileToSign')->willReturn($file);
$engine = self::invokePrivate($service, 'getEngine');
$this->assertInstanceOf($instanceOf, $engine);
}
public static function providerGetEngineWillWorkWithLazyLoadedEngine(): array {
return [
['pdf', Pkcs12Handler::class],
['PDF', Pkcs12Handler::class],
['odt', Pkcs7Handler::class],
['ODT', Pkcs7Handler::class],
['jpg', Pkcs7Handler::class],
['JPG', Pkcs7Handler::class],
['png', Pkcs7Handler::class],
['PNG', Pkcs7Handler::class],
['txt', Pkcs7Handler::class],
['TXT', Pkcs7Handler::class],
];
}
#[DataProvider('providerGetOrGeneratePfxContent')]
public function testGetOrGeneratePfxContent(bool $signWithoutPassword, string $occurrency): void {
$service = $this->getService([
'getFileToSign',
'identifyEngine',
'generateTemporaryPassword',
'computeHash',
'updateSignRequest',
'updateLibreSignFile',
'dispatchSignedEvent',
'validateDocMdpAllowsSignatures',
]);
$signEngineHandler = $this->getMockBuilder(Pkcs12Handler::class)
->disableOriginalConstructor()
->onlyMethods([
'getCertificate',
'getPfxOfCurrentSigner',
'generateCertificate',
'sign',
])
->getMock();
$signEngineHandler->expects($this->{$occurrency}())->method('generateCertificate');
$service->method('identifyEngine')->willReturn($signEngineHandler);
$service
->setSignWithoutPassword($signWithoutPassword)
->sign();
}
public static function providerGetOrGeneratePfxContent(): array {
return [
[true, 'once'],
[false, 'never'],
];
}
#[DataProvider('providerStoreUserMetadata')]
public function testStoreUserMetadata(bool $collectMetadata, ?array $previous, array $new, ?array $expected): void {
$signRequest = new \OCA\Libresign\Db\SignRequest();
$this->appConfig->setValueBool('libresign', 'collect_metadata', $collectMetadata);
$signRequest->setMetadata($previous);
$this->getService()
->setSignRequest($signRequest)
->storeUserMetadata($new);
$this->assertEquals(
$expected,
$signRequest->getMetadata()
);
}
public static function providerStoreUserMetadata(): array {
return [
// don't collect metadata
[false, null, [], null],
[false, null, ['b' => 2], null],
[false, null, ['b' => null], null],
[false, null, ['b' => []], null],
[false, null, ['b' => ['']], null],
[false, null, ['b' => ['b' => 1]], null],
// collect metadata without previous value
[true, null, [], null],
[true, null, ['b' => 2], ['b' => 2]],
[true, null, ['b' => null], ['b' => null]],
[true, null, ['b' => []], ['b' => []]],
[true, null, ['b' => ['']], ['b' => ['']]],
[true, null, ['b' => ['b' => 1]], ['b' => ['b' => 1]]],
// collect metadata with previous value
[true, ['a' => 1], ['a' => 2], ['a' => 2]],
[true, ['a' => 1], ['a' => null], ['a' => null]],
[true, ['a' => 1], ['a' => []], ['a' => []]],
[true, ['a' => 1], ['a' => ['']], ['a' => ['']]],
[true, ['a' => 1], ['a' => ['b' => 1]], ['a' => ['b' => 1]]],
[true, ['a' => 1], ['b' => 2], ['a' => 1, 'b' => 2]],
];
}
private function createSignRequestMock(array $methods): MockObject {
$signRequest = $this->createMock(SignRequest::class);
$signRequest->method('__call')->willReturnCallback(fn (string $method)
=> $methods[$method] ?? null
);
return $signRequest;
}
#[DataProvider('providerGetSignatureParamsCommonName')]
public function testGetSignatureParamsCommonName(
array $certData,
string $expectedIssuerCN,
string $expectedSignerCN,
): void {
$service = $this->getService(['readCertificate']);
$libreSignFile = new \OCA\Libresign\Db\File();
$libreSignFile->setUuid('uuid');
$service->setLibreSignFile($libreSignFile);
$service->method('readCertificate')->willReturn($certData);
$service->setCurrentUser(null);
$signRequest = $this->createSignRequestMock([
'getId' => 171,
'getMetadata' => [],
]);
$service->setSignRequest($signRequest);
$actual = $this->invokePrivate($service, 'getSignatureParams');
$this->assertEquals($expectedIssuerCN, $actual['IssuerCommonName']);
$this->assertEquals($expectedSignerCN, $actual['SignerCommonName']);
$this->assertEquals('uuid', $actual['DocumentUUID']);
$this->assertArrayHasKey('DocumentUUID', $actual);
$this->assertArrayHasKey('LocalSignerTimezone', $actual);
$this->assertArrayHasKey('LocalSignerSignatureDateTime', $actual);
}
public static function providerGetSignatureParamsCommonName(): array {
return [
'simple CNs' => [
[
'issuer' => ['CN' => 'LibreCode'],
'subject' => ['CN' => 'Jane Doe'],
],
'LibreCode',
'Jane Doe',
],
'empty CNs' => [
[
'issuer' => ['CN' => ''],
'subject' => ['CN' => ''],
],
'',
'',
],
];
}
#[DataProvider('providerGetSignatureParamsSignerEmail')]
public function testGetSignatureParamsSignerEmail(
array $certData,
string $authenticatedUserEmail,
array $expected,
): void {
$libreSignFile = new \OCA\Libresign\Db\File();
$libreSignFile->setUuid('uuid');
$service = $this->getService(['readCertificate']);
$service->method('readCertificate')
->willReturn($certData);
$service->setLibreSignFile($libreSignFile);
$signRequest = $this->createMock(SignRequest::class);
$signRequest
->method('__call')
->willReturnCallback(fn (string $method)
=> match ($method) {
'getId' => 171,
'getMetadata' => [],
}
);
$service->setSignRequest($signRequest);
if ($authenticatedUserEmail) {
$user = $this->createMock(\OCP\IUser::class);
$user->method('getEMailAddress')->willReturn($authenticatedUserEmail);
} else {
$user = null;
}
$service->setCurrentUser($user);
$actual = $this->invokePrivate($service, 'getSignatureParams');
if (isset($expected['SignerEmail'])) {
$this->assertArrayHasKey('SignerEmail', $actual);
$this->assertEquals($expected['SignerEmail'], $actual['SignerEmail']);
} else {
$this->assertArrayNotHasKey('SignerEmail', $actual);
}
}
public static function providerGetSignatureParamsSignerEmail(): array {
return [
[
[], '', [],
],
[
[
'extensions' => [
'subjectAltName' => '',
],
],
'',
[
],
],
[
[
'extensions' => [
'subjectAltName' => 'email:test@email.coop',
],
],
'',
[
'SignerEmail' => 'test@email.coop',
],
],
[
[
'extensions' => [
'subjectAltName' => 'email:test@email.coop,otherinfo',
],
],
'',
[
'SignerEmail' => 'test@email.coop',
],
],
[
[
'extensions' => [
'subjectAltName' => 'otherinfo,email:test@email.coop',
],
],
'',
[
'SignerEmail' => 'test@email.coop',
],
],
[
[
'extensions' => [
'subjectAltName' => 'otherinfo,email:test@email.coop,moreinfo',
],
],
'',
[
'SignerEmail' => 'test@email.coop',
],
],
[
[
'extensions' => [
'subjectAltName' => 'test@email.coop',
],
],
'',
[
'SignerEmail' => 'test@email.coop',
],
],
[
[],
'test@email.coop',
[
'SignerEmail' => 'test@email.coop',
],
],
];
}
#[DataProvider('providerGetSignatureParamsSignerEmailFallback')]
public function testGetSignatureParamsSignerEmailFallback(
string $methodName,
string $email,
): void {
$service = $this->getService(['readCertificate']);
$signRequest = $this->createMock(SignRequest::class);
$signRequest->method('__call')->willReturn(171);
$service->setSignRequest($signRequest);
$identifyMethod = $this->createMock(IIdentifyMethod::class);
$identifyMethod->method('getName')->willReturn($methodName);
$entity = new IdentifyMethod();
$entity->setIdentifierValue($email);
$identifyMethod->method('getEntity')->willReturn($entity);
$this->identifyMethodService->method('getIdentifiedMethod')->willReturn($identifyMethod);
$actual = $this->invokePrivate($service, 'getSignatureParams');
if (empty($email)) {
$this->assertArrayNotHasKey('SignerEmail', $actual);
} else {
$this->assertArrayHasKey('SignerEmail', $actual);
$this->assertEquals($email, $actual['SignerEmail']);
}
}
public static function providerGetSignatureParamsSignerEmailFallback(): array {
return [
['account', '',],
['email', 'signer@email.tld',],
];
}
#[DataProvider('providerGetSignatureParamsMetadata')]
public function testGetSignatureParamsMetadata(
array $metadata,
array $expected,
): void {
$service = $this->getService(['readCertificate']);
$service->method('readCertificate')->willReturn([]);
$signRequest = $this->createMock(SignRequest::class);
$signRequest
->method('__call')
->willReturnCallback(fn (string $method)
=> match ($method) {
'getId' => 171,
'getMetadata' => $metadata,
}
);
$service->setSignRequest($signRequest);
$actual = $this->invokePrivate($service, 'getSignatureParams');
if (empty($expected)) {
$this->assertArrayNotHasKey('SignerIP', $actual);
$this->assertArrayNotHasKey('SignerUserAgent', $actual);
return;
}
if (isset($expected['SignerIP'])) {
$this->assertArrayHasKey('SignerIP', $actual);
$this->assertEquals($expected['SignerIP'], $actual['SignerIP']);
} else {
$this->assertArrayNotHasKey('SignerIP', $actual);
}
if (isset($expected['SignerUserAgent'])) {
$this->assertArrayHasKey('SignerUserAgent', $actual);
$this->assertEquals($expected['SignerUserAgent'], $actual['SignerUserAgent']);
} else {
$this->assertArrayNotHasKey('SignerUserAgent', $actual);
}
}
public static function providerGetSignatureParamsMetadata(): array {
return [
[[], []],
[
[
'remote-address' => '',
'user-agent' => '',
],
[
'SignerIP' => '',
'SignerUserAgent' => '',
],
],
[
[
'remote-address' => '127.0.0.1',
'user-agent' => 'Robot',
],
[
'SignerIP' => '127.0.0.1',
'SignerUserAgent' => 'Robot',
],
],
];
}
#[DataProvider('providerSetVisibleElements')]
public function testSetVisibleElements(
array $signerList,
array $fileElements,
array $tempFiles,
array $signatureFile,
bool $canCreateSignature,
?string $exception,
bool $isAuthenticatedSigner,
): void {
$service = $this->getService();
$signRequest = $this->createMock(SignRequest::class);
$signRequest
->method('__call')
->willReturnCallback(fn (string $method)
=> match ($method) {
'getFileId' => 171,
'getId' => 171,
}
);
$service->setSignRequest($signRequest);
$fileElements = array_map(function ($value) {
$fileElement = new FileElement();
$fileElement->setId($value['id']);
return $fileElement;
}, $fileElements);
$this->fileElementMapper->method('getByFileIdAndSignRequestId')->willReturn($fileElements);
$this->signerElementsService->method('canCreateSignature')->willReturn($canCreateSignature);
$this->userElementMapper->method('findOne')->willReturnCallback(function () use ($signatureFile) {
if (!empty($signatureFile)) {
$userElement = new UserElement();
$userElement->setFileId(1);
return $userElement;
}
throw new DoesNotExistException('User element not found');
});
$this->folderService->method('getFileById')
->willReturnCallback(function ($id) use ($signatureFile) {
if (isset($signatureFile[$id]) && $signatureFile[$id]['valid']) {
$file = $this->getMockBuilder(\OCP\Files\File::class)->getMock();
$file->method('getContent')->willReturn($signatureFile[$id]['content']);
return $file;
}
throw new NotFoundException();
});
vfsStream::setup('home');
$this->tempManager->method('getTemporaryFile')
->willReturnCallback(function ($postFix) {
preg_match('/.*(\d+).*/', $postFix, $matches);
$path = 'vfs://home/_' . $matches[1] . '.png';
return $path;
});
if ($exception) {
$this->expectException($exception);
}
if ($isAuthenticatedSigner) {
$currentUser = $this->createMock(\OCP\IUser::class);
}
$service->setCurrentUser($currentUser ?? null);
$service->setVisibleElements($signerList);
if (!$exception) {
$visibleElements = $service->getVisibleElements();
$this->assertCount(count($fileElements), $visibleElements);
foreach ($fileElements as $key => $element) {
$this->assertArrayHasKey($key, $visibleElements);
$this->assertSame($element, $visibleElements[$key]->getFileElement());
$this->assertEquals(
isset($signerList[$key], $signerList[$key]['profileNodeId'], $tempFiles[$signerList[$key]['profileNodeId']])
? $tempFiles[$signerList[$key]['profileNodeId']] . '/_' . $signerList[$key]['profileNodeId'] . '.png'
: '',
$visibleElements[$key]->getTempFile(),
);
}
}
}
public static function providerSetVisibleElements(): array {
$validDocumentId = 171;
$validProfileNodeId = 1;
$vfsPath = 'vfs://home';
return [
'empty list, can create signature' => self::createScenarioSetVisibleElements(
signerList: [],
fileElements: [],
tempFiles: [],
signatureFile: [],
canCreateSignature: true,
isAuthenticatedSigner: true,
),
'empty list, cannot create signature' => self::createScenarioSetVisibleElements(
signerList: [],
fileElements: [],
tempFiles: [],
signatureFile: [],
canCreateSignature: false,
isAuthenticatedSigner: true,
),
'valid signer with signature file, valid content of signature file' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId, 'profileNodeId' => $validProfileNodeId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => $vfsPath],
signatureFile: [$validProfileNodeId => ['valid' => true, 'content' => 'valid content']],
canCreateSignature: true,
isAuthenticatedSigner: true,
),
'valid signer with signature file, invalid content of signature file' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId, 'profileNodeId' => $validProfileNodeId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => false],
signatureFile: [$validProfileNodeId => ['valid' => true, 'content' => '']],
canCreateSignature: true,
isAuthenticatedSigner: true,
expectedException: LibresignException::class,
),
'unauthenticated signer without profileNodeId' => self::createScenarioSetVisibleElements(
signerList: [],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => $vfsPath],
signatureFile: [$validProfileNodeId => ['valid' => true, 'content' => 'valid content']],
canCreateSignature: true,
isAuthenticatedSigner: false,
expectedException: LibresignException::class,
),
'invalid signature file, with invalid field' => self::createScenarioSetVisibleElements(
signerList: [
['fake' => 'value', 'profileNodeId' => $validProfileNodeId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => $vfsPath],
signatureFile: [$validProfileNodeId => ['valid' => false, 'content' => 'valid content']],
canCreateSignature: true,
isAuthenticatedSigner: true,
expectedException: LibresignException::class,
),
'invalid signature file, with invalid user element' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId, 'profileNodeId' => $validProfileNodeId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => $vfsPath],
signatureFile: [$validProfileNodeId => ['valid' => false, 'content' => 'valid content']],
canCreateSignature: true,
isAuthenticatedSigner: true,
expectedException: LibresignException::class,
),
'invalid signature file, with invalid type of profileNodeId' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId, 'profileNodeId' => 'not-a-number'],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => $vfsPath],
signatureFile: [$validProfileNodeId => ['valid' => false, 'content' => 'valid content']],
canCreateSignature: true,
isAuthenticatedSigner: true,
expectedException: LibresignException::class,
),
'invalid signature file' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId, 'profileNodeId' => $validProfileNodeId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => $vfsPath],
signatureFile: [$validProfileNodeId => ['valid' => false, 'content' => 'valid content']],
canCreateSignature: true,
isAuthenticatedSigner: true,
expectedException: LibresignException::class,
),
'missing profileNodeId throws exception' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [],
signatureFile: [],
canCreateSignature: true,
isAuthenticatedSigner: true,
expectedException: LibresignException::class,
),
'cannot create signature, visible element fallback' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [],
signatureFile: [],
canCreateSignature: false,
isAuthenticatedSigner: true,
),
'no authenticated user, missing session file' => self::createScenarioSetVisibleElements(
signerList: [['documentElementId' => $validDocumentId, 'profileNodeId' => $validProfileNodeId]],
fileElements: [['id' => $validDocumentId]],
tempFiles: [],
signatureFile: [],
canCreateSignature: true,
isAuthenticatedSigner: false,
expectedException: LibresignException::class,
),
'user fallback with valid user element' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId, 'profileNodeId' => $validProfileNodeId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [$validProfileNodeId => $vfsPath],
signatureFile: [$validProfileNodeId => ['valid' => true, 'content' => 'valid content']],
canCreateSignature: true,
isAuthenticatedSigner: true,
),
'authenticated user, file not found' => self::createScenarioSetVisibleElements(
signerList: [
['documentElementId' => $validDocumentId, 'profileNodeId' => $validProfileNodeId],
],
fileElements: [
['id' => $validDocumentId],
],
tempFiles: [],
signatureFile: [],
canCreateSignature: true,
isAuthenticatedSigner: true,
expectedException: LibresignException::class,
),
];
}
private static function createScenarioSetVisibleElements(
array $signerList = [],
array $fileElements = [],
array $tempFiles = [],
array $signatureFile = [],
bool $canCreateSignature = false,
bool $isAuthenticatedSigner = false,
?string $expectedException = null,
): array {
return [
$signerList,
$fileElements,
$tempFiles,
$signatureFile,
$canCreateSignature,
$expectedException,
$isAuthenticatedSigner,
];
}
#[DataProvider('providerGetSignedFile')]
public function testGetSignedFile(
int $timesCalled,
string $managerUid,
?string $ownerUid = null,
?int $nodeId = null,
): void {
$service = $this->getService(['getNodeByIdUsingUid']);
$libreSignFile = new \OCA\Libresign\Db\File();
$libreSignFile->setSignedNodeId($nodeId);
$libreSignFile->setUserId($managerUid);
$service->setLibreSignFile($libreSignFile);
$fileToSign = $this->createMock(\OCP\Files\File::class);
$user = $this->createMock(\OCP\IUser::class);
$user->method('getUID')->willReturn($ownerUid);
$fileToSign->method('getOwner')->willReturn($user);
$service
->expects($this->exactly($timesCalled))
->method('getNodeByIdUsingUid')
->willReturn($fileToSign);
$this->invokePrivate($service, 'getSignedFile');
}
public static function providerGetSignedFile(): array {
return [
[0, 'managerUid', '', null],
[1, 'managerUid', 'managerUid', 1],
[2, 'managerUid', 'johndoe', 1],
];
}
#[DataProvider('providerGetNodeByIdUsingUid')]
public function testGetNodeByIdUsingUid(
string $typeOfNode,
string $exceptionMessage,
): void {
$service = $this->getService();
if ($exceptionMessage) {
$this->expectExceptionMessageMatches($exceptionMessage);
}
$leaf = $this->createMock($typeOfNode);
$userFolder = $this->createMock(\OCP\Files\Folder::class);
$userFolder->method('getFirstNodeById')->willReturn($leaf);
$this->root->method('getUserFolder')->willReturnCallback(function () use ($userFolder, $exceptionMessage) {
switch ($exceptionMessage) {
case '/User not found/':
throw new NoUserException();
case '/not have permission/':
throw new NotPermittedException();
case '/File not found/':
return $userFolder;
default:
return $userFolder;
}
});
$actual = $this->invokePrivate($service, 'getNodeByIdUsingUid', ['', 1]);
$this->assertEquals($leaf, $actual);
}
public static function providerGetNodeByIdUsingUid(): array {
return [
[\OCP\Files\Folder::class, '/User not found/'],
[\OCP\Files\Folder::class, '/not have permission/'],
[\OCP\Files\Folder::class, '/File not found/'],
[\OCP\Files\File::class, ''],
];
}
public function testSignThrowsExceptionWhenDocMdpLevel1Detected(): void {
$this->expectException(LibresignException::class);
$service = $this->getService(['getNextcloudFile', 'getEngine']);
$nextcloudFile = $this->createMock(\OCP\Files\File::class);
$nextcloudFile->method('getContent')->willReturn(file_get_contents(__DIR__ . '/../../fixtures/pdfs/real_jsignpdf_level1.pdf'));
$service->method('getNextcloudFile')->willReturn($nextcloudFile);
$engineMock = $this->createMock(Pkcs12Handler::class);
$service->method('getEngine')->willReturn($engineMock);
$signRequest = $this->createMock(SignRequest::class);
$signRequest->method('__call')->willReturnCallback(function ($method) {
switch ($method) {
case 'getFileId':
return 1;
case 'getSigningOrder':
return 1;
default: return null;
}
});
$libreSignFile = $this->createMock(\OCA\Libresign\Db\File::class);
$libreSignFile->method('getDocmdpLevelEnum')->willReturn(\OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED);
$service
->setSignRequest($signRequest)
->setLibreSignFile($libreSignFile)
->sign();
}
#[DataProvider('provideValidateDocMdpAllowsSignaturesScenarios')]
public function testValidateDocMdpAllowsSignaturesWithVariousPdfFixtures(
callable $pdfContentGenerator,
bool $shouldThrowException,
): void {
if (!$shouldThrowException) {
$this->expectNotToPerformAssertions();
} else {
$this->expectException(LibresignException::class);
}
$service = $this->getService(['getLibreSignFileAsResource']);
$pdfContent = $pdfContentGenerator($this);
$resource = fopen('php://memory', 'r+');
fwrite($resource, $pdfContent);
rewind($resource);
$service->method('getLibreSignFileAsResource')->willReturn($resource);
$libreSignFile = $this->createMock(\OCA\Libresign\Db\File::class);
$libreSignFile->method('getDocmdpLevelEnum')->willReturn(\OCA\Libresign\Enum\DocMdpLevel::NOT_CERTIFIED);
$service->setLibreSignFile($libreSignFile);
self::invokePrivate($service, 'validateDocMdpAllowsSignatures');
}
public static function provideValidateDocMdpAllowsSignaturesScenarios(): array {
return [
'Unsigned PDF - should NOT throw exception' => [
'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createMinimalPdf(),
'shouldThrowException' => false,
],
'DocMDP level 0 (not certified) - should NOT throw exception' => [
'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(0, false),
'shouldThrowException' => false,
],
'DocMDP level 1 (no changes allowed) - SHOULD throw exception' => [
'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(1, false),
'shouldThrowException' => true,
],
'DocMDP level 2 (form filling allowed) - should NOT throw exception' => [
'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(2, false),
'shouldThrowException' => false,
],
'DocMDP level 3 (annotations allowed) - should NOT throw exception' => [
'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(3, false),
'shouldThrowException' => false,
],
'DocMDP level 1 with modifications - SHOULD throw exception' => [
'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(1, true),
'shouldThrowException' => true,
],
];
}
}