import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { CyberCoreSigner } from '../src/signer-sdk.js'; import { CyberCoreError, AgentNotRunningError, UserCancelledError, CertificateNotFoundError, CertificateExpiredError, CertificateInvalidError, PrivateKeyNotAvailableError, PinRequiredError, PinIncorrectError, TokenNotFoundError, RequestExpiredError, ReplayDetectedError, InvalidOriginError, UnsupportedAlgorithmError, ProviderNotAvailableError, SignatureFailedError, TimeoutError, ApplicationNotAuthorizedError } from '../src/errors.js'; describe('@cybercore/signer-sdk - Testes Unitários do SDK', () => { it('deve inicializar com parâmetros padrões e gerar UUID de aplicação', () => { const signer = new CyberCoreSigner(); assert.equal(signer.baseUrl, 'http://127.0.0.1:21325'); assert.ok(signer.applicationId); assert.equal(typeof signer.applicationId, 'string'); assert.equal(signer.applicationName, 'Aplicação Web'); }); it('deve respeitar configurações customizadas no construtor', () => { const customAppId = '12345678-1234-1234-1234-123456789012'; const signer = new CyberCoreSigner({ baseUrl: 'http://127.0.0.1:21325/', applicationId: customAppId, applicationName: 'Portal Minha Empresa', timeoutMs: 45000 }); assert.equal(signer.baseUrl, 'http://127.0.0.1:21325'); assert.equal(signer.applicationId, customAppId); assert.equal(signer.applicationName, 'Portal Minha Empresa'); assert.equal(signer.timeoutMs, 45000); }); it('deve montar baseUrl automaticamente quando host e port forem especificados', () => { const signer = new CyberCoreSigner({ host: 'localhost', port: 29999 }); assert.equal(signer.host, 'localhost'); assert.equal(signer.port, 29999); assert.equal(signer.baseUrl, 'http://localhost:29999'); }); it('deve retornar as capacidades criptográficas do agente em getCapabilities()', async () => { const signer = new CyberCoreSigner(); const caps = await signer.getCapabilities(); assert.equal(caps.protocolVersion, '1'); assert.ok(Array.isArray(caps.supportedAlgorithms)); assert.ok(caps.supportedAlgorithms.includes('SHA256withRSA')); assert.ok(caps.certificateTypes.includes('A1')); assert.ok(caps.certificateTypes.includes('A3')); }); it('deve calcular hash SHA-256 com precisão criptográfica no browser/node', async () => { const signer = new CyberCoreSigner(); const texto = 'CyberCore Signer 2026'; const hashBase64 = await signer._calculateSha256(texto); assert.ok(hashBase64); assert.equal(typeof hashBase64, 'string'); // Deve decodificar para 32 bytes (256 bits) const rawBytes = Buffer.from(hashBase64, 'base64'); assert.equal(rawBytes.length, 32); }); it('deve mapear códigos de erro estruturados CCSP para classes específicas', () => { const signer = new CyberCoreSigner(); assert.throws( () => signer._throwMappedError('USER_CANCELLED', 'Operação cancelada'), UserCancelledError ); assert.throws( () => signer._throwMappedError('CERTIFICATE_NOT_FOUND', 'Não encontrado'), CertificateNotFoundError ); assert.throws( () => signer._throwMappedError('CERTIFICATE_EXPIRED', 'Certificado expirado'), CertificateExpiredError ); assert.throws( () => signer._throwMappedError('CERTIFICATE_INVALID', 'Certificado inválido'), CertificateInvalidError ); assert.throws( () => signer._throwMappedError('PRIVATE_KEY_NOT_AVAILABLE', 'Sem chave privada'), PrivateKeyNotAvailableError ); assert.throws( () => signer._throwMappedError('PIN_REQUIRED', 'PIN obrigatório'), PinRequiredError ); assert.throws( () => signer._throwMappedError('PIN_INCORRECT', 'PIN errado'), PinIncorrectError ); assert.throws( () => signer._throwMappedError('TOKEN_NOT_FOUND', 'Sem token'), TokenNotFoundError ); assert.throws( () => signer._throwMappedError('TOKEN_REMOVED', 'Token removido'), TokenNotFoundError ); assert.throws( () => signer._throwMappedError('REQUEST_EXPIRED', 'Expirou'), RequestExpiredError ); assert.throws( () => signer._throwMappedError('REPLAY_DETECTED', 'Replay'), ReplayDetectedError ); assert.throws( () => signer._throwMappedError('INVALID_ORIGIN', 'Origem inválida'), InvalidOriginError ); assert.throws( () => signer._throwMappedError('UNSUPPORTED_ALGORITHM', 'Algoritmo inválido'), UnsupportedAlgorithmError ); assert.throws( () => signer._throwMappedError('PROVIDER_NOT_AVAILABLE', 'Provider indisponível'), ProviderNotAvailableError ); assert.throws( () => signer._throwMappedError('SIGNATURE_FAILED', 'Falha na assinatura'), SignatureFailedError ); assert.throws( () => signer._throwMappedError('TIMEOUT_ERROR', 'Timeout'), TimeoutError ); assert.throws( () => signer._throwMappedError('APPLICATION_NOT_AUTHORIZED', 'Não autorizada'), ApplicationNotAuthorizedError ); assert.throws( () => signer._throwMappedError('OUTRO_ERRO', 'Erro genérico'), CyberCoreError ); }); it('deve fornecer orientação acionável rica em AgentNotRunningError', () => { const err = new AgentNotRunningError(undefined, { host: '127.0.0.1', port: 21325 }); assert.equal(err.code, 'AGENT_NOT_RUNNING'); assert.equal(err.host, '127.0.0.1'); assert.equal(err.port, 21325); assert.ok(err.actionableGuidance.includes('bandeja do sistema')); assert.ok(err.actionableGuidance.includes('instalador oficial')); assert.ok(err.actionableGuidance.includes('21325')); }); it('deve validar parâmetros obrigatórios no método sign()', async () => { const signer = new CyberCoreSigner(); await assert.rejects( async () => signer.sign(null), { name: 'CyberCoreError', code: 'BAD_REQUEST' } ); await assert.rejects( async () => signer.sign({ certificateId: null }), { name: 'CertificateNotFoundError' } ); await assert.rejects( async () => signer.sign({ certificateId: 'cert-1' }), { name: 'CyberCoreError', code: 'BAD_REQUEST' } ); }); it('deve validar integridade básica na verificação de assinatura em verify()', async () => { const signer = new CyberCoreSigner(); // Inválido por falta de campos const invalidEmpty = await signer.verify({}); assert.equal(invalidEmpty, false); // Válido com 32 bytes de digest e assinatura Base64 const fakeDigest32 = Buffer.alloc(32, 1).toString('base64'); const fakeSig256 = Buffer.alloc(256, 2).toString('base64'); const fakeCert = Buffer.alloc(100, 3).toString('base64'); const valid = await signer.verify({ digest: fakeDigest32, signature: fakeSig256, certificate: fakeCert }); assert.equal(valid, true); }); it('deve extrair dados do titular com extractSubjectInfo()', () => { const signer = new CyberCoreSigner(); // Simula uma string binária DER contendo padrão ICP-Brasil "MARCOS DA SILVA:12345678909" const sampleCertDer = Buffer.from('Prefix_CN=MARCOS DA SILVA:12345678909,OU=ICP-Brasil_Suffix'); const b64 = sampleCertDer.toString('base64'); const info = signer.extractSubjectInfo(b64); assert.equal(info.name, 'MARCOS DA SILVA'); assert.equal(info.cpfCnpj, '12345678909'); }); it('deve exigir arquivo válido em signFile()', async () => { const signer = new CyberCoreSigner(); await assert.rejects( async () => signer.signFile(null), { name: 'CyberCoreError', code: 'BAD_REQUEST' } ); }); it('deve exigir certificado em authenticate()', async () => { const signer = new CyberCoreSigner(); await assert.rejects( async () => signer.authenticate({}), { name: 'CertificateNotFoundError' } ); }); it('deve preparar payload de PDF com document_type PDF em signPdf()', async () => { const signer = new CyberCoreSigner(); const fakePdfBytes = Buffer.from('%PDF-1.4 ... dados de teste ... %%EOF'); // Mock do método _fetch interno para validar a montagem do payload let capturedPayload = null; signer._fetch = async (endpoint, options) => { capturedPayload = JSON.parse(options.body); return { status: 'signed', request_id: capturedPayload.request_id, signature: 'MEUCIQ...base64...', certificate: 'MII...base64...', algorithm: 'SHA256withRSA', document_name: capturedPayload.document_name, document_type: capturedPayload.document_type }; }; const res = await signer.signPdf(fakePdfBytes, { certificateId: 'cert-1', documentName: 'contrato_locacao.pdf' }); assert.equal(res.status, 'signed'); assert.equal(res.documentName, 'contrato_locacao.pdf'); assert.equal(res.documentType, 'PDF'); assert.ok(capturedPayload.digest); assert.equal(capturedPayload.document_type, 'PDF'); }); it('deve preparar assinatura de XML com digest do conteúdo em signXml()', async () => { const signer = new CyberCoreSigner(); const xmlContent = ''; let capturedPayload = null; signer._fetch = async (endpoint, options) => { capturedPayload = JSON.parse(options.body); return { status: 'signed', request_id: capturedPayload.request_id, signature: 'MEUCIQ...base64...', certificate: 'MII...base64...', algorithm: 'SHA256withRSA', document_name: capturedPayload.document_name, document_type: capturedPayload.document_type }; }; const res = await signer.signXml(xmlContent, { certificateId: 'cert-1', documentName: 'nfe-3524.xml' }); assert.equal(res.status, 'signed'); assert.equal(res.documentType, 'XML'); assert.equal(capturedPayload.document_name, 'nfe-3524.xml'); assert.ok(capturedPayload.digest); }); it('deve realizar autenticação / login digital com endpoint /authenticate', async () => { const signer = new CyberCoreSigner(); let capturedPayload = null; let calledEndpoint = null; signer._fetch = async (endpoint, options) => { calledEndpoint = endpoint; capturedPayload = JSON.parse(options.body); const fakeCertB64 = Buffer.from('CN=DOUTOR SILVA:98765432100').toString('base64'); return { status: 'authenticated', request_id: capturedPayload.request_id, signature: 'SIG_AUTH_BASE64', certificate: fakeCertB64, algorithm: 'SHA256withRSA', subject_name: 'DOUTOR SILVA', subject_cpf_cnpj: '98765432100' }; }; const res = await signer.authenticate({ certificateId: 'token-oab-1', challenge: 'server-login-nonce-xyz-2026' }); assert.equal(calledEndpoint, '/ccsp/v1/authenticate'); assert.equal(res.status, 'authenticated'); assert.equal(res.challenge, 'server-login-nonce-xyz-2026'); assert.equal(res.subjectName, 'DOUTOR SILVA'); assert.equal(res.subjectCpfCnpj, '98765432100'); assert.equal(capturedPayload.purpose, 'AUTHENTICATION'); }); it('deve consultar lista de temas de selos com getSealThemes()', async () => { const signer = new CyberCoreSigner(); signer._fetch = async (endpoint, options) => { assert.equal(endpoint, '/ccsp/v1/seals'); return { version: '1', status: 'success', default_seal_id: 'icp-brasil', seals: [ { id: 'icp-brasil', name: 'ICP-Brasil Oficial' }, { id: 'gov-br', name: 'Gov.br Oficial' } ] }; }; const res = await signer.getSealThemes(); assert.equal(res.status, 'success'); assert.equal(res.default_seal_id, 'icp-brasil'); assert.equal(res.seals.length, 2); }); it('deve assinar documento PDF completo em Base64 com signPdfDocument()', async () => { const signer = new CyberCoreSigner(); let capturedPayload = null; let calledEndpoint = null; const fakeSignedPdfB64 = Buffer.from('%PDF-1.4 Fake Signed PDF Content').toString('base64'); signer._fetch = async (endpoint, options) => { calledEndpoint = endpoint; capturedPayload = JSON.parse(options.body); return { version: '1', request_id: capturedPayload.request_id, status: 'signed', signed_pdf: fakeSignedPdfB64, sha256: 'FAKESHA256HASH12345', certificate: 'FAKECERTBASE64', certificate_chain: ['FAKECERTBASE64'], algorithm: 'SHA256withRSA', subject_name: 'MARIA DA SILVA', subject_cpf_cnpj: '12345678900', document_name: capturedPayload.document_name }; }; const rawPdfBuffer = Buffer.from('%PDF-1.4 Document Original'); const res = await signer.signPdfDocument({ pdf: rawPdfBuffer, certificateId: 'cert-a3-win', documentName: 'contrato.pdf', page: 2, x: 350.5, y: 50.0, scale: 1.1, sealThemeId: 'gov-br', customExtraInfo: 'Matrícula: 1048' }); assert.equal(calledEndpoint, '/ccsp/v1/sign-pdf'); assert.equal(res.status, 'signed'); assert.equal(res.signedPdf, fakeSignedPdfB64); assert.equal(res.sha256, 'FAKESHA256HASH12345'); assert.equal(res.subjectName, 'MARIA DA SILVA'); assert.equal(res.subjectCpfCnpj, '12345678900'); assert.equal(capturedPayload.page, 2); assert.equal(capturedPayload.x, 350.5); assert.equal(capturedPayload.y, 50.0); assert.equal(capturedPayload.scale, 1.1); assert.equal(capturedPayload.seal_theme_id, 'gov-br'); assert.equal(capturedPayload.custom_extra_info, 'Matrícula: 1048'); // Valida método helper de bytes const signedBytes = res.getSignedPdfBytes(); assert.ok(signedBytes instanceof Uint8Array); assert.ok(signedBytes.length > 0); }); it('deve assinar documento XML completo com signXmlDocument()', async () => { const signer = new CyberCoreSigner(); let capturedPayload = null; let calledEndpoint = null; const fakeSignedXml = '......'; signer._fetch = async (endpoint, options) => { calledEndpoint = endpoint; capturedPayload = JSON.parse(options.body); return { version: '1', request_id: capturedPayload.request_id, status: 'signed', signed_xml: fakeSignedXml, sha256: 'XMLSHA256HASH', certificate: 'FAKECERTBASE64', certificate_chain: ['FAKECERTBASE64'], algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', subject_name: 'CYBERCORE LTDA', subject_cpf_cnpj: '12345678000199', document_name: capturedPayload.document_name }; }; const xmlInput = 'Dados'; const res = await signer.signXmlDocument({ xml: xmlInput, certificateId: 'cert-a1-pj', documentName: 'nfe-123.xml', profile: 'XADES_BES', targetElementTag: 'NFe', targetElementId: 'NFe123' }); assert.equal(calledEndpoint, '/ccsp/v1/sign-xml'); assert.equal(res.status, 'signed'); assert.equal(res.signedXml, fakeSignedXml); assert.equal(res.subjectName, 'CYBERCORE LTDA'); assert.equal(capturedPayload.profile, 'XADES_BES'); assert.equal(capturedPayload.target_element_tag, 'NFe'); assert.equal(capturedPayload.target_element_id, 'NFe123'); // Helper de bytes const xmlBytes = res.getSignedXmlBytes(); assert.ok(xmlBytes instanceof Uint8Array); }); });