feat: initial commit - cybercore signer desktop agent
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { CyberCoreSigner } from '../src/signer-sdk.js';
|
||||
import {
|
||||
CyberCoreError,
|
||||
UserCancelledError,
|
||||
CertificateNotFoundError,
|
||||
PinIncorrectError,
|
||||
TokenNotFoundError,
|
||||
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'
|
||||
});
|
||||
|
||||
assert.equal(signer.baseUrl, 'http://127.0.0.1:21325');
|
||||
assert.equal(signer.applicationId, customAppId);
|
||||
assert.equal(signer.applicationName, 'Portal Minha Empresa');
|
||||
});
|
||||
|
||||
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('PIN_INCORRECT', 'PIN errado'),
|
||||
PinIncorrectError
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => signer._throwMappedError('TOKEN_NOT_FOUND', 'Sem token'),
|
||||
TokenNotFoundError
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => signer._throwMappedError('APPLICATION_NOT_AUTHORIZED', 'Não autorizada'),
|
||||
ApplicationNotAuthorizedError
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => signer._throwMappedError('OUTRO_ERRO', 'Erro genérico'),
|
||||
CyberCoreError
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user