257 lines
8.4 KiB
JavaScript
257 lines
8.4 KiB
JavaScript
import { describe, it, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import http from 'node:http';
|
|
import {
|
|
CyberCoreSigner,
|
|
AgentNotRunningError,
|
|
ApplicationNotAuthorizedError,
|
|
CertificateExpiredError,
|
|
UserCancelledError,
|
|
TimeoutError
|
|
} from '../src/signer-sdk.js';
|
|
|
|
describe('@cybercore/signer-sdk - Testes de Integração com Mock HTTP Server', () => {
|
|
let server;
|
|
let serverPort;
|
|
let baseUrl;
|
|
|
|
// Servidor Mock HTTP emulando o protocolo CCSP/1
|
|
before(async () => {
|
|
server = http.createServer((req, res) => {
|
|
// CORS & Preflight
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-CyberCore-AppId');
|
|
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', () => {
|
|
const url = new URL(req.url, `http://127.0.0.1:${serverPort}`);
|
|
|
|
if (url.pathname === '/ccsp/v1/status') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
version: '1',
|
|
status: 'ready',
|
|
agent_version: '0.1.0',
|
|
os: 'Linux x86_64'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === '/ccsp/v1/delay') {
|
|
// Endpoint propositalmente lento para testar timeout
|
|
setTimeout(() => {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ status: 'slow_response' }));
|
|
}, 300);
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === '/ccsp/v1/authorize') {
|
|
const payload = JSON.parse(body || '{}');
|
|
if (payload.application_id === 'blocked-app') {
|
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
version: '1',
|
|
status: 'rejected',
|
|
error_code: 'APPLICATION_NOT_AUTHORIZED',
|
|
message: 'Aplicação bloqueada pelo usuário.'
|
|
}));
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
version: '1',
|
|
status: 'authorized',
|
|
application_id: payload.application_id
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === '/ccsp/v1/certificates') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
version: '1',
|
|
status: 'success',
|
|
certificates: [
|
|
{
|
|
id: 'cert-a1-1',
|
|
alias: 'Certificado A1 Teste',
|
|
subject_name: 'JOAO DA SILVA:12345678900',
|
|
certificate_type: 'A1',
|
|
has_private_key: true
|
|
}
|
|
]
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === '/ccsp/v1/sign') {
|
|
const payload = JSON.parse(body || '{}');
|
|
if (payload.certificate_hint === 'expired-cert') {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
version: '1',
|
|
request_id: payload.request_id,
|
|
status: 'error',
|
|
error_code: 'CERTIFICATE_EXPIRED',
|
|
message: 'Certificado expirou em 2025.'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (payload.certificate_hint === 'cancel-cert') {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
version: '1',
|
|
request_id: payload.request_id,
|
|
status: 'error',
|
|
error_code: 'USER_CANCELLED',
|
|
message: 'Assinatura cancelada no diálogo.'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
version: '1',
|
|
request_id: payload.request_id,
|
|
status: 'signed',
|
|
signature: 'MOCK_SIGNATURE_BASE64',
|
|
certificate: 'MOCK_CERT_BASE64',
|
|
algorithm: payload.algorithm || 'SHA256withRSA',
|
|
document_name: payload.document_name
|
|
}));
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error_code: 'NOT_FOUND', message: 'Endpoint não encontrado' }));
|
|
});
|
|
});
|
|
|
|
await new Promise((resolve, reject) => {
|
|
server.listen(0, '127.0.0.1', () => {
|
|
serverPort = server.address().port;
|
|
baseUrl = `http://127.0.0.1:${serverPort}`;
|
|
resolve();
|
|
});
|
|
server.on('error', reject);
|
|
});
|
|
});
|
|
|
|
after(async () => {
|
|
if (server) {
|
|
await new Promise(resolve => server.close(resolve));
|
|
}
|
|
});
|
|
|
|
it('deve confirmar que o agente está ativo com isInstalled()', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl });
|
|
const installed = await signer.isInstalled();
|
|
assert.equal(installed, true);
|
|
});
|
|
|
|
it('deve retornar dados de status corretos em getStatus()', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl });
|
|
const status = await signer.getStatus();
|
|
assert.equal(status.version, '1');
|
|
assert.equal(status.status, 'ready');
|
|
assert.equal(status.agent_version, '0.1.0');
|
|
});
|
|
|
|
it('deve realizar handshake com connect() com sucesso', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl, applicationName: 'ERP Teste' });
|
|
const connected = await signer.connect();
|
|
assert.equal(connected, true);
|
|
assert.equal(signer.connected, true);
|
|
});
|
|
|
|
it('deve lançar ApplicationNotAuthorizedError se o agente rejeitar autorização', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl, applicationId: 'blocked-app' });
|
|
await assert.rejects(
|
|
async () => signer.connect(),
|
|
ApplicationNotAuthorizedError
|
|
);
|
|
});
|
|
|
|
it('deve listar certificados disponíveis em getCertificates()', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl });
|
|
const certs = await signer.getCertificates();
|
|
assert.ok(Array.isArray(certs));
|
|
assert.equal(certs.length, 1);
|
|
assert.equal(certs[0].id, 'cert-a1-1');
|
|
});
|
|
|
|
it('deve assinar com sucesso via sign()', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl });
|
|
const result = await signer.sign({
|
|
certificateId: 'cert-a1-1',
|
|
digest: Buffer.alloc(32, 7).toString('base64'),
|
|
documentName: 'relatorio.pdf'
|
|
});
|
|
|
|
assert.equal(result.status, 'signed');
|
|
assert.equal(result.signature, 'MOCK_SIGNATURE_BASE64');
|
|
assert.equal(result.documentName, 'relatorio.pdf');
|
|
});
|
|
|
|
it('deve tratar erro CERTIFICATE_EXPIRED lançando CertificateExpiredError', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl });
|
|
await assert.rejects(
|
|
async () => signer.sign({
|
|
certificateId: 'expired-cert',
|
|
digest: Buffer.alloc(32, 1).toString('base64')
|
|
}),
|
|
CertificateExpiredError
|
|
);
|
|
});
|
|
|
|
it('deve tratar cancelamento de usuário lançando UserCancelledError', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl });
|
|
await assert.rejects(
|
|
async () => signer.sign({
|
|
certificateId: 'cancel-cert',
|
|
digest: Buffer.alloc(32, 2).toString('base64')
|
|
}),
|
|
UserCancelledError
|
|
);
|
|
});
|
|
|
|
it('deve lançar TimeoutError quando a chamada exceder o timeoutMs configurado', async () => {
|
|
const signer = new CyberCoreSigner({ baseUrl, timeoutMs: 50 });
|
|
await assert.rejects(
|
|
async () => signer._fetch('/ccsp/v1/delay'),
|
|
TimeoutError
|
|
);
|
|
});
|
|
|
|
it('deve lançar AgentNotRunningError com mensagem acionável se porta estiver inativa', async () => {
|
|
// Porta inativa aleatória
|
|
const deadPort = 64999;
|
|
const signer = new CyberCoreSigner({ host: '127.0.0.1', port: deadPort });
|
|
|
|
const installed = await signer.isInstalled(300);
|
|
assert.equal(installed, false);
|
|
|
|
await assert.rejects(
|
|
async () => signer.getStatus(300),
|
|
err => {
|
|
assert.ok(err instanceof AgentNotRunningError);
|
|
assert.equal(err.code, 'AGENT_NOT_RUNNING');
|
|
assert.equal(err.port, deadPort);
|
|
assert.ok(err.actionableGuidance.includes('bandeja do sistema'));
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
});
|