Files
cybercore-signer/sdk/test/schema-validation.test.js

246 lines
8.7 KiB
JavaScript

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: '<nfeProc>...</nfeProc>',
certificate: 'CERT_BASE64',
algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
document_name: capturedPayload.document_name
};
}
return {};
};
await signer.signXmlDocument({
xml: '<nfeProc><infNFe Id="NFe3524">Dados</infNFe></nfeProc>',
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');
});
});