From 8785f14bcb1ca487e4b510b373d61402b1c24364 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 22 Mar 2026 00:03:49 -0300 Subject: [PATCH 1/6] test(config): add EnvironmentConfig unit tests Signed-off-by: Vitor Mattos --- tests/Unit/Config/EnvironmentConfigTest.php | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/Unit/Config/EnvironmentConfigTest.php diff --git a/tests/Unit/Config/EnvironmentConfigTest.php b/tests/Unit/Config/EnvironmentConfigTest.php new file mode 100644 index 0000000..a5fdc35 --- /dev/null +++ b/tests/Unit/Config/EnvironmentConfigTest.php @@ -0,0 +1,56 @@ +sandboxMode); + self::assertSame( + 'https://nfse.fazenda.gov.br/NFS-e/api/v1', + $config->baseUrl, + ); + } + + public function testSandboxModeSelectsSandboxUrl(): void + { + $config = new EnvironmentConfig(sandboxMode: true); + + self::assertTrue($config->sandboxMode); + self::assertSame( + 'https://hml.nfse.fazenda.gov.br/NFS-e/api/v1', + $config->baseUrl, + ); + } + + public function testCustomBaseUrlOverridesMode(): void + { + $custom = 'http://localhost:8080/NFS-e/api/v1'; + $config = new EnvironmentConfig(sandboxMode: false, baseUrl: $custom); + + self::assertFalse($config->sandboxMode); + self::assertSame($custom, $config->baseUrl); + } + + public function testCustomBaseUrlOverridesSandboxUrl(): void + { + $custom = 'http://mock-server/NFS-e/api/v1'; + $config = new EnvironmentConfig(sandboxMode: true, baseUrl: $custom); + + self::assertSame($custom, $config->baseUrl); + } +} From 543413a13bd6d2f8ca963a84c71033ec7f12c469 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 22 Mar 2026 00:04:15 -0300 Subject: [PATCH 2/6] feat(config): add EnvironmentConfig DTO Signed-off-by: Vitor Mattos --- src/Config/EnvironmentConfig.php | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/Config/EnvironmentConfig.php diff --git a/src/Config/EnvironmentConfig.php b/src/Config/EnvironmentConfig.php new file mode 100644 index 0000000..d25bd66 --- /dev/null +++ b/src/Config/EnvironmentConfig.php @@ -0,0 +1,34 @@ +baseUrl = $baseUrl ?? ($sandboxMode + ? self::BASE_URL_SANDBOX + : self::BASE_URL_PROD); + } +} From eeb87b73ed3020df5879f7c72fdc4cfc97d1b613 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 22 Mar 2026 00:04:42 -0300 Subject: [PATCH 3/6] test(config): add CertConfig unit tests Signed-off-by: Vitor Mattos --- tests/Unit/Config/CertConfigTest.php | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/Unit/Config/CertConfigTest.php diff --git a/tests/Unit/Config/CertConfigTest.php b/tests/Unit/Config/CertConfigTest.php new file mode 100644 index 0000000..ad09eb4 --- /dev/null +++ b/tests/Unit/Config/CertConfigTest.php @@ -0,0 +1,69 @@ +cnpj); + self::assertSame('/etc/nfse/certs/company.pfx', $config->pfxPath); + self::assertSame('secret/nfse/29842527000145', $config->vaultPath); + } + + public function testCnpjIsReadonly(): void + { + $config = new CertConfig( + cnpj: '29842527000145', + pfxPath: '/etc/nfse/certs/company.pfx', + vaultPath: 'secret/nfse/29842527000145', + ); + + $this->expectException(\Error::class); + /** @phpstan-ignore-next-line */ + $config->cnpj = 'other'; + } + + public function testPfxPathIsReadonly(): void + { + $config = new CertConfig( + cnpj: '29842527000145', + pfxPath: '/etc/nfse/certs/company.pfx', + vaultPath: 'secret/nfse/29842527000145', + ); + + $this->expectException(\Error::class); + /** @phpstan-ignore-next-line */ + $config->pfxPath = 'other'; + } + + public function testVaultPathIsReadonly(): void + { + $config = new CertConfig( + cnpj: '29842527000145', + pfxPath: '/etc/nfse/certs/company.pfx', + vaultPath: 'secret/nfse/29842527000145', + ); + + $this->expectException(\Error::class); + /** @phpstan-ignore-next-line */ + $config->vaultPath = 'other'; + } +} From 0bcf83290f79c8550201b028290b4a1998e16eed Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 22 Mar 2026 00:05:06 -0300 Subject: [PATCH 4/6] feat(config): add CertConfig DTO Signed-off-by: Vitor Mattos --- src/Config/CertConfig.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/Config/CertConfig.php diff --git a/src/Config/CertConfig.php b/src/Config/CertConfig.php new file mode 100644 index 0000000..5a0df1d --- /dev/null +++ b/src/Config/CertConfig.php @@ -0,0 +1,30 @@ + Date: Sun, 22 Mar 2026 00:07:55 -0300 Subject: [PATCH 5/6] test(client): update NfseClient tests to use EnvironmentConfig and CertConfig Signed-off-by: Vitor Mattos --- tests/Unit/Http/NfseClientTest.php | 70 ++++++++++++------------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/tests/Unit/Http/NfseClientTest.php b/tests/Unit/Http/NfseClientTest.php index ff02251..6d720c8 100644 --- a/tests/Unit/Http/NfseClientTest.php +++ b/tests/Unit/Http/NfseClientTest.php @@ -9,6 +9,8 @@ use donatj\MockWebServer\MockWebServer; use donatj\MockWebServer\Response; +use LibreCodeCoop\NfsePHP\Config\CertConfig; +use LibreCodeCoop\NfsePHP\Config\EnvironmentConfig; use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface; use LibreCodeCoop\NfsePHP\Dto\DpsData; use LibreCodeCoop\NfsePHP\Exception\CancellationException; @@ -65,13 +67,7 @@ public function testEmitReturnsReceiptDataOnSuccess(): void new Response($payload, ['Content-Type' => 'application/json'], 200) ); - $store = new NoOpSecretStore(); - $client = new NfseClient( - secretStore: $store, - sandboxMode: false, - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - signer: $this->signer, - ); + $client = $this->makeClient($this->signer); $dps = $this->makeDps(); $receipt = $client->emit($dps); @@ -94,11 +90,7 @@ public function testQueryReturnsReceiptDataOnSuccess(): void new Response($payload, ['Content-Type' => 'application/json'], 200) ); - $store = new NoOpSecretStore(); - $client = new NfseClient( - secretStore: $store, - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - ); + $client = $this->makeClient(); $receipt = $client->query('xyz-456'); @@ -112,11 +104,7 @@ public function testCancelReturnsTrueOnSuccess(): void new Response('{}', ['Content-Type' => 'application/json'], 200) ); - $store = new NoOpSecretStore(); - $client = new NfseClient( - secretStore: $store, - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - ); + $client = $this->makeClient(); self::assertTrue($client->cancel('abc-123', 'Cancelamento a pedido do tomador')); } @@ -134,11 +122,7 @@ public function testEmitThrowsIssuanceExceptionWhenGatewayRejects(): void new Response($payload, ['Content-Type' => 'application/json'], 422), ); - $client = new NfseClient( - secretStore: new NoOpSecretStore(), - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - signer: $this->signer, - ); + $client = $this->makeClient($this->signer); $this->expectException(IssuanceException::class); $client->emit($this->makeDps()); @@ -153,11 +137,7 @@ public function testIssuanceExceptionCarriesErrorCodeHttpStatusAndUpstreamPayloa new Response(json_encode($errorData, JSON_THROW_ON_ERROR), ['Content-Type' => 'application/json'], 422), ); - $client = new NfseClient( - secretStore: new NoOpSecretStore(), - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - signer: $this->signer, - ); + $client = $this->makeClient($this->signer); try { $client->emit($this->makeDps()); @@ -176,10 +156,7 @@ public function testQueryThrowsQueryExceptionWhenGatewayReturnsError(): void new Response('{"error":"not found"}', ['Content-Type' => 'application/json'], 404), ); - $client = new NfseClient( - secretStore: new NoOpSecretStore(), - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - ); + $client = $this->makeClient(); $this->expectException(QueryException::class); $client->query('missing-key'); @@ -192,10 +169,7 @@ public function testQueryExceptionCarriesErrorCodeAndHttpStatus(): void new Response('{"error":"not found"}', ['Content-Type' => 'application/json'], 404), ); - $client = new NfseClient( - secretStore: new NoOpSecretStore(), - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - ); + $client = $this->makeClient(); try { $client->query('missing-key'); @@ -213,10 +187,7 @@ public function testCancelThrowsCancellationExceptionWhenGatewayReturnsError(): new Response('{"error":"cannot cancel"}', ['Content-Type' => 'application/json'], 409), ); - $client = new NfseClient( - secretStore: new NoOpSecretStore(), - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - ); + $client = $this->makeClient(); $this->expectException(CancellationException::class); $client->cancel('blocked-key', 'a pedido do tomador'); @@ -229,10 +200,7 @@ public function testCancellationExceptionCarriesErrorCodeAndHttpStatus(): void new Response('{"error":"cannot cancel"}', ['Content-Type' => 'application/json'], 409), ); - $client = new NfseClient( - secretStore: new NoOpSecretStore(), - baseUrlOverride: self::$server->getServerRoot() . '/NFS-e/api/v1', - ); + $client = $this->makeClient(); try { $client->cancel('blocked-key', 'a pedido do tomador'); @@ -245,6 +213,22 @@ public function testCancellationExceptionCarriesErrorCodeAndHttpStatus(): void // ------------------------------------------------------------------------- + private function makeClient(?XmlSignerInterface $signer = null): NfseClient + { + return new NfseClient( + environment: new EnvironmentConfig( + baseUrl: self::$server->getServerRoot() . '/NFS-e/api/v1', + ), + cert: new CertConfig( + cnpj: '29842527000145', + pfxPath: '/dev/null', + vaultPath: 'secret/nfse/29842527000145', + ), + secretStore: new NoOpSecretStore(), + signer: $signer, + ); + } + private function makeDps(): DpsData { return new DpsData( From cc026524e2fc5deb43e620f799057ca91344a385 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 22 Mar 2026 00:08:44 -0300 Subject: [PATCH 6/6] refactor(client): accept EnvironmentConfig and CertConfig in constructor Signed-off-by: Vitor Mattos --- src/Http/NfseClient.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Http/NfseClient.php b/src/Http/NfseClient.php index 9f86e71..5b8d2c1 100644 --- a/src/Http/NfseClient.php +++ b/src/Http/NfseClient.php @@ -7,6 +7,8 @@ namespace LibreCodeCoop\NfsePHP\Http; +use LibreCodeCoop\NfsePHP\Config\CertConfig; +use LibreCodeCoop\NfsePHP\Config\EnvironmentConfig; use LibreCodeCoop\NfsePHP\Contracts\NfseClientInterface; use LibreCodeCoop\NfsePHP\Contracts\SecretStoreInterface; use LibreCodeCoop\NfsePHP\Contracts\XmlSignerInterface; @@ -25,25 +27,19 @@ * * Communicates with the SEFIN gateway to issue, query, and cancel NFS-e. * All requests carry a signed DPS XML payload. - * - * Gateway sandbox base URL: https://hml.nfse.fazenda.gov.br/NFS-e/api/v1 - * Gateway production base URL: https://nfse.fazenda.gov.br/NFS-e/api/v1 */ class NfseClient implements NfseClientInterface { - private const BASE_URL_PROD = 'https://nfse.fazenda.gov.br/NFS-e/api/v1'; - private const BASE_URL_SANDBOX = 'https://hml.nfse.fazenda.gov.br/NFS-e/api/v1'; - private readonly string $baseUrl; private readonly XmlSignerInterface $signer; public function __construct( + private readonly EnvironmentConfig $environment, + private readonly CertConfig $cert, private readonly SecretStoreInterface $secretStore, - private readonly bool $sandboxMode = false, - ?string $baseUrlOverride = null, ?XmlSignerInterface $signer = null, ) { - $this->baseUrl = $baseUrlOverride ?? ($sandboxMode ? self::BASE_URL_SANDBOX : self::BASE_URL_PROD); + $this->baseUrl = $environment->baseUrl; $this->signer = $signer ?? new DpsSigner($secretStore); }