diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 7a06e705d0..70580b4318 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -8,7 +8,7 @@ namespace OCA\Libresign; -use OCA\Libresign\Service\EnvelopeService; +use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\SignatureTextService; use OCA\Libresign\Service\SignerElementsService; use OCP\App\IAppManager; diff --git a/lib/Service/Envelope/EnvelopeFileRelocator.php b/lib/Service/Envelope/EnvelopeFileRelocator.php new file mode 100644 index 0000000000..0e94d29b00 --- /dev/null +++ b/lib/Service/Envelope/EnvelopeFileRelocator.php @@ -0,0 +1,45 @@ +folderService->setUserId($userManager->getUID()); + $userRootFolder = $this->folderService->getUserRootFolder(); + $envelopeFolder = $userRootFolder->getFirstNodeById($envelopeFolderId); + + if (!$envelopeFolder instanceof \OCP\Files\Folder) { + throw new LibresignException('Envelope folder not found'); + } + + if ($this->isNodeInsideFolder($sourceNode, $envelopeFolder)) { + return $sourceNode; + } + + if (!$sourceNode instanceof \OCP\Files\File) { + throw new LibresignException('Invalid file type for envelope'); + } + + return $envelopeFolder->newFile($sourceNode->getName(), $sourceNode->getContent()); + } + + private function isNodeInsideFolder(Node $node, \OCP\Files\Folder $folder): bool { + return str_starts_with($node->getPath(), $folder->getPath() . '/'); + } +} diff --git a/lib/Service/Envelope/EnvelopeService.php b/lib/Service/Envelope/EnvelopeService.php new file mode 100644 index 0000000000..8871030e69 --- /dev/null +++ b/lib/Service/Envelope/EnvelopeService.php @@ -0,0 +1,139 @@ +appConfig->getValueBool(Application::APP_ID, 'envelope_enabled', true); + } + + /** + * @throws LibresignException + */ + public function validateEnvelopeConstraints(int $fileCount): void { + if (!$this->isEnabled()) { + throw new LibresignException($this->l10n->t('Envelope feature is disabled')); + } + + $maxFiles = $this->getMaxFilesPerEnvelope(); + if ($fileCount > $maxFiles) { + throw new LibresignException( + $this->l10n->t('Maximum number of files per envelope (%s) exceeded', [$maxFiles]) + ); + } + } + + public function createEnvelope( + string $name, + string $userId, + int $filesCount = 0, + ?string $path = null, + ): FileEntity { + $this->folderService->setUserId($userId); + + $uuid = UUIDUtil::getUUID(); + if ($path) { + $envelopeFolder = $this->folderService->getOrCreateFolderByAbsolutePath($path); + } else { + $parentFolder = $this->folderService->getFolder(); + $folderName = $name . '_' . $uuid; + $envelopeFolder = $parentFolder->newFolder($folderName); + } + + $envelope = new FileEntity(); + $envelope->setNodeId($envelopeFolder->getId()); + $envelope->setNodeTypeEnum(NodeType::ENVELOPE); + $envelope->setName($name); + $envelope->setUuid($uuid); + $envelope->setCreatedAt(new DateTime()); + $envelope->setStatus(FileEntity::STATUS_DRAFT); + + $envelope->setMetadata(['filesCount' => $filesCount]); + + if ($userId) { + $envelope->setUserId($userId); + } + + return $this->fileMapper->insert($envelope); + } + + public function addFileToEnvelope(int $envelopeId, FileEntity $file): FileEntity { + $envelope = $this->fileMapper->getById($envelopeId); + + if (!$envelope->isEnvelope()) { + throw new LibresignException($this->l10n->t('The specified ID is not an envelope')); + } + + if ($envelope->getStatus() > FileEntity::STATUS_DRAFT) { + throw new LibresignException($this->l10n->t('Cannot add files to an envelope that is already in signing process')); + } + + $maxFiles = $this->getMaxFilesPerEnvelope(); + $currentCount = $this->fileMapper->countChildrenFiles($envelopeId); + if ($currentCount >= $maxFiles) { + throw new LibresignException( + $this->l10n->t('Maximum number of files per envelope (%s) exceeded', [$maxFiles]) + ); + } + + $file->setParentFileId($envelopeId); + $file->setNodeTypeEnum(NodeType::FILE); + + return $this->fileMapper->update($file); + } + + public function getEnvelopeByFileId(int $fileId): ?FileEntity { + try { + return $this->fileMapper->getParentEnvelope($fileId); + } catch (DoesNotExistException) { + return null; + } + } + + public function getEnvelopeFolder(FileEntity $envelope): \OCP\Files\Folder { + $userId = $envelope->getUserId(); + if (!$userId) { + throw new LibresignException('Envelope does not have a user'); + } + + $this->folderService->setUserId($userId); + $userRootFolder = $this->folderService->getUserRootFolder(); + + $envelopeFolderNode = $userRootFolder->getFirstNodeById($envelope->getNodeId()); + if (!$envelopeFolderNode instanceof \OCP\Files\Folder) { + throw new LibresignException('Envelope folder not found'); + } + + return $envelopeFolderNode; + } + + private function getMaxFilesPerEnvelope(): int { + return $this->appConfig->getValueInt(Application::APP_ID, 'envelope_max_files', 50); + } +} diff --git a/lib/Service/EnvelopeFileRelocator.php b/lib/Service/EnvelopeFileRelocator.php new file mode 100644 index 0000000000..1c94fbf79f --- /dev/null +++ b/lib/Service/EnvelopeFileRelocator.php @@ -0,0 +1,44 @@ +folderService->setUserId($userManager->getUID()); + $userRootFolder = $this->folderService->getUserRootFolder(); + $envelopeFolder = $userRootFolder->getFirstNodeById($envelopeFolderId); + + if (!$envelopeFolder instanceof \OCP\Files\Folder) { + throw new LibresignException('Envelope folder not found'); + } + + if ($this->isNodeInsideFolder($sourceNode, $envelopeFolder)) { + return $sourceNode; + } + + if (!$sourceNode instanceof \OCP\Files\File) { + throw new LibresignException('Invalid file type for envelope'); + } + + return $envelopeFolder->newFile($sourceNode->getName(), $sourceNode->getContent()); + } + + private function isNodeInsideFolder(Node $node, \OCP\Files\Folder $folder): bool { + return str_starts_with($node->getPath(), $folder->getPath() . '/'); + } +} diff --git a/lib/Service/EnvelopeService.php b/lib/Service/EnvelopeService.php index c9cbd1340a..c2fab5305a 100644 --- a/lib/Service/EnvelopeService.php +++ b/lib/Service/EnvelopeService.php @@ -48,14 +48,22 @@ public function validateEnvelopeConstraints(int $fileCount): void { } } - public function createEnvelope(string $name, string $userId, int $filesCount = 0): FileEntity { + public function createEnvelope( + string $name, + string $userId, + int $filesCount = 0, + ?string $path = null, + ): FileEntity { $this->folderService->setUserId($userId); - $parentFolder = $this->folderService->getFolder(); - $uuid = UUIDUtil::getUUID(); - $folderName = $name . '_' . $uuid; - $envelopeFolder = $parentFolder->newFolder($folderName); + if ($path) { + $envelopeFolder = $this->folderService->getOrCreateFolderByAbsolutePath($path); + } else { + $parentFolder = $this->folderService->getFolder(); + $folderName = $name . '_' . $uuid; + $envelopeFolder = $parentFolder->newFolder($folderName); + } $envelope = new FileEntity(); $envelope->setNodeId($envelopeFolder->getId()); @@ -114,9 +122,9 @@ public function getEnvelopeFolder(FileEntity $envelope): \OCP\Files\Folder { } $this->folderService->setUserId($userId); - $userFolder = $this->folderService->getFolder(); + $userRootFolder = $this->folderService->getUserRootFolder(); - $envelopeFolderNode = $userFolder->getFirstNodeById($envelope->getNodeId()); + $envelopeFolderNode = $userRootFolder->getFirstNodeById($envelope->getNodeId()); if (!$envelopeFolderNode instanceof \OCP\Files\Folder) { throw new LibresignException('Envelope folder not found'); } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 01a2bdf415..be8211173a 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -21,6 +21,7 @@ use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Helper\FileUploadHelper; use OCA\Libresign\ResponseDefinitions; +use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\File\CertificateChainService; use OCA\Libresign\Service\File\EnvelopeAssembler; use OCA\Libresign\Service\File\EnvelopeProgressService; diff --git a/lib/Service/FolderService.php b/lib/Service/FolderService.php index 2d97f6cd7f..bb24543309 100644 --- a/lib/Service/FolderService.php +++ b/lib/Service/FolderService.php @@ -45,6 +45,19 @@ public function getUserId(): ?string { return $this->userId; } + /** + * Get the user's root folder (full home), not the LibreSign container. + * + * @throws LibresignException + */ + public function getUserRootFolder(): Folder { + if (!$this->userId) { + throw new LibresignException('Invalid user to resolve folder'); + } + + return $this->root->getUserFolder($this->userId); + } + /** * Get folder for user and creates it if non-existent * @@ -200,4 +213,50 @@ public function getFileByPath(string $path): Node { throw new LibresignException($this->l10n->t('Invalid data to validate file'), 404); } } + + /** + * Ensure a folder exists at a given absolute user path, creating missing segments. + * If the final folder already exists, it must be empty. + * + * @throws LibresignException + */ + public function getOrCreateFolderByAbsolutePath(string $path): Folder { + if (!$this->userId) { + throw new LibresignException('Invalid user to create envelope folder'); + } + + $cleanPath = ltrim($path, '/'); + $userFolder = $this->root->getUserFolder($this->userId); + + if ($cleanPath === '') { + return $userFolder; + } + + $segments = array_filter(explode('/', $cleanPath), static fn (string $segment) => $segment !== ''); + $folder = $userFolder; + $isLastSegment = false; + + foreach ($segments as $index => $segment) { + $isLastSegment = ($index === count($segments) - 1); + + try { + $node = $folder->get($segment); + if (!$node instanceof Folder) { + throw new LibresignException('Invalid folder path'); + } + $folder = $node; + + if ($isLastSegment) { + $contents = $folder->getDirectoryListing(); + if (count($contents) > 0) { + throw new LibresignException($this->l10n->t('Folder already exists and is not empty: %s', [$path])); + } + } + } catch (NotFoundException) { + $folder = $folder->newFolder($segment); + } + } + + return $folder; + } } diff --git a/lib/Service/RequestSignatureService.php b/lib/Service/RequestSignatureService.php index 78f9e49653..86ba071b4b 100644 --- a/lib/Service/RequestSignatureService.php +++ b/lib/Service/RequestSignatureService.php @@ -21,6 +21,8 @@ use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Helper\FileUploadHelper; use OCA\Libresign\Helper\ValidateHelper; +use OCA\Libresign\Service\Envelope\EnvelopeFileRelocator; +use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IMimeTypeDetector; @@ -58,6 +60,7 @@ public function __construct( protected FileStatusService $fileStatusService, protected DocMdpConfigService $docMdpConfigService, protected EnvelopeService $envelopeService, + protected EnvelopeFileRelocator $envelopeFileRelocator, protected FileUploadHelper $uploadHelper, protected SignRequestService $signRequestService, ) { @@ -162,7 +165,8 @@ public function saveEnvelope(array $data): array { $createdNodes = []; try { - $envelope = $this->envelopeService->createEnvelope($envelopeName, $userId, $filesCount); + $envelopePath = $data['settings']['path'] ?? null; + $envelope = $this->envelopeService->createEnvelope($envelopeName, $userId, $filesCount, $envelopePath); $envelopeFolder = $this->envelopeService->getEnvelopeFolder($envelope); $envelopeSettings = array_merge($data['settings'] ?? [], [ @@ -191,20 +195,30 @@ public function saveEnvelope(array $data): array { private function processFileData(array $fileData, ?IUser $userManager, array $settings): Node { if (isset($fileData['uploadedFile'])) { - return $this->fileService->getNodeFromData([ + $sourceNode = $this->fileService->getNodeFromData([ 'userManager' => $userManager, 'name' => $fileData['name'] ?? '', 'uploadedFile' => $fileData['uploadedFile'], 'settings' => $settings, ]); + } else { + $sourceNode = $this->fileService->getNodeFromData([ + 'userManager' => $userManager, + 'name' => $fileData['name'] ?? '', + 'file' => $fileData, + 'settings' => $settings, + ]); } - return $this->fileService->getNodeFromData([ - 'userManager' => $userManager, - 'name' => $fileData['name'] ?? '', - 'file' => $fileData, - 'settings' => $settings, - ]); + if (isset($settings['envelopeFolderId'])) { + return $this->envelopeFileRelocator->ensureFileInEnvelopeFolder( + $sourceNode, + $settings['envelopeFolderId'], + $userManager, + ); + } + + return $sourceNode; } private function rollbackEnvelopeCreation(?FileEntity $envelope, array $files, array $createdNodes): void { diff --git a/src/actions/openInLibreSignAction.js b/src/actions/openInLibreSignAction.js index e9b0bea3ac..5579c1248c 100644 --- a/src/actions/openInLibreSignAction.js +++ b/src/actions/openInLibreSignAction.js @@ -3,12 +3,43 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { registerFileAction, FileAction } from '@nextcloud/files' +import { getCapabilities } from '@nextcloud/capabilities' import { loadState } from '@nextcloud/initial-state' import { translate as t } from '@nextcloud/l10n' +import { showError } from '@nextcloud/dialogs' +import { spawnDialog } from '@nextcloud/vue/functions/dialog' +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' +import EditNameDialog from '../Components/Common/EditNameDialog.vue' // eslint-disable-next-line import/no-unresolved import SvgIcon from '../../img/app-dark.svg?raw' -import logger from '../logger.js' + +/** + * Prompts user for envelope name via dialog + */ +function promptEnvelopeName() { + return new Promise((resolve) => { + const propsData = { + title: t('libresign', 'Envelope name'), + label: t('libresign', 'Enter a name for the envelope'), + placeholder: t('libresign', 'Envelope name'), + } + + spawnDialog( + { + ...EditNameDialog, + mounted() { + EditNameDialog.mounted?.call(this) + this.$on('close', (value) => { + resolve(value) + }) + }, + }, + propsData, + ) + }) +} export const action = new FileAction({ id: 'open-in-libresign', @@ -33,6 +64,43 @@ export const action = new FileAction({ } }, + /** + * Multiple files: create envelope (if > 1) or delegate to exec (if = 1) + */ + async execBatch({ nodes }) { + if (nodes.length === 1) { + await this.exec({ nodes }) + return [null] + } + + const envelopeName = await promptEnvelopeName() + + if (!envelopeName) { + return new Array(nodes.length).fill(null) + } + + return axios.post(generateOcsUrl('/apps/libresign/api/v1/file'), { + files: nodes.map(node => ({ fileId: node.fileid })), + name: envelopeName, + }).then((response) => { + const envelopeData = response.data?.ocs?.data + + window.OCA.Libresign.pendingEnvelope = envelopeData + + window.OCA.Files.Sidebar.close() + + window.OCA.Files.Sidebar.setActiveTab('libresign') + const firstNode = nodes[0] + window.OCA.Files.Sidebar.open(firstNode.path) + + return new Array(nodes.length).fill(null) + }).catch((error) => { + console.error('[LibreSign] API error:', error) + showError(error.response?.data?.ocs?.data?.message) + return new Array(nodes.length).fill(null) + }) + }, + order: -1000, }) diff --git a/tests/php/Unit/CapabilitiesTest.php b/tests/php/Unit/CapabilitiesTest.php index 21a96ca998..edb0f5722d 100644 --- a/tests/php/Unit/CapabilitiesTest.php +++ b/tests/php/Unit/CapabilitiesTest.php @@ -7,7 +7,7 @@ */ use OCA\Libresign\Capabilities; -use OCA\Libresign\Service\EnvelopeService; +use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\SignatureTextService; use OCA\Libresign\Service\SignerElementsService; use OCP\App\IAppManager; diff --git a/tests/php/Unit/Service/EnvelopeFileRelocatorTest.php b/tests/php/Unit/Service/EnvelopeFileRelocatorTest.php new file mode 100644 index 0000000000..a1937464aa --- /dev/null +++ b/tests/php/Unit/Service/EnvelopeFileRelocatorTest.php @@ -0,0 +1,115 @@ +folderService = $this->createMock(FolderService::class); + $this->relocator = new EnvelopeFileRelocator($this->folderService); + } + + public function testReturnsOriginalWhenAlreadyInside(): void { + $sourceFile = $this->createMock(\OCP\Files\File::class); + $sourceFile->method('getPath')->willReturn('/user/files/Envelope/doc.pdf'); + + $envelopeFolder = $this->createMock(Folder::class); + $envelopeFolder->method('getPath')->willReturn('/user/files/Envelope'); + + $rootFolder = $this->createMock(Folder::class); + $rootFolder->method('getFirstNodeById')->with(10)->willReturn($envelopeFolder); + + $this->folderService->expects($this->once())->method('setUserId')->with('u1'); + $this->folderService->method('getUserRootFolder')->willReturn($rootFolder); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('u1'); + + $result = $this->relocator->ensureFileInEnvelopeFolder($sourceFile, 10, $user); + self::assertSame($sourceFile, $result); + } + + public function testCopiesFileWhenOutside(): void { + $sourceFile = $this->createMock(\OCP\Files\File::class); + $sourceFile->method('getPath')->willReturn('/user/files/Other/doc.pdf'); + $sourceFile->method('getName')->willReturn('doc.pdf'); + $sourceFile->method('getContent')->willReturn('content'); + + $copiedFile = $this->createMock(\OCP\Files\File::class); + + $envelopeFolder = $this->createMock(Folder::class); + $envelopeFolder->method('getPath')->willReturn('/user/files/Envelope'); + $envelopeFolder->expects($this->once()) + ->method('newFile') + ->with('doc.pdf', 'content') + ->willReturn($copiedFile); + + $rootFolder = $this->createMock(Folder::class); + $rootFolder->method('getFirstNodeById')->with(10)->willReturn($envelopeFolder); + + $this->folderService->expects($this->once())->method('setUserId')->with('u1'); + $this->folderService->method('getUserRootFolder')->willReturn($rootFolder); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('u1'); + + $result = $this->relocator->ensureFileInEnvelopeFolder($sourceFile, 10, $user); + self::assertSame($copiedFile, $result); + } + + public function testThrowsWhenEnvelopeFolderNotFound(): void { + $sourceFile = $this->createMock(\OCP\Files\File::class); + $sourceFile->method('getPath')->willReturn('/user/files/doc.pdf'); + + $rootFolder = $this->createMock(Folder::class); + $rootFolder->method('getFirstNodeById')->with(10)->willReturn($this->createMock(Node::class)); + + $this->folderService->method('setUserId'); + $this->folderService->method('getUserRootFolder')->willReturn($rootFolder); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('u1'); + + $this->expectException(LibresignException::class); + $this->expectExceptionMessage('Envelope folder not found'); + $this->relocator->ensureFileInEnvelopeFolder($sourceFile, 10, $user); + } + + public function testThrowsWhenSourceIsNotFile(): void { + $sourceNode = $this->createMock(Node::class); + $sourceNode->method('getPath')->willReturn('/user/files/Other'); + + $envelopeFolder = $this->createMock(Folder::class); + $envelopeFolder->method('getPath')->willReturn('/user/files/Envelope'); + + $rootFolder = $this->createMock(Folder::class); + $rootFolder->method('getFirstNodeById')->with(10)->willReturn($envelopeFolder); + + $this->folderService->method('setUserId'); + $this->folderService->method('getUserRootFolder')->willReturn($rootFolder); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('u1'); + + $this->expectException(LibresignException::class); + $this->expectExceptionMessage('Invalid file type for envelope'); + $this->relocator->ensureFileInEnvelopeFolder($sourceNode, 10, $user); + } +} diff --git a/tests/php/Unit/Service/EnvelopeServiceTest.php b/tests/php/Unit/Service/EnvelopeServiceTest.php index 9c3cf89490..0998424b47 100644 --- a/tests/php/Unit/Service/EnvelopeServiceTest.php +++ b/tests/php/Unit/Service/EnvelopeServiceTest.php @@ -13,12 +13,13 @@ use OCA\Libresign\Db\FileMapper; use OCA\Libresign\Enum\NodeType; use OCA\Libresign\Exception\LibresignException; -use OCA\Libresign\Service\EnvelopeService; +use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\FolderService; use OCA\Libresign\Tests\Unit\TestCase; use OCP\Files\Folder; use OCP\IAppConfig; use OCP\IL10N; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; final class EnvelopeServiceTest extends TestCase { @@ -186,4 +187,141 @@ function ($folderName) use ($mockEnvelopeFolder, &$capturedFolderName) { $this->assertStringStartsWith('Contract_', $capturedFolderName); $this->assertStringContainsString($envelope->getUuid(), $capturedFolderName); } + + #[DataProvider('envelopeCreationProvider')] + public function testEnvelopeCreationWithCustomPathOrDefaultNaming( + string $name, + string $userId, + int $filesCount, + ?string $customPath, + bool $expectCustomPath, + int $expectedNodeId, + ): void { + $this->fileMapper->method('insert')->willReturnArgument(0); + + if ($expectCustomPath) { + $mockEnvelopeFolder = $this->createMock(Folder::class); + $mockEnvelopeFolder->method('getId')->willReturn($expectedNodeId); + + $this->folderService + ->expects($this->once()) + ->method('getOrCreateFolderByAbsolutePath') + ->with($customPath) + ->willReturn($mockEnvelopeFolder); + + $envelope = $this->service->createEnvelope($name, $userId, $filesCount, $customPath); + + $this->assertSame($expectedNodeId, $envelope->getNodeId()); + $this->assertSame($name, $envelope->getName()); + $this->assertSame($userId, $envelope->getUserId()); + $this->assertSame(['filesCount' => $filesCount], $envelope->getMetadata()); + } else { + $mockDefaultFolder = $this->createMock(Folder::class); + $mockEnvelopeFolder = $this->createMock(Folder::class); + $mockEnvelopeFolder->method('getId')->willReturn($expectedNodeId); + + $this->folderService + ->expects($this->once()) + ->method('getFolder') + ->willReturn($mockDefaultFolder); + + $capturedFolderName = ''; + $mockDefaultFolder->method('newFolder') + ->willReturnCallback(function ($folderName) use ($mockEnvelopeFolder, &$capturedFolderName) { + $capturedFolderName = $folderName; + return $mockEnvelopeFolder; + }); + + $envelope = $this->service->createEnvelope($name, $userId, $filesCount, $customPath); + + $this->assertStringStartsWith($name . '_', $capturedFolderName); + $this->assertStringContainsString($envelope->getUuid(), $capturedFolderName); + $this->assertSame($expectedNodeId, $envelope->getNodeId()); + $this->assertSame($name, $envelope->getName()); + } + + $this->assertTrue($envelope->isEnvelope()); + $this->assertSame(FileEntity::STATUS_DRAFT, $envelope->getStatus()); + } + + public static function envelopeCreationProvider(): array { + return [ + 'custom path - root level' => [ + 'name' => 'Root Envelope', + 'userId' => 'user1', + 'filesCount' => 2, + 'customPath' => '/EnvelopeAtRoot', + 'expectCustomPath' => true, + 'expectedNodeId' => 100, + ], + 'custom path - nested' => [ + 'name' => 'Legal Contract', + 'userId' => 'user2', + 'filesCount' => 5, + 'customPath' => '/Documents/Legal/Contracts/2026', + 'expectCustomPath' => true, + 'expectedNodeId' => 200, + ], + 'custom path - with spaces' => [ + 'name' => 'Important Files', + 'userId' => 'user3', + 'filesCount' => 3, + 'customPath' => '/My Documents/Important Files', + 'expectCustomPath' => true, + 'expectedNodeId' => 300, + ], + 'default path - no custom path provided' => [ + 'name' => 'Standard Envelope', + 'userId' => 'user4', + 'filesCount' => 1, + 'customPath' => null, + 'expectCustomPath' => false, + 'expectedNodeId' => 888, + ], + 'default path - single file' => [ + 'name' => 'Contract Package', + 'userId' => 'testuser', + 'filesCount' => 1, + 'customPath' => null, + 'expectCustomPath' => false, + 'expectedNodeId' => 999, + ], + ]; + } + + public function testEnvelopeCreationFailsWhenCustomPathNotEmpty(): void { + $this->expectException(LibresignException::class); + + $this->folderService + ->method('getOrCreateFolderByAbsolutePath') + ->willThrowException(new LibresignException('Folder not empty')); + + $this->service->createEnvelope('Test', 'user', 1, '/Documents/Existing'); + } + + #[DataProvider('envelopeConstraintsProvider')] + public function testValidateEnvelopeConstraints( + int $fileCount, + bool $shouldPass, + ): void { + if (!$shouldPass) { + $this->expectException(LibresignException::class); + } + + $this->service->validateEnvelopeConstraints($fileCount); + + if ($shouldPass) { + $this->assertTrue(true); + } + } + + public static function envelopeConstraintsProvider(): array { + return [ + 'valid - 1 file' => [1, true], + 'valid - 10 files' => [10, true], + 'valid - exactly max (50)' => [50, true], + 'invalid - exceeds max' => [51, false], + 'invalid - way over max' => [100, false], + ]; + } } diff --git a/tests/php/Unit/Service/FileServiceTest.php b/tests/php/Unit/Service/FileServiceTest.php index 967ba90be6..62fdf48b69 100644 --- a/tests/php/Unit/Service/FileServiceTest.php +++ b/tests/php/Unit/Service/FileServiceTest.php @@ -29,7 +29,7 @@ private function createFileService(array $overrides = []): FileService { \OCP\Files\IRootFolder::class, \Psr\Log\LoggerInterface::class, \OCP\IL10N::class, - \OCA\Libresign\Service\EnvelopeService::class, + \OCA\Libresign\Service\Envelope\EnvelopeService::class, \OCA\Libresign\Service\File\SignersLoader::class, \OCA\Libresign\Helper\FileUploadHelper::class, \OCA\Libresign\Service\File\EnvelopeAssembler::class, diff --git a/tests/php/Unit/Service/FolderServiceTest.php b/tests/php/Unit/Service/FolderServiceTest.php index af4bc8f3a6..ec4a827556 100644 --- a/tests/php/Unit/Service/FolderServiceTest.php +++ b/tests/php/Unit/Service/FolderServiceTest.php @@ -334,4 +334,100 @@ public function testGetFolderForFileCreatesNewFolderWhenNoEnvelopeId(): void { $this->assertInstanceOf(Folder::class, $result); } + + public function testGetUserRootFolderReturnsUserFolder(): void { + $mockUserFolder = $this->createMock(Folder::class); + $this->root->expects($this->once()) + ->method('getUserFolder') + ->with('171') + ->willReturn($mockUserFolder); + + $service = $this->getInstance('171'); + $result = $service->getUserRootFolder(); + + $this->assertSame($mockUserFolder, $result); + } + + #[DataProvider('providerGetOrCreateFolderByAbsolutePath')] + public function testGetOrCreateFolderByAbsolutePathCreatesNestedFolders( + string $path, + array $existingFolders, + array $expectedNewFolders, + ): void { + $mockUserFolder = $this->createMock(Folder::class); + $this->root->method('getUserFolder')->willReturn($mockUserFolder); + + $currentFolder = $mockUserFolder; + $segments = array_filter(explode('/', ltrim($path, '/'))); + + foreach ($segments as $index => $segment) { + if (in_array($segment, $existingFolders)) { + $existingFolder = $this->createMock(Folder::class); + $existingFolder->method('getDirectoryListing')->willReturn([]); + $currentFolder->method('get') + ->with($segment) + ->willReturn($existingFolder); + $currentFolder = $existingFolder; + } elseif (in_array($segment, $expectedNewFolders)) { + $currentFolder->method('get') + ->with($segment) + ->willThrowException(new \OCP\Files\NotFoundException()); + + $newFolder = $this->createMock(Folder::class); + $newFolder->method('getDirectoryListing')->willReturn([]); + $currentFolder->method('newFolder') + ->with($segment) + ->willReturn($newFolder); + $currentFolder = $newFolder; + } + } + + $service = $this->getInstance('171'); + $result = $service->getOrCreateFolderByAbsolutePath($path); + + $this->assertInstanceOf(Folder::class, $result); + } + + public static function providerGetOrCreateFolderByAbsolutePath(): array { + return [ + 'create single folder at root' => [ + '/Envelopes', + [], + ['Envelopes'], + ], + 'create nested folders' => [ + '/Documents/Legal/Contracts', + [], + ['Documents', 'Legal', 'Contracts'], + ], + 'use existing folder' => [ + '/Existing', + ['Existing'], + [], + ], + 'create inside existing folder' => [ + '/Documents/NewFolder', + ['Documents'], + ['NewFolder'], + ], + ]; + } + + public function testGetOrCreateFolderByAbsolutePathFailsWhenFolderNotEmpty(): void { + $mockUserFolder = $this->createMock(Folder::class); + $this->root->method('getUserFolder')->willReturn($mockUserFolder); + + $existingFolder = $this->createMock(Folder::class); + $existingFile = $this->createMock(\OCP\Files\File::class); + $existingFolder->method('getDirectoryListing')->willReturn([$existingFile]); + + $mockUserFolder->method('get') + ->with('NotEmpty') + ->willReturn($existingFolder); + + $service = $this->getInstance('171'); + + $this->expectException(\OCA\Libresign\Exception\LibresignException::class); + $service->getOrCreateFolderByAbsolutePath('/NotEmpty'); + } } diff --git a/tests/php/Unit/Service/RequestSignatureServiceTest.php b/tests/php/Unit/Service/RequestSignatureServiceTest.php index aade86a603..98bd13c960 100644 --- a/tests/php/Unit/Service/RequestSignatureServiceTest.php +++ b/tests/php/Unit/Service/RequestSignatureServiceTest.php @@ -15,7 +15,8 @@ use OCA\Libresign\Helper\FileUploadHelper; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\DocMdpConfigService; -use OCA\Libresign\Service\EnvelopeService; +use OCA\Libresign\Service\Envelope\EnvelopeFileRelocator; +use OCA\Libresign\Service\Envelope\EnvelopeService; use OCA\Libresign\Service\FileElementService; use OCA\Libresign\Service\FileService; use OCA\Libresign\Service\FileStatusService; @@ -63,6 +64,7 @@ final class RequestSignatureServiceTest extends \OCA\Libresign\Tests\Unit\TestCa private FileStatusService&MockObject $fileStatusService; private DocMdpConfigService&MockObject $docMdpConfigService; private EnvelopeService&MockObject $envelopeService; + private EnvelopeFileRelocator&MockObject $envelopeFileRelocator; private FileUploadHelper&MockObject $uploadHelper; private SignRequestService&MockObject $signRequestService; @@ -96,6 +98,7 @@ public function setUp(): void { $this->fileStatusService = $this->createMock(FileStatusService::class); $this->docMdpConfigService = $this->createMock(DocMdpConfigService::class); $this->envelopeService = $this->createMock(EnvelopeService::class); + $this->envelopeFileRelocator = $this->createMock(EnvelopeFileRelocator::class); $this->uploadHelper = $this->createMock(FileUploadHelper::class); $this->signRequestService = $this->createMock(SignRequestService::class); } @@ -124,6 +127,7 @@ private function getService(): RequestSignatureService { $this->fileStatusService, $this->docMdpConfigService, $this->envelopeService, + $this->envelopeFileRelocator, $this->uploadHelper, $this->signRequestService, );