test(ccsp): add bidirectional cross-validation tests against ccsp-v1 schema
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user