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 @@ +baseUrl = $baseUrl ?? ($sandboxMode + ? self::BASE_URL_SANDBOX + : self::BASE_URL_PROD); + } +} 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); } 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'; + } +} 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); + } +} 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(