Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

# Local sandbox setup for nfse-php smoke tests
# Copy to .env.local and fill the secret values.

# Full HEAD URL for sandbox mTLS smoke test
# Example:
# NFSE_HEAD_URL="https://adn.producaorestrita.nfse.gov.br/dps/00000000000000000000000000000000000000000000"
NFSE_HEAD_URL="https://adn.producaorestrita.nfse.gov.br/dps/CHANGE_ME"

# PFX certificate used for mTLS (local file, never commit)
NFSE_MTLS_PFX_PATH=".secrets/pfx/2025-LibreCode.pfx"
NFSE_MTLS_PFX_PASSWORD="CHANGE_ME"

# Optional: set to 1 to print TLS handshake details
NFSE_CURL_VERBOSE=0

# Optional OpenBao/Vault settings (if needed by local app wiring)
VAULT_ADDR="http://openbao:8200"
VAULT_TOKEN=""
VAULT_ROLE_ID=""
VAULT_SECRET_ID=""
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
/.php-cs-fixer.cache
/.phpunit.cache
/composer.lock
/.env
/.env.local
/.secrets/
67 changes: 67 additions & 0 deletions tests/Integration/Http/SandboxMtlsHeadTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Tests\Integration\Http;

use LibreCodeCoop\NfsePHP\Tests\Support\LoadsLocalEnv;
use LibreCodeCoop\NfsePHP\Tests\TestCase;

/**
* Optional sandbox connectivity smoke test (mTLS).
* Skips if env vars are not configured.
*/
class SandboxMtlsHeadTest extends TestCase
{
use LoadsLocalEnv;

public function testSandboxHeadWithMtlsWhenEnvIsPresent(): void
{
self::loadLocalEnv();

$url = getenv('NFSE_HEAD_URL') ?: '';
$pfxPath = getenv('NFSE_MTLS_PFX_PATH') ?: '';
$pfxPassword = getenv('NFSE_MTLS_PFX_PASSWORD') ?: '';

if ($url === '' || $pfxPath === '' || $pfxPassword === '') {
self::markTestSkipped('Set NFSE_HEAD_URL, NFSE_MTLS_PFX_PATH and NFSE_MTLS_PFX_PASSWORD to run sandbox mTLS test.');
}

if (!str_starts_with($pfxPath, '/')) {
$pfxPath = dirname(__DIR__, 3) . '/' . ltrim($pfxPath, '/');
}

if (!is_file($pfxPath)) {
self::markTestSkipped('Configured PFX file does not exist for mTLS test.');
}

$cmd = sprintf(
'curl --silent --show-error --output /dev/null --write-out "%%{http_code}" --head --cert-type P12 --cert %s %s; echo "|exit:$?"',
escapeshellarg($pfxPath . ':' . $pfxPassword),
escapeshellarg($url)
);

$result = shell_exec($cmd);

self::assertNotFalse($result, 'curl execution failed');

$result = trim((string) $result);

if (!str_contains($result, '|exit:')) {
self::fail('Unexpected curl result format.');
}

[$httpCode, $exitPart] = explode('|exit:', $result, 2);
$httpCode = trim($httpCode);
$exitCode = (int) trim($exitPart);

if ($exitCode !== 0) {
self::markTestSkipped('mTLS curl failed in local runtime (likely OpenSSL/PFX compatibility).');
}

self::assertContains($httpCode, ['200', '401', '403', '404']);
}
}
70 changes: 70 additions & 0 deletions tests/Integration/Xml/DpsSignerIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Tests\Integration\Xml;

use LibreCodeCoop\NfsePHP\Exception\PfxImportException;
use LibreCodeCoop\NfsePHP\SecretStore\NoOpSecretStore;
use LibreCodeCoop\NfsePHP\Tests\Support\LoadsLocalEnv;
use LibreCodeCoop\NfsePHP\Tests\TestCase;
use LibreCodeCoop\NfsePHP\Xml\DpsSigner;

/**
* Optional integration test:
* - Uses real PFX when env vars are available
* - Skips cleanly when env vars are absent
*/
class DpsSignerIntegrationTest extends TestCase
{
use LoadsLocalEnv;

public function testSignsXmlWithConfiguredPfxWhenEnvIsPresent(): void
{
self::loadLocalEnv();

$cnpj = getenv('NFS_TEST_CNPJ') ?: '11222333000181';
$pfxPath = getenv('NFSE_MTLS_PFX_PATH') ?: '';
$pfxPassword = getenv('NFSE_MTLS_PFX_PASSWORD') ?: '';

if ($pfxPath === '' || $pfxPassword === '') {
self::markTestSkipped('Set NFSE_MTLS_PFX_PATH and NFSE_MTLS_PFX_PASSWORD to run real-PFX integration test.');
}

if (!str_starts_with($pfxPath, '/')) {
$pfxPath = dirname(__DIR__, 3) . '/' . ltrim($pfxPath, '/');
}

if (!is_file($pfxPath)) {
self::markTestSkipped('Configured PFX file does not exist for integration test.');
}

$store = new NoOpSecretStore();
$store->put('pfx/' . $cnpj, [
'pfx_path' => $pfxPath,
'password' => $pfxPassword,
]);

$signer = new DpsSigner($store);
$xml = '<DPS><infDPS Id="DPS123"><x>abc</x></infDPS></DPS>';

try {
$signed = $signer->sign($xml, $cnpj);
} catch (PfxImportException $e) {
$message = strtolower($e->getMessage());

// Local OpenSSL runtime may not support legacy PKCS#12 algorithms.
if (str_contains($message, 'digital envelope routines') || str_contains($message, 'asn1 encoding routines')) {
self::markTestSkipped('Local OpenSSL runtime cannot import this PFX format.');
}

throw $e;
}

self::assertStringContainsString('<Signature', $signed);
self::assertStringContainsString('DigestValue', $signed);
}
}
62 changes: 62 additions & 0 deletions tests/Support/LoadsLocalEnv.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Tests\Support;

trait LoadsLocalEnv
{
private static bool $envLoaded = false;

protected static function loadLocalEnv(): void
{
if (self::$envLoaded) {
return;
}

self::$envLoaded = true;

$root = dirname(__DIR__, 2);

self::loadFile($root . '/.env.local');
self::loadFile($root . '/.env');
}

private static function loadFile(string $path): void
{
if (!is_file($path)) {
return;
}

$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

if ($lines === false) {
return;
}

foreach ($lines as $line) {
$line = trim($line);

if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
continue;
}

[$key, $value] = explode('=', $line, 2);

$key = trim($key);
if ($key === '' || getenv($key) !== false) {
continue;
}

$value = trim($value);
$value = trim($value, "\"'");

putenv($key . '=' . $value);
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
}
Loading