Files
cybercore-signer/sdk/test/signer-sdk.test.js
T

476 lines
16 KiB
JavaScript

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 = '<nfeProc versao="4.00"><NFe><infNFe Id="NFe3524..."></infNFe></NFe></nfeProc>';
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 = '<nfeProc><NFe>...</NFe><ds:Signature>...</ds:Signature></nfeProc>';
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 = '<nfeProc><NFe Id="NFe123">Dados</NFe></nfeProc>';
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);
});
});