feat(sdk): add typescript types actionable errors and mock server tests
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
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;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user