=8.1). Usa as CLASSES REAIS do SDK `foxnfe` * (FoxNfe\Client, FoxNfe\Webhook) e substitui APENAS o transporte HTTP * (handler do Guzzle) por um mock: nenhuma chamada de rede, SMTP ou emissão. * * Pré-requisito: composer require foxdigital/foxnfe-php:1.3.0 * Rode: php consumer.php * Saída de sucesso: PHP_SDK_MOCK_OK ... (exit 0) */ // Autoloader do Composer (traz FoxNfe\ + Guzzle). O harness de build pode // apontar um autoloader isolado por variável de ambiente FOXNFE_AUTOLOAD. $autoload = getenv('FOXNFE_AUTOLOAD') ?: 'vendor/autoload.php'; require $autoload; use FoxNfe\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Psr7\Response; function ensure(bool $cond, string $msg): void { if (!$cond) { fwrite(STDERR, "FALHA: {$msg}\n"); exit(1); } } // /auth/me devolve estrutura aninhada, exatamente como a API entrega. $meBody = [ 'user' => ['id' => 1, 'name' => 'Integração', 'email' => 'dev@example.com', 'tenant_id' => 7], 'tenant' => ['id' => 7, 'slug' => 'minha-empresa', 'name' => 'Minha Empresa', 'status' => 'active'], 'subscription' => null, 'abilities' => [], ]; $cfgBody = ['configured' => true, 'active' => true, 'secret_preview' => '••••••••lue-1234']; $json = static fn (array $b): string => json_encode($b, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); $mock = new MockHandler([ new Response(200, ['Content-Type' => 'application/json'], $json($meBody)), new Response(200, ['Content-Type' => 'application/json'], $json($cfgBody)), ]); // baseUrl inerte (sdk.invalid) garante que qualquer chamada real falharia. $client = (new Client('minha-empresa', 'https://sdk.invalid/api/v1/'))->withToken('token-de-teste-local'); // Substitui SÓ o transporte: injeta o MockHandler no stack do Guzzle interno. $http = (new ReflectionProperty(Client::class, 'http'))->getValue($client); $http->getConfig('handler')->setHandler($mock); $me = $client->me(); // exercita Client::me() real ensure(($me['tenant']['slug'] ?? null) === 'minha-empresa', 'estrutura REAL: $me[tenant][slug]'); ensure(($me['user']['tenant_id'] ?? null) === 7, 'estrutura REAL: $me[user][tenant_id]'); $cfg = $client->webhook()->getConfig(); // exercita Webhook::getConfig() real ensure(($cfg['configured'] ?? null) === true, 'config deve estar presente'); ensure(!array_key_exists('webhook_secret', $cfg), 'a API nunca devolve o segredo completo'); // ── Verificação de assinatura HMAC sobre o corpo canônico ───────────────── // ksort recursivo + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE. function ksort_recursive(array &$arr): void { ksort($arr); foreach ($arr as &$v) { if (is_array($v)) { ksort_recursive($v); } } } function canonical_json(array $payload): string { ksort_recursive($payload); return json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); } $secret = 'whsec_exemplo_apenas_para_teste_local_1234567890'; $envelope = [ 'specversion' => '1.0', 'id' => '550e8400-e29b-41d4-a716-446655440000', 'source' => 'https://foxnfe.com.br', 'type' => 'br.com.centralfox.foxnfe.nfe.authorized', 'datacontenttype' => 'application/json', 'time' => '2026-09-05T12:00:00-03:00', 'data' => ['nfe_id' => 123, 'status' => 'authorized'], ]; $body = canonical_json($envelope); $signature = 'sha256=' . hash_hmac('sha256', $body, $secret); $verify = static function (string $rawBody, string $header, string $sharedSecret): bool { $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $sharedSecret); return hash_equals($expected, $header); // tempo constante }; $tampered = $envelope; $tampered['data'] = ['nfe_id' => 999, 'status' => 'authorized']; ensure($verify($body, $signature, $secret) === true, 'assinatura válida deveria passar'); ensure($verify(canonical_json($tampered), $signature, $secret) === false, 'corpo adulterado deveria falhar'); ensure($verify($body, $signature, 'segredo-errado') === false, 'segredo errado deveria falhar'); echo 'PHP_SDK_MOCK_OK sdk=foxnfe@1.3.0 config=' . var_export($cfg['configured'], true) . ' tenant=' . $me['tenant']['slug'] . ' hmac_valid=pass hmac_tampered=reject external_requests=0' . PHP_EOL;