import { describe, it, before } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { CyberCoreSigner } from '../src/signer-sdk.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); describe('@cybercore/signer-sdk - Validação Cruzada de Payloads do SDK contra JSON Schema CCSP/1', () => { let schema; before(() => { const schemaPath = path.resolve(__dirname, '../../docs/ccsp-v1.schema.json'); assert.ok(fs.existsSync(schemaPath), `Arquivo de schema não encontrado em: ${schemaPath}`); schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')); }); function validatePayloadAgainstDefinition(payload, definitionName) { const def = schema.definitions?.[definitionName]; assert.ok(def, `Definição '${definitionName}' deve existir no JSON Schema.`); // 1. Verifica campos obrigatórios declarados em 'required' if (Array.isArray(def.required)) { for (const field of def.required) { assert.ok( payload[field] !== undefined && payload[field] !== null, `Payload para '${definitionName}' não contém o campo obrigatório '${field}'` ); } } // 2. Verifica tipos das propriedades declaradas if (def.properties) { for (const [key, value] of Object.entries(payload)) { if (value === null || value === undefined) continue; const propDef = def.properties[key]; if (!propDef) continue; const expectedType = propDef.type; if (typeof expectedType === 'string') { assertTypeMatches(value, expectedType, key, definitionName); } else if (Array.isArray(expectedType)) { // Tipos múltiplos (ex: ["string", "null"]) const matchesAny = expectedType.some(t => checkType(value, t)); assert.ok(matchesAny, `Campo '${key}' em '${definitionName}' não corresponde a nenhum dos tipos permitidos: ${expectedType.join(', ')}`); } // Validação de enums if (Array.isArray(propDef.enum)) { assert.ok( propDef.enum.includes(value), `Campo '${key}' em '${definitionName}' possui valor '${value}' fora do enum permitido: [${propDef.enum.join(', ')}]` ); } } } } function checkType(value, expectedType) { switch (expectedType) { case 'string': return typeof value === 'string'; case 'boolean': return typeof value === 'boolean'; case 'number': return typeof value === 'number'; case 'integer': return Number.isInteger(value); case 'array': return Array.isArray(value); case 'object': return typeof value === 'object' && value !== null && !Array.isArray(value); case 'null': return value === null; default: return true; } } function assertTypeMatches(value, expectedType, propName, defName) { assert.ok( checkType(value, expectedType), `Campo '${propName}' em '${defName}' deve ser do tipo '${expectedType}', recebido: ${typeof value}` ); } it('deve gerar payload válido para AuthorizeRequest no connect()', async () => { const signer = new CyberCoreSigner({ applicationName: 'App Teste Schema' }); let capturedPayload = null; signer._fetch = async (endpoint, options) => { if (endpoint === '/ccsp/v1/authorize') { capturedPayload = JSON.parse(options.body); return { version: '1', status: 'authorized', application_id: capturedPayload.application_id }; } return {}; }; await signer.connect(); assert.ok(capturedPayload); validatePayloadAgainstDefinition(capturedPayload, 'AuthorizeRequest'); assert.equal(capturedPayload.version, '1'); assert.equal(capturedPayload.application_id, signer.applicationId); assert.equal(capturedPayload.name, 'App Teste Schema'); }); it('deve gerar payload válido para SignRequest no sign()', async () => { const signer = new CyberCoreSigner(); let capturedPayload = null; signer._fetch = async (endpoint, options) => { if (endpoint === '/ccsp/v1/sign') { capturedPayload = JSON.parse(options.body); return { version: '1', request_id: capturedPayload.request_id, status: 'signed', signature: 'SIG_BASE64', certificate: 'CERT_BASE64', algorithm: capturedPayload.algorithm, document_name: capturedPayload.document_name }; } return {}; }; await signer.sign({ certificateId: 'cert-1', digest: Buffer.alloc(32, 5).toString('base64'), documentName: 'documento_auditoria.pdf', documentType: 'PDF' }); assert.ok(capturedPayload); validatePayloadAgainstDefinition(capturedPayload, 'SignRequest'); assert.equal(capturedPayload.version, '1'); assert.equal(capturedPayload.operation, 'sign'); assert.equal(capturedPayload.algorithm, 'SHA256withRSA'); assert.ok(capturedPayload.request_id); assert.ok(capturedPayload.nonce); assert.ok(capturedPayload.expires_at); }); it('deve gerar payload válido para SignPdfRequest no signPdfDocument()', async () => { const signer = new CyberCoreSigner(); let capturedPayload = null; signer._fetch = async (endpoint, options) => { if (endpoint === '/ccsp/v1/sign-pdf') { capturedPayload = JSON.parse(options.body); return { version: '1', request_id: capturedPayload.request_id, status: 'signed', signed_pdf: 'MOCK_SIGNED_PDF_BASE64', certificate: 'CERT_BASE64', algorithm: 'SHA256withRSA', document_name: capturedPayload.document_name }; } return {}; }; await signer.signPdfDocument({ pdf: Buffer.from('%PDF-1.4 Mock Raw PDF Content'), certificateId: 'token-a3-smartcard', documentName: 'termo_adesao.pdf', page: 1, x: 100, y: 200, scale: 1.0, sealThemeId: 'icp-brasil' }); assert.ok(capturedPayload); validatePayloadAgainstDefinition(capturedPayload, 'SignPdfRequest'); assert.equal(capturedPayload.version, '1'); assert.equal(capturedPayload.operation, 'sign_pdf'); assert.equal(capturedPayload.page, 1); assert.equal(capturedPayload.seal_theme_id, 'icp-brasil'); }); it('deve gerar payload válido para SignXmlRequest no signXmlDocument()', async () => { const signer = new CyberCoreSigner(); let capturedPayload = null; signer._fetch = async (endpoint, options) => { if (endpoint === '/ccsp/v1/sign-xml') { capturedPayload = JSON.parse(options.body); return { version: '1', request_id: capturedPayload.request_id, status: 'signed', signed_xml: '...', certificate: 'CERT_BASE64', algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', document_name: capturedPayload.document_name }; } return {}; }; await signer.signXmlDocument({ xml: 'Dados', certificateId: 'cert-a1-pj', documentName: 'nota_fiscal.xml', profile: 'XADES_BES', targetElementTag: 'infNFe', targetElementId: 'NFe3524' }); assert.ok(capturedPayload); validatePayloadAgainstDefinition(capturedPayload, 'SignXmlRequest'); assert.equal(capturedPayload.version, '1'); assert.equal(capturedPayload.operation, 'sign_xml'); assert.equal(capturedPayload.profile, 'XADES_BES'); assert.equal(capturedPayload.target_element_tag, 'infNFe'); }); it('deve gerar payload válido para AuthenticateRequest no authenticate()', async () => { const signer = new CyberCoreSigner(); let capturedPayload = null; signer._fetch = async (endpoint, options) => { if (endpoint === '/ccsp/v1/authenticate') { capturedPayload = JSON.parse(options.body); return { version: '1', request_id: capturedPayload.request_id, status: 'authenticated', signature: 'SIG_BASE64', certificate: Buffer.from('CN=DOUTOR MARCOS:12345678900').toString('base64'), algorithm: 'SHA256withRSA', subject_name: 'DOUTOR MARCOS', subject_cpf_cnpj: '12345678900' }; } return {}; }; await signer.authenticate({ certificateId: 'cert-oab', challenge: 'random-server-challenge-string' }); assert.ok(capturedPayload); validatePayloadAgainstDefinition(capturedPayload, 'AuthenticateRequest'); assert.equal(capturedPayload.version, '1'); assert.equal(capturedPayload.operation, 'authenticate'); assert.equal(capturedPayload.challenge, 'random-server-challenge-string'); }); });