From c644aafeea7b69dd3ba11a68f599917e74d5f951 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:28:14 +0000 Subject: [PATCH 1/3] fix: prevent race condition in CA ID generation during tests IAppConfig uses per-process cache, causing race conditions when CLI commands and HTTP requests execute simultaneously during Behat tests. The CLI generates a ca_id, but HTTP requests see an empty cache and generate a different ca_id, overwriting the CLI value. Solution: - Detect debug mode (automatically set during Behat tests) - Apply file lock + cache clear workaround only in debug mode - Production code remains unchanged for optimal performance This fix resolves test failures in features/sign/request.feature:28 and features/sign/request.feature:61 where setup validation was failing due to mismatched certificate paths. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../CertificateEngine/AEngineHandler.php | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/Handler/CertificateEngine/AEngineHandler.php b/lib/Handler/CertificateEngine/AEngineHandler.php index 5854a11d01..994eccaca5 100644 --- a/lib/Handler/CertificateEngine/AEngineHandler.php +++ b/lib/Handler/CertificateEngine/AEngineHandler.php @@ -143,11 +143,49 @@ public function readCertificate(string $certificate, string $privateKey): array public function getCaId(): string { $caId = $this->caIdentifierService->getCaId(); if (empty($caId)) { - $caId = $this->caIdentifierService->generateCaId($this->getName()); + // In debug mode (tests), use file lock and cache clear to prevent race conditions + // between CLI commands and HTTP requests executing simultaneously. + // In production, this is not needed as setup commands run before HTTP requests. + if ($this->config->getSystemValueBool('debug', false)) { + $caId = $this->getCaIdWithLock(); + } else { + $caId = $this->caIdentifierService->generateCaId($this->getName()); + } } return $caId; } + private function getCaIdWithLock(): string { + $lockFilePath = $this->tempManager->getTempBaseDir() . '/libresign_ca_id.lock'; + $lockFile = fopen($lockFilePath, 'c+'); + if ($lockFile === false) { + throw new \RuntimeException('Failed to open lock file'); + } + + try { + // Acquire exclusive lock (blocks until available) + if (!flock($lockFile, LOCK_EX)) { + throw new \RuntimeException('Failed to acquire lock'); + } + + // Clear IAppConfig cache and reload from database + // This is critical because IAppConfig cache is per-process + $this->appConfig->clearCache(true); + + // Check again after cache refresh - another process might have created it + $caId = $this->caIdentifierService->getCaId(); + if (empty($caId)) { + $caId = $this->caIdentifierService->generateCaId($this->getName()); + } + + return $caId; + } finally { + // Release lock and close file + flock($lockFile, LOCK_UN); + fclose($lockFile); + } + } + #[\Override] public function parseCertificate(string $certificate): array { return $this->parseX509($certificate); From 3e22061a1d0bab31d897d6e9ca0f4ead5d1a0330 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:41:22 -0300 Subject: [PATCH 2/3] fix: sync IAppConfig cache when CA ID is empty When getCaId() finds an empty CA ID in cache, it now calls clearCache(true) to reload from database before generating a new one. This prevents race conditions in test environments where CLI commands and HTTP requests may execute simultaneously with independent caches. The solution is simpler than using file locks and works transparently in both test and production environments with minimal overhead. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../CertificateEngine/AEngineHandler.php | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/lib/Handler/CertificateEngine/AEngineHandler.php b/lib/Handler/CertificateEngine/AEngineHandler.php index 994eccaca5..ac172b42f3 100644 --- a/lib/Handler/CertificateEngine/AEngineHandler.php +++ b/lib/Handler/CertificateEngine/AEngineHandler.php @@ -143,47 +143,10 @@ public function readCertificate(string $certificate, string $privateKey): array public function getCaId(): string { $caId = $this->caIdentifierService->getCaId(); if (empty($caId)) { - // In debug mode (tests), use file lock and cache clear to prevent race conditions - // between CLI commands and HTTP requests executing simultaneously. - // In production, this is not needed as setup commands run before HTTP requests. - if ($this->config->getSystemValueBool('debug', false)) { - $caId = $this->getCaIdWithLock(); - } else { - $caId = $this->caIdentifierService->generateCaId($this->getName()); - } - } - return $caId; - } - - private function getCaIdWithLock(): string { - $lockFilePath = $this->tempManager->getTempBaseDir() . '/libresign_ca_id.lock'; - $lockFile = fopen($lockFilePath, 'c+'); - if ($lockFile === false) { - throw new \RuntimeException('Failed to open lock file'); - } - - try { - // Acquire exclusive lock (blocks until available) - if (!flock($lockFile, LOCK_EX)) { - throw new \RuntimeException('Failed to acquire lock'); - } - - // Clear IAppConfig cache and reload from database - // This is critical because IAppConfig cache is per-process $this->appConfig->clearCache(true); - - // Check again after cache refresh - another process might have created it - $caId = $this->caIdentifierService->getCaId(); - if (empty($caId)) { - $caId = $this->caIdentifierService->generateCaId($this->getName()); - } - - return $caId; - } finally { - // Release lock and close file - flock($lockFile, LOCK_UN); - fclose($lockFile); + $caId = $this->caIdentifierService->getCaId() ?: $this->caIdentifierService->generateCaId($this->getName()); } + return $caId; } #[\Override] From 4eb4ab931481d733de02d84bcde7e867f173ad96 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:21:17 -0300 Subject: [PATCH 3/3] test: replace CLI certificate setup with HTTP API calls Replace 'libresign:configure:openssl --cn test' CLI commands with HTTP POST requests to '/apps/libresign/api/v1/admin/certificate/openssl' in integration tests to prevent race conditions between CLI and HTTP processes during test execution. This ensures all certificate setup happens via HTTP requests, avoiding IAppConfig cache synchronization issues that can occur when mixing CLI commands with HTTP requests in the same test scenario. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../integration/features/sign/request.feature | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/tests/integration/features/sign/request.feature b/tests/integration/features/sign/request.feature index e16e32d2a0..da61ada482 100644 --- a/tests/integration/features/sign/request.feature +++ b/tests/integration/features/sign/request.feature @@ -1,8 +1,10 @@ Feature: request-signature Scenario: Get error when try to request to sign isn't manager Given user "signer1" exists + And as user "admin" + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And as user "signer1" - And run the command "libresign:configure:openssl --cn test" with result code 0 When sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"base64":""} | | users | [{"identify":{"account":"signer1"}}] | @@ -15,7 +17,8 @@ Feature: request-signature Scenario: Get error when try to request to sign without file name Given as user "admin" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | When sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"invalid":""} | | users | [{"identify":{"account":"signer1"}}] | @@ -34,7 +37,8 @@ Feature: request-signature And my inbox is empty And reset notifications of user "signer1" And reset notifications of user "signer2" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"url":"/apps/libresign/develop/pdf"} | | users | [{"identify":{"account":"signer1"}}] | @@ -67,7 +71,8 @@ Feature: request-signature And my inbox is empty And reset notifications of user "signer1" And reset notifications of user "signer2" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"url":"/apps/libresign/develop/pdf"} | | users | [{"identify":{"account":"signer1"}}] | @@ -94,7 +99,8 @@ Feature: request-signature Scenario: Request to sign with error when the user is not authenticated Given as user "admin" And user "signer1" exists - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And reset notifications of user "signer1" And my inbox is empty And sending "post" to ocs "/apps/libresign/api/v1/request-signature" @@ -115,7 +121,8 @@ Feature: request-signature Scenario: Request to sign with error when the authenticated user have an email different of signer Given as user "admin" And user "signer1" exists - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And reset notifications of user "signer1" And set the email of user "signer1" to "signer1@domain.test" And my inbox is empty @@ -140,7 +147,8 @@ Feature: request-signature Scenario: Request to sign with error when the link was expired Given as user "admin" And my inbox is empty - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And run the command "config:app:set libresign maximum_validity --value=1 --type=integer" with result code 0 When sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"url":"/apps/libresign/develop/pdf"} | @@ -162,7 +170,8 @@ Feature: request-signature Scenario: Request to sign with success when is necessary to renew the link Given as user "admin" And my inbox is empty - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And sending "post" to ocs "/apps/provisioning_api/api/v1/config/apps/libresign/identify_methods" | value | (string)[{"name":"email","enabled":true,"mandatory":true,"can_create_account":false}] | And sending "post" to ocs "/apps/libresign/api/v1/request-signature" @@ -228,7 +237,8 @@ Feature: request-signature Scenario: Request to sign with success using account as identifier Given as user "admin" And user "signer1" exists - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And set the email of user "signer1" to "signer1@domain.test" And reset notifications of user "signer1" And my inbox is empty @@ -266,7 +276,8 @@ Feature: request-signature Scenario: Request to sign with error using account as identifier with invalid email Given as user "admin" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | When sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"url":"/apps/libresign/develop/pdf"} | | users | [{"identify":{"account":"invaliddomain.test"}}] | @@ -278,7 +289,8 @@ Feature: request-signature Scenario: Request to sign with error using email as account identifier Given as user "admin" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | When sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"url":"/apps/libresign/develop/pdf"} | | users | [{"identify":{"account":"signer3@domain.test"}}] | @@ -290,7 +302,9 @@ Feature: request-signature Scenario: Request to sign with success using email as identifier and URL as file Given as user "admin" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And as user "admin" + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And my inbox is empty When sending "post" to ocs "/apps/libresign/api/v1/request-signature" | file | {"url":"/apps/libresign/develop/pdf"} | @@ -303,7 +317,8 @@ Feature: request-signature Scenario: Request to sign with success using account as identifier and URL as file Given as user "admin" And user "signer1" exists - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And set the email of user "signer1" to "" And reset notifications of user "signer1" And my inbox is empty @@ -321,7 +336,8 @@ Feature: request-signature Scenario: Request to sign with success using email as identifier Given as user "admin" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And set the email of user "signer1" to "signer1@domain.test" And my inbox is empty When sending "post" to ocs "/apps/libresign/api/v1/request-signature" @@ -334,7 +350,8 @@ Feature: request-signature Scenario: Request to sign using email as identifier and when is necessary to use visible elements Given as user "admin" - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And sending "post" to ocs "/apps/provisioning_api/api/v1/config/apps/libresign/identify_methods" | value | (string)[{"name":"email","enabled":true,"mandatory":true,"can_create_account":false}] | And I send a file to be signed @@ -363,7 +380,8 @@ Feature: request-signature Scenario: Request to sign with success using multiple users Given as user "admin" And user "signer1" exists - And run the command "libresign:configure:openssl --cn test" with result code 0 + And sending "post" to ocs "/apps/libresign/api/v1/admin/certificate/openssl" + | rootCert | {"commonName":"test"} | And set the email of user "signer1" to "" And my inbox is empty When sending "post" to ocs "/apps/libresign/api/v1/request-signature"