diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 4742dd25db..9c822093f9 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -17,6 +17,7 @@ use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Db\UserElement; use OCA\Libresign\Db\UserElementMapper; +use OCA\Libresign\Enum\CRLReason; use OCA\Libresign\Enum\FileStatus; use OCA\Libresign\Exception\InvalidPasswordException; use OCA\Libresign\Exception\LibresignException; @@ -24,6 +25,7 @@ use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Helper\FileUploadHelper; use OCA\Libresign\Helper\ValidateHelper; +use OCA\Libresign\Service\Crl\CrlService; use OCA\Settings\Mailer\NewUserMailHelper; use OCP\Accounts\IAccountManager; use OCP\AppFramework\Db\DoesNotExistException; @@ -78,6 +80,7 @@ public function __construct( private IClientService $clientService, private ITimeFactory $timeFactory, private FileUploadHelper $uploadHelper, + private CrlService $crlService, ) { } @@ -565,7 +568,16 @@ public function uploadPfx(array $file, IUser $user): void { } public function deletePfx(IUser $user): void { - $this->pkcs12Handler->deletePfx($user->getUID()); + $uid = $user->getUID(); + + $this->crlService->revokeUserCertificates( + $uid, + CRLReason::CESSATION_OF_OPERATION, + 'Certificate deleted by account owner.', + $uid, + ); + + $this->pkcs12Handler->deletePfx($uid); } /** diff --git a/lib/Service/IdentifyMethod/SignatureMethod/Password.php b/lib/Service/IdentifyMethod/SignatureMethod/Password.php index 264d515783..85bf2170bf 100644 --- a/lib/Service/IdentifyMethod/SignatureMethod/Password.php +++ b/lib/Service/IdentifyMethod/SignatureMethod/Password.php @@ -53,7 +53,7 @@ private function validateCertificateRevocation(array $certificateData): void { return; } if ($status === CrlValidationStatus::REVOKED) { - throw new LibresignException($this->identifyService->getL10n()->t('Certificate has been revoked'), 400); + throw new LibresignException($this->identifyService->getL10n()->t('Certificate has been revoked'), 422); } // Admin explicitly disabled external CRL validation – allow signing. if ($status === CrlValidationStatus::DISABLED) { @@ -63,7 +63,7 @@ private function validateCertificateRevocation(array $certificateData): void { // fail-closed – we cannot confirm the certificate is not revoked. throw new LibresignException( $this->identifyService->getL10n()->t('Certificate revocation status could not be verified'), - 400 + 422 ); } @@ -71,11 +71,11 @@ private function validateCertificateExpiration(array $certificateData): void { if (array_key_exists('validTo_time_t', $certificateData)) { $validTo = $certificateData['validTo_time_t']; if (!is_int($validTo)) { - throw new LibresignException($this->identifyService->getL10n()->t('Invalid certificate'), 400); + throw new LibresignException($this->identifyService->getL10n()->t('Invalid certificate'), 422); } $now = (new \DateTime())->getTimestamp(); if ($validTo <= $now) { - throw new LibresignException($this->identifyService->getL10n()->t('Certificate has expired'), 400); + throw new LibresignException($this->identifyService->getL10n()->t('Certificate has expired'), 422); } } } diff --git a/src/tests/store/sign.spec.ts b/src/tests/store/sign.spec.ts index b0317a2806..904b400f87 100644 --- a/src/tests/store/sign.spec.ts +++ b/src/tests/store/sign.spec.ts @@ -357,6 +357,20 @@ describe('useSignStore', () => { expect(result.type).toBe('signError') expect(result.errors).toEqual(['err']) }) + + it('returns signError for certificate validation errors and preserves API message', () => { + const store = useSignStore() + const apiErrors = [{ message: 'Certificate has been revoked', code: 422 }] + const error = { + response: { data: { ocs: { data: { errors: apiErrors } } } }, + } + + const result = store.parseSignError(error) + + expect(result.type).toBe('signError') + expect(result.action).toBeUndefined() + expect(result.errors).toEqual(apiErrors) + }) }) describe('setFileToSign', () => { diff --git a/src/tests/views/SignPDF/Sign.spec.ts b/src/tests/views/SignPDF/Sign.spec.ts index fbfc05ee12..0a1a64168f 100644 --- a/src/tests/views/SignPDF/Sign.spec.ts +++ b/src/tests/views/SignPDF/Sign.spec.ts @@ -400,6 +400,53 @@ describe('Sign.vue - signWithTokenCode', () => { }) }) + describe('Sign.vue - API error handling', () => { + it('keeps certificate validation errors in signStore and does not open certificate modal', async () => { + const SignComponent = await import('../../../views/SignPDF/_partials/Sign.vue') + const submitSignature = (SignComponent.default as any).methods.submitSignature + + const apiErrors = [{ message: 'Certificate has been revoked', code: 422 }] + const context = { + loading: false, + elements: [], + canCreateSignature: false, + signRequestUuid: 'test-sign-request-uuid', + signMethodsStore: { + certificateEngine: 'openssl', + }, + signatureElementsStore: { + signs: {}, + }, + actionHandler: { + showModal: vi.fn(), + closeModal: vi.fn(), + }, + signStore: { + document: { id: 10 }, + clearSigningErrors: vi.fn(), + setSigningErrors: vi.fn(), + submitSignature: vi.fn().mockRejectedValue({ + type: 'signError', + errors: apiErrors, + }), + }, + $emit: vi.fn(), + sidebarStore: { + hideSidebar: vi.fn(), + }, + } + + await submitSignature.call(context, { + method: 'password', + token: '123456', + }) + + expect(context.actionHandler.showModal).not.toHaveBeenCalled() + expect(context.signStore.setSigningErrors).toHaveBeenCalledWith(apiErrors) + expect(context.loading).toBe(false) + }) + }) + describe('proceedWithSigning - Full flow with WhatsApp token', () => { let proceedWithSigningLogic: ProceedWithSigningLogic diff --git a/tests/php/Unit/Handler/SigningErrorHandlerTest.php b/tests/php/Unit/Handler/SigningErrorHandlerTest.php index 82fb4d98c7..7d2fd1178c 100644 --- a/tests/php/Unit/Handler/SigningErrorHandlerTest.php +++ b/tests/php/Unit/Handler/SigningErrorHandlerTest.php @@ -46,6 +46,11 @@ public static function libresignExceptionProvider(): array { 'message' => 'Password required', 'expectedAction' => JSActions::ACTION_CREATE_SIGNATURE_PASSWORD, ], + 'code 422 revoked certificate triggers do nothing action' => [ + 'code' => 422, + 'message' => 'Certificate has been revoked', + 'expectedAction' => JSActions::ACTION_DO_NOTHING, + ], 'code 401 triggers do nothing action' => [ 'code' => 401, 'message' => 'Unauthorized', diff --git a/tests/php/Unit/Service/AccountServiceTest.php b/tests/php/Unit/Service/AccountServiceTest.php index 0b19ad17da..6f20d3cf20 100644 --- a/tests/php/Unit/Service/AccountServiceTest.php +++ b/tests/php/Unit/Service/AccountServiceTest.php @@ -17,11 +17,13 @@ use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Db\UserElement; use OCA\Libresign\Db\UserElementMapper; +use OCA\Libresign\Enum\CRLReason; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Helper\FileUploadHelper; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\AccountService; +use OCA\Libresign\Service\Crl\CrlService; use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\IdDocsService; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; @@ -81,6 +83,7 @@ final class AccountServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { private RequestSignatureService&MockObject $requestSignatureService; private Pkcs12Handler&MockObject $pkcs12Handler; private FileUploadHelper&MockObject $uploadHelper; + private CrlService&MockObject $crlService; public function setUp(): void { parent::setUp(); @@ -115,6 +118,7 @@ public function setUp(): void { $this->clientService = $this->createMock(ClientService::class); $this->timeFactory = $this->createMock(TimeFactory::class); $this->uploadHelper = $this->createMock(FileUploadHelper::class); + $this->crlService = $this->createMock(CrlService::class); } private function getService(): AccountService { @@ -146,10 +150,32 @@ private function getService(): AccountService { $this->folderService, $this->clientService, $this->timeFactory, - $this->uploadHelper + $this->uploadHelper, + $this->crlService ); } + public function testDeletePfxRevokesCertificatesWithReasonAndDeletesPfx(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('admin'); + + $this->crlService->expects($this->once()) + ->method('revokeUserCertificates') + ->with( + 'admin', + CRLReason::CESSATION_OF_OPERATION, + 'Certificate deleted by account owner.', + 'admin' + ) + ->willReturn(1); + + $this->pkcs12Handler->expects($this->once()) + ->method('deletePfx') + ->with('admin'); + + $this->getService()->deletePfx($user); + } + #[DataProvider('provideValidateCertificateDataCases')] public function testValidateCertificateDataUsingDataProvider($arguments, $expectedErrorMessage):void { if (is_callable($arguments)) { diff --git a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php index 49d6cb100d..2a3eb1bf5a 100644 --- a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php @@ -136,7 +136,12 @@ public static function providerValidateToSignWithError(): array { } #[DataProvider('providerValidateToSignWithCertificateData')] - public function testValidateToSignWithCertificateData(array $certificateData, bool $shouldThrow, string $expectedMessage = ''): void { + public function testValidateToSignWithCertificateData( + array $certificateData, + bool $shouldThrow, + string $expectedMessage = '', + ?int $expectedCode = null, + ): void { $this->pkcs12Handler = $this->getPkcs12Instance(['getPfxOfCurrentSigner', 'setCertificate', 'setPassword', 'readCertificate']); $this->pkcs12Handler->method('getPfxOfCurrentSigner')->willReturn('mock-pfx'); $this->pkcs12Handler->method('setCertificate')->willReturnSelf(); @@ -153,6 +158,9 @@ public function testValidateToSignWithCertificateData(array $certificateData, bo if ($expectedMessage) { $this->expectExceptionMessage($expectedMessage); } + if ($expectedCode !== null) { + $this->expectExceptionCode($expectedCode); + } } $password->validateToSign(); @@ -183,6 +191,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Certificate has expired', + 'expectedCode' => 422, ], 'invalid certificate - validTo_time_t is string' => [ 'certificateData' => [ @@ -190,6 +199,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Invalid certificate', + 'expectedCode' => 422, ], 'invalid certificate - validTo_time_t is null' => [ 'certificateData' => [ @@ -233,6 +243,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Certificate has been revoked', + 'expectedCode' => 422, ], 'valid certificate with crl validation' => [ 'certificateData' => [ @@ -255,6 +266,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Certificate revocation status could not be verified', + 'expectedCode' => 422, ], 'invalid certificate - crl validation empty string' => [ 'certificateData' => [