diff --git a/docs/ccsp-v1.schema.json b/docs/ccsp-v1.schema.json index 29b09d3..e0094b9 100644 --- a/docs/ccsp-v1.schema.json +++ b/docs/ccsp-v1.schema.json @@ -34,6 +34,8 @@ "USER_CANCELLED", "PROVIDER_NOT_AVAILABLE", "COMMUNICATION_ERROR", + "AGENT_NOT_RUNNING", + "TIMEOUT_ERROR", "BAD_REQUEST", "METHOD_NOT_ALLOWED" ], @@ -149,11 +151,15 @@ "SealsResponse": { "type": "object", - "required": ["version", "status", "themes"], + "required": ["version", "status", "seals"], "properties": { "version": { "type": "string", "enum": ["1"] }, "status": { "type": "string", "enum": ["success"] }, "default_seal_id": { "type": ["string", "null"] }, + "seals": { + "type": "array", + "items": { "$ref": "#/definitions/SealThemeItem" } + }, "themes": { "type": "array", "items": { "$ref": "#/definitions/SealThemeItem" } @@ -351,6 +357,61 @@ "document_name": { "type": ["string", "null"] } }, "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 } } } diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java index 0f7c535..611857b 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java @@ -927,6 +927,7 @@ public class CcspServer { resp.put("status", "success"); resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id()); resp.put("seals", sealList); + resp.put("themes", sealList); sendJsonResponse(exchange, 200, resp); } diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspSchemaValidationTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspSchemaValidationTest.java new file mode 100644 index 0000000..42a15a9 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspSchemaValidationTest.java @@ -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 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 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 requestApplicationAuthorization(String origin, String appName, String applicationId) { + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture 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 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 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 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 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 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 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 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 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 -> {} + } + } +} diff --git a/sdk/test/schema-validation.test.js b/sdk/test/schema-validation.test.js new file mode 100644 index 0000000..76eb183 --- /dev/null +++ b/sdk/test/schema-validation.test.js @@ -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: '...', + 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'); + }); +});