test(ccsp): add bidirectional cross-validation tests against ccsp-v1 schema

This commit is contained in:
Claudecio Martins
2026-10-20 16:42:06 -03:00
parent 957f9c9abd
commit 9dcbac61ec
4 changed files with 733 additions and 1 deletions
+62 -1
View File
@@ -34,6 +34,8 @@
"USER_CANCELLED", "USER_CANCELLED",
"PROVIDER_NOT_AVAILABLE", "PROVIDER_NOT_AVAILABLE",
"COMMUNICATION_ERROR", "COMMUNICATION_ERROR",
"AGENT_NOT_RUNNING",
"TIMEOUT_ERROR",
"BAD_REQUEST", "BAD_REQUEST",
"METHOD_NOT_ALLOWED" "METHOD_NOT_ALLOWED"
], ],
@@ -149,11 +151,15 @@
"SealsResponse": { "SealsResponse": {
"type": "object", "type": "object",
"required": ["version", "status", "themes"], "required": ["version", "status", "seals"],
"properties": { "properties": {
"version": { "type": "string", "enum": ["1"] }, "version": { "type": "string", "enum": ["1"] },
"status": { "type": "string", "enum": ["success"] }, "status": { "type": "string", "enum": ["success"] },
"default_seal_id": { "type": ["string", "null"] }, "default_seal_id": { "type": ["string", "null"] },
"seals": {
"type": "array",
"items": { "$ref": "#/definitions/SealThemeItem" }
},
"themes": { "themes": {
"type": "array", "type": "array",
"items": { "$ref": "#/definitions/SealThemeItem" } "items": { "$ref": "#/definitions/SealThemeItem" }
@@ -351,6 +357,61 @@
"document_name": { "type": ["string", "null"] } "document_name": { "type": ["string", "null"] }
}, },
"additionalProperties": true "additionalProperties": true
},
"AuthenticateRequest": {
"type": "object",
"required": [
"version",
"request_id",
"application_id",
"operation",
"challenge",
"nonce",
"expires_at"
],
"properties": {
"version": { "type": "string", "enum": ["1"] },
"request_id": { "type": "string" },
"application_id": { "type": "string" },
"operation": { "type": "string", "enum": ["authenticate"] },
"algorithm": { "type": "string" },
"digest": { "type": ["string", "null"] },
"challenge": { "type": "string" },
"purpose": { "type": ["string", "null"] },
"document_type": { "type": ["string", "null"] },
"certificate_hint": { "type": ["string", "null"] },
"nonce": { "type": "string" },
"expires_at": { "type": "string" }
},
"additionalProperties": true
},
"AuthenticateResponse": {
"type": "object",
"required": [
"version",
"request_id",
"status",
"signature",
"certificate",
"algorithm"
],
"properties": {
"version": { "type": "string", "enum": ["1"] },
"request_id": { "type": "string" },
"status": { "type": "string", "enum": ["authenticated", "signed"] },
"signature": { "type": "string" },
"certificate": { "type": "string" },
"certificate_chain": {
"type": "array",
"items": { "type": "string" }
},
"algorithm": { "type": "string" },
"subject_name": { "type": ["string", "null"] },
"subject_cpf_cnpj": { "type": ["string", "null"] }
},
"additionalProperties": true
} }
} }
} }
@@ -927,6 +927,7 @@ public class CcspServer {
resp.put("status", "success"); resp.put("status", "success");
resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id()); resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id());
resp.put("seals", sealList); resp.put("seals", sealList);
resp.put("themes", sealList);
sendJsonResponse(exchange, 200, resp); sendJsonResponse(exchange, 200, resp);
} }
@@ -0,0 +1,425 @@
package br.com.cybercore.signer.core.protocol;
import br.com.cybercore.signer.core.crypto.SigningService;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.provider.CertificateProvider;
import br.com.cybercore.signer.core.provider.ProviderType;
import br.com.cybercore.signer.core.security.ApplicationRegistry;
import br.com.cybercore.signer.core.security.AuthorizedApplication;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@DisplayName("Testes de Validação Cruzada contra o JSON Schema CCSP/1 (ccsp-v1.schema.json)")
class CcspSchemaValidationTest {
private CcspServer server;
private HttpClient httpClient;
private String baseUrl;
private ObjectMapper objectMapper;
private JsonNode schemaRoot;
@BeforeEach
void setUp(@TempDir Path tempDir) throws Exception {
objectMapper = new ObjectMapper();
// Localiza e carrega docs/ccsp-v1.schema.json
Path schemaPath = Path.of("../../docs/ccsp-v1.schema.json");
if (!Files.exists(schemaPath)) {
schemaPath = Path.of("docs/ccsp-v1.schema.json");
}
assertThat(schemaPath).exists();
schemaRoot = objectMapper.readTree(Files.readString(schemaPath, StandardCharsets.UTF_8));
CertificateManager certManager = new CertificateManager();
SigningService signingService = mock(SigningService.class);
ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json"));
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
long now = System.currentTimeMillis();
Date notBefore = new Date(now - 100000);
Date notAfter = new Date(now + 365L * 24 * 3600 * 1000);
X500Name subject = new X500Name("CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR");
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
subject,
BigInteger.valueOf(System.currentTimeMillis()),
notBefore,
notAfter,
subject,
kp.getPublic()
);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate());
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));
appRegistry.register(new AuthorizedApplication(
"app-schema-test",
"Schema Validation Test App",
Set.of("http://localhost:3000", "http://127.0.0.1:3000"),
true,
Instant.now()
));
ManagedCertificate testCert = new ManagedCertificate(
"cert-schema-1",
"Certificado Schema Teste",
CertificateType.A1,
CertificateStatus.AVAILABLE,
"CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR",
"CN=Schema Test ICP-Brasil:12345678901, O=CyberCore, C=BR",
"12345678",
"a1b2c3",
"d4e5f6",
Instant.now().minus(10, ChronoUnit.DAYS),
Instant.now().plus(300, ChronoUnit.DAYS),
"SHA256withRSA",
2048,
"Schema Test ICP-Brasil",
null,
null,
"provider-schema-test",
"Certificado Schema Teste"
);
CertificateProvider dummyProvider = new CertificateProvider() {
@Override public String getProviderId() { return "provider-schema-test"; }
@Override public String getDisplayName() { return "Provedor Teste Schema"; }
@Override public ProviderType getType() { return ProviderType.A1_FILE; }
@Override public boolean isAvailable() { return true; }
@Override public List<ManagedCertificate> listCertificates() { return List.of(testCert); }
@Override public SigningResult sign(SigningContext context) {
return new SigningResult(context.requestId(), new byte[]{1, 2, 3}, "mock-cert", List.of("mock-cert"), "SHA256withRSA", Instant.now());
}
@Override public Optional<SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) {
return Optional.of(new SigningKeyMaterial(kp.getPrivate(), new X509Certificate[]{cert}, null));
}
};
certManager.registerProvider(dummyProvider);
certManager.discoverCertificates();
when(signingService.sign(any())).thenReturn(
new SigningResult("req-1", new byte[]{10, 20, 30}, "base64-cert", List.of("base64-cert"), "SHA256withRSA", Instant.now())
);
server = new CcspServer(0, certManager, signingService, appRegistry, new CcspUserPromptListener() {
@Override
public CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId) {
return CompletableFuture.completedFuture(true);
}
@Override
public CompletableFuture<Boolean> requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) {
return CompletableFuture.completedFuture(true);
}
});
server.start();
int port = server.getPort();
baseUrl = "http://127.0.0.1:" + port;
httpClient = HttpClient.newHttpClient();
}
@AfterEach
void tearDown() {
if (server != null) {
server.stop();
}
}
@Test
@DisplayName("Garante integridade e completude do catálogo de ErrorCode no schema formal")
void shouldValidateErrorCodeCatalogInSchema() {
JsonNode errorCodeEnum = schemaRoot.path("definitions").path("ErrorCode").path("enum");
assertThat(errorCodeEnum.isArray()).isTrue();
Set<String> errorCodes = new HashSet<>();
errorCodeEnum.forEach(node -> errorCodes.add(node.asText()));
assertThat(errorCodes).contains(
"CERTIFICATE_NOT_FOUND",
"CERTIFICATE_EXPIRED",
"CERTIFICATE_INVALID",
"PRIVATE_KEY_NOT_AVAILABLE",
"TOKEN_NOT_FOUND",
"TOKEN_REMOVED",
"PIN_REQUIRED",
"PIN_INCORRECT",
"PKCS11_ERROR",
"SIGNATURE_FAILED",
"APPLICATION_NOT_AUTHORIZED",
"REQUEST_EXPIRED",
"REPLAY_DETECTED",
"INVALID_ORIGIN",
"UNSUPPORTED_ALGORITHM",
"USER_CANCELLED",
"PROVIDER_NOT_AVAILABLE",
"COMMUNICATION_ERROR",
"AGENT_NOT_RUNNING",
"TIMEOUT_ERROR",
"BAD_REQUEST"
);
}
@Test
@DisplayName("Valida resposta do endpoint /ccsp/v1/status contra a definição StatusResponse")
void shouldValidateStatusEndpointResponseAgainstSchema() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/status"))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(200);
JsonNode json = objectMapper.readTree(response.body());
validateAgainstDefinition(json, "StatusResponse");
assertThat(json.path("version").asText()).isEqualTo("1");
assertThat(json.path("status").asText()).isEqualTo("ready");
}
@Test
@DisplayName("Valida resposta do endpoint /ccsp/v1/authorize contra a definição AuthorizeResponse")
void shouldValidateAuthorizeEndpointResponseAgainstSchema() throws Exception {
String payload = """
{
"version": "1",
"application_id": "app-schema-test",
"name": "Schema Validation Test App"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/authorize"))
.header("Content-Type", "application/json")
.header("Origin", "http://localhost:3000")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(200);
JsonNode json = objectMapper.readTree(response.body());
validateAgainstDefinition(json, "AuthorizeResponse");
assertThat(json.path("version").asText()).isEqualTo("1");
assertThat(json.path("status").asText()).isEqualTo("authorized");
assertThat(json.path("application_id").asText()).isEqualTo("app-schema-test");
}
@Test
@DisplayName("Valida resposta do endpoint /ccsp/v1/certificates contra CertificatesResponse e CertificateDto")
void shouldValidateCertificatesEndpointResponseAgainstSchema() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/certificates?application_id=app-schema-test"))
.header("Origin", "http://localhost:3000")
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(200);
JsonNode json = objectMapper.readTree(response.body());
validateAgainstDefinition(json, "CertificatesResponse");
JsonNode certificates = json.path("certificates");
assertThat(certificates.isArray()).isTrue();
assertThat(certificates.size()).isGreaterThanOrEqualTo(1);
for (JsonNode certNode : certificates) {
validateAgainstDefinition(certNode, "CertificateItem");
}
}
@Test
@DisplayName("Valida resposta do endpoint /ccsp/v1/sign contra a definição SignResponse")
void shouldValidateSignEndpointResponseAgainstSchema() throws Exception {
String payload = """
{
"version": "1",
"request_id": "%s",
"application_id": "app-schema-test",
"operation": "sign",
"algorithm": "SHA256withRSA",
"digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"certificate_hint": "cert-schema-1",
"document_name": "documento_teste.txt",
"nonce": "%s",
"expires_at": "%s"
}
""".formatted(
UUID.randomUUID(),
UUID.randomUUID(),
Instant.now().plus(60, ChronoUnit.SECONDS).toString()
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
.header("Content-Type", "application/json")
.header("Origin", "http://localhost:3000")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(200);
JsonNode json = objectMapper.readTree(response.body());
validateAgainstDefinition(json, "SignResponse");
assertThat(json.path("version").asText()).isEqualTo("1");
assertThat(json.path("status").asText()).isEqualTo("signed");
assertThat(json.path("signature").asText()).isNotEmpty();
assertThat(json.path("certificate").asText()).isNotEmpty();
}
@Test
@DisplayName("Valida resposta de erro contra a definição ErrorResponse e ErrorCode catalog")
void shouldValidateErrorResponseAgainstSchema() throws Exception {
String payload = """
{
"version": "1",
"request_id": "%s",
"application_id": "app-schema-test",
"operation": "sign",
"algorithm": "SHA256withRSA",
"digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"certificate_hint": "non-existent-cert-id",
"nonce": "%s",
"expires_at": "%s"
}
""".formatted(
UUID.randomUUID(),
UUID.randomUUID(),
Instant.now().plus(60, ChronoUnit.SECONDS).toString()
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
.header("Content-Type", "application/json")
.header("Origin", "http://localhost:3000")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isGreaterThanOrEqualTo(400);
JsonNode json = objectMapper.readTree(response.body());
validateAgainstDefinition(json, "ErrorResponse");
String errorCode = json.path("error_code").asText();
JsonNode errorCodeEnum = schemaRoot.path("definitions").path("ErrorCode").path("enum");
List<String> validCodes = new ArrayList<>();
errorCodeEnum.forEach(node -> validCodes.add(node.asText()));
assertThat(validCodes).contains(errorCode);
}
@Test
@DisplayName("Valida resposta do endpoint /ccsp/v1/seals contra a definição SealsResponse")
void shouldValidateSealsEndpointResponseAgainstSchema() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/seals"))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.statusCode()).isEqualTo(200);
JsonNode json = objectMapper.readTree(response.body());
validateAgainstDefinition(json, "SealsResponse");
}
private void validateAgainstDefinition(JsonNode json, String definitionName) {
JsonNode def = schemaRoot.path("definitions").path(definitionName);
assertThat(def.isMissingNode()).as("Definição %s deve existir no schema", definitionName).isFalse();
JsonNode required = def.path("required");
if (required.isArray()) {
for (JsonNode reqNode : required) {
String field = reqNode.asText();
assertThat(json.hasNonNull(field))
.as("Campo obrigatório '%s' da definição '%s' deve estar presente e não nulo", field, definitionName)
.isTrue();
}
}
JsonNode properties = def.path("properties");
if (properties.isObject()) {
properties.fieldNames().forEachRemaining(propName -> {
if (json.has(propName) && !json.get(propName).isNull()) {
JsonNode propDef = properties.get(propName);
String expectedType = propDef.path("type").asText(null);
if (expectedType != null) {
assertTypeMatches(json.get(propName), expectedType, propName, definitionName);
}
}
});
}
}
private void assertTypeMatches(JsonNode valueNode, String expectedType, String propName, String defName) {
switch (expectedType) {
case "string" -> assertThat(valueNode.isTextual())
.as("Propriedade '%s' em '%s' deve ser string", propName, defName).isTrue();
case "boolean" -> assertThat(valueNode.isBoolean())
.as("Propriedade '%s' em '%s' deve ser boolean", propName, defName).isTrue();
case "number" -> assertThat(valueNode.isNumber())
.as("Propriedade '%s' em '%s' deve ser number", propName, defName).isTrue();
case "integer" -> assertThat(valueNode.isIntegralNumber())
.as("Propriedade '%s' em '%s' deve ser integer", propName, defName).isTrue();
case "array" -> assertThat(valueNode.isArray())
.as("Propriedade '%s' em '%s' deve ser array", propName, defName).isTrue();
case "object" -> assertThat(valueNode.isObject())
.as("Propriedade '%s' em '%s' deve ser object", propName, defName).isTrue();
default -> {}
}
}
}
+245
View File
@@ -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');
});
});