diff --git a/documentacion/README.md b/documentacion/README.md
index 1028fa4..3371778 100644
--- a/documentacion/README.md
+++ b/documentacion/README.md
@@ -69,20 +69,19 @@ crypto:
- [x] Tokens para certificados
- [x] Registro de certificados
- [x] Cifrado RSA de contraseñas
-- [x] Fallback a local cuando AEAT devuelve error
-- [ ] Anulación de facturas (básico)
+- [x] Anulación de facturas (básico, sin comunicación AEAT)
- [ ] Consultas
- [ ] Subsanación
-- [ ] Conexión real con AEAT (certificado necesario en servidor con Python cryptography)
+- [ ] Conexión real con AEAT (necesita certificado FNMT registrado)
## Pruebas
```bash
-# Tests de certificados
+# Tests de certificados (en test/)
python test/run_tests.py
# Test de factura
-python test_invoice.py
+python test/test_invoice.py
```
## Notas
diff --git a/documentacion/api.md b/documentacion/api.md
index a7f057d..5f4365c 100644
--- a/documentacion/api.md
+++ b/documentacion/api.md
@@ -63,7 +63,11 @@ Registra y valida un certificado digital.
{
"success": false,
"error": "certificate_expired",
- "cert": {...}
+ "cert": {
+ "subject": "...",
+ "issuer": "...",
+ "expired": true
+ }
}
```
@@ -85,7 +89,7 @@ Registra una factura en VeriFactu. No requiere token (el certificado se seleccio
"fecha_expedicion": "17-04-2026",
"tipo_factura": "F1",
"descripcion": "Factura de prueba",
- "iva": [
+ "IVA": [
{"base": 100.00, "cuota": 21.00, "tipo": 21.0}
],
"importe_total": 121.00
diff --git a/documentacion/formato_datos.md b/documentacion/formato_datos.md
index 4e0ac36..3441e5e 100644
--- a/documentacion/formato_datos.md
+++ b/documentacion/formato_datos.md
@@ -30,7 +30,7 @@ Ejemplo: `17-04-2026`
| R2 | Rectificativa por sustitución |
| R3 | Rectificativa por descuento |
| R4 | Rectificativa por devolución |
-| R5 | Rectificativa por的其他原因 |
+| R5 | Rectificativa por otros motivos |
## Sistema Informático
diff --git a/documentacion/verifactu.md b/documentacion/verifactu.md
index b99f965..0d18416 100644
--- a/documentacion/verifactu.md
+++ b/documentacion/verifactu.md
@@ -6,7 +6,7 @@ Sistema mandatory de facturación electrónica de la AEAT (Agencia Estatal de Ad
## Obligatoriedad
-A partir de certain fecha, todas las facturasemitidas deben registrarse en VeriFactu, independientemente del formato (紙 o digital).
+A partir de cierta fecha, todas las facturas emitidas deben registrarse en VeriFactu, independientemente del formato (digital).
## Operaciones
diff --git a/test/check_password.py b/test/check_password.py
new file mode 100644
index 0000000..622d6ca
--- /dev/null
+++ b/test/check_password.py
@@ -0,0 +1,26 @@
+import sys
+from cryptography.hazmat.primitives.serialization import pkcs12
+from cryptography.hazmat.backends import default_backend
+
+# Try multiple password variants
+passwords = [
+ "Mecedora12@",
+ "MECEDORA12@",
+ "mecedora12@",
+ "53950250R",
+ "1752317947215",
+ "MECEDORA12",
+]
+
+cert_path = r"D:\Importante\53950250R_JOSEP VICENT_MESTRE__1752317947215 - copia.p12"
+
+for pw in passwords:
+ try:
+ with open(cert_path, "rb") as f:
+ pkcs12.load_key_and_certificates(f.read(), pw.encode(), default_backend())
+ print(f"OK: Password is '{pw}'")
+ sys.exit(0)
+ except Exception as e:
+ print(f"FAIL: '{pw}' - {e}")
+
+print("None of the passwords worked!")
\ No newline at end of file
diff --git a/test/convert_cert.py b/test/convert_cert.py
new file mode 100644
index 0000000..50deb32
--- /dev/null
+++ b/test/convert_cert.py
@@ -0,0 +1,34 @@
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.serialization import pkcs12
+from cryptography.hazmat.backends import default_backend
+import sys
+
+p12_path = sys.argv[1] if len(sys.argv) > 1 else "data/certs/personal.p12"
+password = sys.argv[2] if len(sys.argv) > 2 else "Mecedora12"
+key_path = sys.argv[3] if len(sys.argv) > 3 else "data/certs/cert_key.pem"
+cert_path = sys.argv[4] if len(sys.argv) > 4 else "data/certs/cert_cert.pem"
+
+with open(p12_path, "rb") as f:
+ p12_data = f.read()
+
+private_key, certificate, additional_certs = pkcs12.load_key_and_certificates(
+ p12_data,
+ password.encode(),
+ default_backend()
+)
+
+key_pem = private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption()
+)
+
+cert_pem = certificate.public_bytes(serialization.Encoding.PEM)
+
+with open(key_path, "wb") as f:
+ f.write(key_pem)
+
+with open(cert_path, "wb") as f:
+ f.write(cert_pem)
+
+print(f"OK: {key_path} {cert_path}")
\ No newline at end of file
diff --git a/test/generate_certs.py b/test/generate_certs.py
new file mode 100644
index 0000000..cfacebf
--- /dev/null
+++ b/test/generate_certs.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+"""
+Script to generate test certificates for VeriFactu API testing.
+Each certificate has a DIFFERENT password for testing purposes.
+"""
+
+import datetime
+import json
+import os
+from cryptography import x509
+from cryptography.x509.oid import NameOID
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.backends import default_backend
+
+PASSWORDS = {
+ "valid_365days": "password365",
+ "valid_60days": "password60",
+ "expired": "password_expired",
+ "expiring_soon": "password_expiring",
+ "not_yet_valid": "password_future",
+}
+
+base_dir = os.path.join(os.path.dirname(__file__), "certs")
+os.makedirs(base_dir, exist_ok=True)
+
+def generate_cert(output_path, password, days_offset, test_name):
+ private_key = rsa.generate_private_key(65537, 2048, default_backend())
+
+ subject = issuer = x509.Name([
+ x509.NameAttribute(NameOID.COMMON_NAME, test_name),
+ ])
+
+ now = datetime.datetime.utcnow()
+ not_valid_before = now + datetime.timedelta(days=days_offset[0])
+ not_valid_after = now + datetime.timedelta(days=days_offset[1])
+
+ cert = x509.CertificateBuilder().subject_name(subject).issuer_name(
+ issuer
+ ).public_key(private_key.public_key()).serial_number(
+ x509.random_serial_number()
+ ).not_valid_before(not_valid_before).not_valid_after(
+ not_valid_after
+ ).sign(private_key, hashes.SHA256(), default_backend())
+
+ from cryptography.hazmat.primitives.serialization import pkcs12
+
+ p12_data = pkcs12.serialize_key_and_certificates(
+ name=test_name.encode(),
+ key=private_key,
+ cert=cert,
+ cas=None,
+ encryption_algorithm=serialization.BestAvailableEncryption(password.encode())
+ )
+
+ with open(output_path, "wb") as f:
+ f.write(p12_data)
+
+ print(f"[OK] Generated: {os.path.basename(output_path)}")
+ print(f" Password: {password}")
+ print(f" Days valid: {days_offset[1] - days_offset[0]}")
+ return password
+
+print("=" * 60)
+print("Generating Certificates with UNIQUE passwords")
+print("=" * 60)
+print()
+
+generate_cert(os.path.join(base_dir, "valid_365days.p12"), PASSWORDS["valid_365days"], (0, 365), "Valid 365 days")
+print()
+generate_cert(os.path.join(base_dir, "valid_60days.p12"), PASSWORDS["valid_60days"], (0, 60), "Valid 60 days")
+print()
+generate_cert(os.path.join(base_dir, "expired.p12"), PASSWORDS["expired"], (-20, -5), "Expired")
+print()
+generate_cert(os.path.join(base_dir, "expiring_soon.p12"), PASSWORDS["expiring_soon"], (0, 15), "Expiring Soon")
+print()
+generate_cert(os.path.join(base_dir, "not_yet_valid.p12"), PASSWORDS["not_yet_valid"], (30, 395), "Not Yet Valid")
+print()
+
+print("=" * 60)
+print("Password Reference:")
+print("=" * 60)
+for k, v in PASSWORDS.items():
+ print(f" {k}: {v}")
+print()
+
+with open("test_passwords.json", "w") as f:
+ json.dump(PASSWORDS, f, indent=2)
+print("Saved: test_passwords.json")
\ No newline at end of file
diff --git a/test/invoice.json b/test/invoice.json
new file mode 100644
index 0000000..0b0f4df
--- /dev/null
+++ b/test/invoice.json
@@ -0,0 +1,27 @@
+{
+ "tipo": "alta",
+ "factura": {
+ "emisor_nif": "53950250R",
+ "num_serie": "FV2026/001",
+ "fecha_expedicion": "17-04-2026",
+ "tipo_factura": "F1",
+ "descripcion": "Factura de prueba",
+ "destinatario": {
+ "nombre": "Cliente Test SL",
+ "nif": "B12345678"
+ },
+ "iva": [
+ {
+ "base": 100.00,
+ "cuota": 21.00,
+ "tipo": 21.0
+ }
+ ],
+ "importe_total": 121.00
+ },
+ "sistema": {
+ "nombre": "VeriFactu API",
+ "nif_proveedor": "53950250R",
+ "version": "1.0"
+ }
+}
\ No newline at end of file
diff --git a/test/run_tests.py b/test/run_tests.py
new file mode 100644
index 0000000..7556749
--- /dev/null
+++ b/test/run_tests.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+"""
+Main test runner for VeriFactu API certificate validation.
+Each test case has its own certificate and password.
+"""
+
+import json
+import os
+import shutil
+import subprocess
+import sys
+import time
+sys.path.insert(0, os.path.dirname(__file__))
+
+from test_simulate import VeriFactuTester
+
+def clear_cert_storage():
+ """Clear certificate storage before tests."""
+ storage_path = "data/certs"
+ if os.path.exists(storage_path):
+ try:
+ shutil.rmtree(storage_path)
+ except:
+ pass
+ os.makedirs(storage_path, exist_ok=True)
+
+def main():
+ print("=" * 70)
+ print("VeriFactu API - Certificate Validation Tests")
+ print("=" * 70)
+ print()
+
+ with open("test_passwords.json", "r") as f:
+ passwords = json.load(f)
+
+ tester = VeriFactuTester()
+
+ tests = [
+ {
+ "name": "Valid 365 days",
+ "cert_file": "test/certs/valid_365days.p12",
+ "password": passwords["valid_365days"],
+ "expected_success": True,
+ "expected_error": None,
+ "expected_warning": False,
+ "description": "Certificado valido, caduca en 365 dias"
+ },
+ {
+ "name": "Valid 60 days",
+ "cert_file": "test/certs/valid_60days.p12",
+ "password": passwords["valid_60days"],
+ "expected_success": True,
+ "expected_error": None,
+ "expected_warning": False,
+ "description": "Certificado valido, caduca en 60 dias"
+ },
+ {
+ "name": "Expired",
+ "cert_file": "test/certs/expired.p12",
+ "password": passwords["expired"],
+ "expected_success": False,
+ "expected_error": "certificate_expired",
+ "expected_warning": False,
+ "description": "Certificado expirado"
+ },
+ {
+ "name": "Expiring soon",
+ "cert_file": "test/certs/expiring_soon.p12",
+ "password": passwords["expiring_soon"],
+ "expected_success": True,
+ "expected_error": None,
+ "expected_warning": True,
+ "description": "Certificado caduca en menos de 30 dias"
+ },
+ {
+ "name": "Not yet valid",
+ "cert_file": "test/certs/not_yet_valid.p12",
+ "password": passwords["not_yet_valid"],
+ "expected_success": False,
+ "expected_error": "certificate_not_yet_valid",
+ "expected_warning": False,
+ "description": "Certificado no valido aun (fecha futura)"
+ },
+ ]
+
+ passed = 0
+ failed = 0
+
+ print(f"{'#':<3} {'Test':<20} {'Expected':<10} {'Result':<10} {'Status'}")
+ print("-" * 60)
+
+ for i, test in enumerate(tests, 1):
+ clear_cert_storage()
+ time.sleep(0.5)
+
+ result = tester.test_certificate(
+ test["cert_file"],
+ test["password"],
+ test["expected_success"] and "PASS" or "FAIL",
+ test["name"]
+ )
+
+ actual_success = result.get("success", False)
+ actual_error = result.get("error", "")
+ actual_warning = len(result.get("warnings", [])) > 0
+
+ expected_str = "PASS" if test["expected_success"] else "FAIL"
+ actual_str = "PASS" if actual_success else "FAIL"
+
+ passed_test = True
+
+ if actual_success != test["expected_success"]:
+ passed_test = False
+ if test["expected_error"] and actual_error != test["expected_error"]:
+ passed_test = False
+ if test["expected_warning"] != actual_warning:
+ passed_test = False
+
+ if passed_test:
+ status = "[PASS]"
+ passed += 1
+ else:
+ status = "[FAIL]"
+ failed += 1
+
+ print(f"{i:<3} {test['name']:<20} {expected_str:<10} {actual_str:<10} {status}")
+
+ if not passed_test:
+ print(f" Error: {actual_error or 'none'}")
+ print(f" Warning: {actual_warning}")
+
+ print("-" * 60)
+ print(f"RESULTS: {passed} passed, {failed} failed")
+ print("=" * 70)
+
+ return 0 if failed == 0 else 1
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/test/simulate.py b/test/simulate.py
new file mode 100644
index 0000000..9eddcfc
--- /dev/null
+++ b/test/simulate.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+"""
+Simulation script for VeriFactu API certificate validation.
+Simulates a real user making API calls to register certificates.
+"""
+
+# ==================== CONFIGURABLE PASSWORD ====================
+# THIS MUST MATCH THE PASSWORD IN generate_certs.py
+CERT_PASSWORD = "Mecedora12@"
+
+# RUTA DEL CERTIFICADO REAL
+REAL_CERT_PATH = r"D:\Importante\53950250R_JOSEP VICENT_MESTRE__1752317947215 - copia.p12"
+# ============================================================
+
+import os
+import sys
+import base64
+import json
+import datetime
+import subprocess
+from pathlib import Path
+from urllib.request import urlopen, Request
+from urllib.error import URLError
+
+API_URL = "http://localhost:6789"
+CERTS_DIR = Path(__file__).parent / "certs"
+
+# Try to import cryptography for RSA encryption
+try:
+ from cryptography import x509
+ from cryptography.hazmat.primitives import serialization
+ from cryptography.hazmat.primitives.asymmetric import padding
+ from cryptography.hazmat.backends import default_backend
+ HAS_CRYPTO = True
+except ImportError:
+ HAS_CRYPTO = False
+
+
+def call_api(endpoint, data=None, method="GET"):
+ """Make HTTP call to API."""
+ url = f"{API_URL}{endpoint}"
+
+ try:
+ if method == "GET":
+ req = Request(url, method="GET")
+ else:
+ req = Request(url, data=json.dumps(data).encode(), method="POST")
+ req.add_header("Content-Type", "application/json")
+
+ with urlopen(req, timeout=10) as response:
+ return json.loads(response.read().decode())
+ except URLError as e:
+ return {"error": str(e)}
+ except Exception as e:
+ return {"error": f"API call failed: {e}"}
+
+
+def get_public_key():
+ """Step 1: Get public key from API."""
+ print("\n" + "=" * 60)
+ print("STEP 1: Get Public Key from API")
+ print("=" * 60)
+
+ result = call_api("/api/v1/health")
+ print(f"Health check: {result}")
+
+ if "error" in result:
+ print(f"ERROR: API not running - {result}")
+ return None
+
+ result = call_api("/api/v1/auth/public-key")
+
+ if "public_key" not in result:
+ print(f"ERROR: No public key in response")
+ return None
+
+ pub_key_b64 = result["public_key"]
+ pub_key = base64.b64decode(pub_key_b64)
+
+ print(f"Public Key received (length: {len(pub_key)} bytes)")
+ return pub_key
+
+
+def encrypt_password(public_key_pem, password):
+ """Step 2: Encrypt password with public key (RSA)."""
+ print("\n" + "=" * 60)
+ print("STEP 2: Encrypt Password")
+ print("=" * 60)
+
+ if not HAS_CRYPTO:
+ print("WARNING: cryptography not available, using base64 (NOT SECURE!)")
+ return base64.b64encode(password.encode()).decode()
+
+ try:
+ public_key = serialization.load_pem_public_key(public_key_pem, default_backend())
+
+ encrypted = public_key.encrypt(
+ password.encode(),
+ padding.PKCS1v15()
+ )
+
+ encrypted_b64 = base64.b64encode(encrypted).decode()
+ print(f"Password encrypted (RSA)")
+ return encrypted_b64
+
+ except Exception as e:
+ print(f"ERROR encrypting: {e}")
+ return None
+
+
+def register_certificate(cert_path, encrypted_password, test_name="default"):
+ """Step 3: Register certificate via API."""
+ print("\n" + "=" * 60)
+ print("STEP 3: Register Certificate")
+ print("=" * 60)
+ print(f"Certificate path: {cert_path}")
+ print(f"Password (encrypted): {encrypted_password[:40]}...")
+
+ data = {
+ "cert_name": test_name.replace(" ", "_").replace("(", "").replace(")", ""),
+ "cert_path": cert_path,
+ "password_encrypted": encrypted_password
+ }
+
+ result = call_api("/api/v1/auth/register", data, method="POST")
+
+ return result
+
+
+def test_certificate(cert_file, password, expected_result, test_name):
+ """Test a single certificate via real API calls."""
+ print(f"\n{'#' * 60}")
+ print(f"# TEST: {test_name}")
+ print(f"# Expected: {expected_result}")
+ print(f"{'#' * 60}")
+
+ # Step 1: Get public key
+ pub_key = get_public_key()
+ if not pub_key:
+ print("[X] Cannot get public key - is API running?")
+ return
+
+ # Step 2: Encrypt password
+ enc_password = encrypt_password(pub_key, password)
+ if not enc_password:
+ print("[X] Cannot encrypt password")
+ return
+
+ # Step 3: Register certificate
+ cert_path = str(CERTS_DIR / cert_file)
+ result = register_certificate(cert_path, enc_password, test_name)
+
+ print(f"\nAPI Response:")
+ print(json.dumps(result, indent=2))
+
+ # Validate result
+ success = result.get("success", False)
+ error = result.get("error", "")
+ warnings = result.get("warnings", [])
+
+ if expected_result == "PASS" and success:
+ print(f"\n[OK] TEST PASSED")
+ elif expected_result == "FAIL" and not success:
+ print(f"\n[OK] TEST PASSED (expected failure: {error})")
+ elif expected_result == "PASS with WARNING" and success and warnings:
+ print(f"\n[OK] TEST PASSED (with warnings: {warnings})")
+ else:
+ print(f"\n[FAIL] TEST FAILED (success={success}, error={error})")
+
+
+def main():
+ """Main test runner."""
+ print("=" * 60)
+ print("VeriFactu API - Real User Simulation")
+ print("=" * 60)
+ print(f"\nAPI URL: {API_URL}")
+ print(f"Certs Directory: {CERTS_DIR}")
+
+ # Check if API is running
+ print("\nChecking if API is running...")
+ result = call_api("/api/v1/health")
+ if "error" in result:
+ print("ERROR: API not running!")
+ print("Start with: ./verifactu.exe")
+ return
+
+ print(f"API is running!")
+
+ print("\n" + "=" * 60)
+ print("Running Tests via Real API Calls")
+ print("=" * 60)
+
+ test_cases = [
+ ("valid_365days.p12", CERT_PASSWORD, "PASS", "Valid certificate (365 days)"),
+ ("valid_60days.p12", CERT_PASSWORD, "PASS", "Valid certificate (60 days)"),
+ ("expired.p12", CERT_PASSWORD, "FAIL", "Expired certificate"),
+ ("expiring_soon.p12", CERT_PASSWORD, "PASS with WARNING", "Expiring soon (15 days)"),
+ ("not_yet_valid.p12", CERT_PASSWORD, "FAIL", "Not yet valid certificate"),
+ ("wrong_password.p12", CERT_PASSWORD, "FAIL", "Wrong password"),
+ (REAL_CERT_PATH, CERT_PASSWORD, "PASS", "REAL certificate with REAL password"),
+ ]
+
+ for cert_file, password, expected, test_name in test_cases:
+ test_certificate(cert_file, password, expected, test_name)
+
+ print("\n" + "=" * 60)
+ print("ALL TESTS COMPLETED")
+ print("=" * 60)
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/test/test_cert.py b/test/test_cert.py
new file mode 100644
index 0000000..c9e27d5
--- /dev/null
+++ b/test/test_cert.py
@@ -0,0 +1,68 @@
+import subprocess
+import os
+
+p12 = "data/certs/personal.p12"
+pwd = "Mecedora12"
+
+cmd = ["python", "convert_cert.py", p12, pwd]
+result = subprocess.run(cmd, capture_output=True, text=True)
+print(f"Convert: {result.returncode} {result.stdout}")
+
+key = "data/certs/cert_key.pem"
+cert = "data/certs/cert_cert.pem"
+
+if os.path.exists(key) and os.path.exists(cert):
+ print(f"Key size: {os.path.getsize(key)}")
+ print(f"Cert size: {os.path.getsize(cert)}")
+
+ cmd = ["go", "run", "main.go"]
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
+
+ import time
+ time.sleep(3)
+
+ import urllib.request
+ import json
+
+ try:
+ req = urllib.request.Request("http://localhost:6789/api/v1/health")
+ with urllib.request.urlopen(req) as resp:
+ print(f"Health: {resp.read()}")
+ except Exception as e:
+ print(f"Health error: {e}")
+
+ invoice = {
+ "tipo": "alta",
+ "factura": {
+ "emisor_nif": "53950250R",
+ "num_serie": "FV2026/FINAL",
+ "fecha_expedicion": "17-04-2026",
+ "tipo_factura": "F1",
+ "descripcion": "Final test",
+ "iva": [{"base": 100, "cuota": 21, "tipo": 21}],
+ "importe_total": 121
+ },
+ "sistema": {
+ "nombre": "Test",
+ "nif_proveedor": "53950250R",
+ "version": "1.0"
+ }
+ }
+
+ req = urllib.request.Request(
+ "http://localhost:6789/api/v1/facturas",
+ data=json.dumps(invoice).encode(),
+ method="POST"
+ )
+ req.add_header("Content-Type", "application/json")
+
+ try:
+ with urllib.request.urlopen(req) as resp:
+ print(f"Invoice: {resp.read()}")
+ except Exception as e:
+ print(f"Invoice error: {e}")
+
+ proc.terminate()
+ proc.wait()
+else:
+ print("Files not found")
\ No newline at end of file
diff --git a/test/test_direct_aeat.py b/test/test_direct_aeat.py
new file mode 100644
index 0000000..3e5bd4c
--- /dev/null
+++ b/test/test_direct_aeat.py
@@ -0,0 +1,79 @@
+import urllib.request
+import urllib.parse
+
+URL = "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
+
+soap_request = """
+
+
+
+
+
+
+ TEST
+ 53950250R
+
+
+
+
+ 1.0
+
+ 53950250R
+ FV2026/001
+ 17-04-2026
+
+ TEST EMPRESA
+ F1
+ Factura de prueba
+
+
+ 01
+ S1
+ 01
+ 100.00
+ 21.00
+
+
+ 21.00
+ 121.00
+
+ S
+
+
+ TEST
+ 53950250R
+ TEST
+ 1
+ 1.0
+ 1
+ S
+
+ 17-04-2026T12:00:00
+ SHA-256
+ TESTHASH
+
+
+
+
+"""
+
+headers = {
+ "Content-Type": "text/xml; charset=utf-8",
+ "SOAPAction": ""
+}
+
+print("Enviando a AEAT test...")
+print(f"URL: {URL}")
+
+req = urllib.request.Request(URL, data=soap_request.encode('utf-8'), headers=headers)
+
+try:
+ with urllib.request.urlopen(req, timeout=30) as response:
+ print(f"\nStatus: {response.status}")
+ content = response.read().decode('utf-8')
+ print(f"\nResponse:\n{content[:1500]}")
+except urllib.error.HTTPError as e:
+ print(f"HTTP Error: {e.code}")
+ print(f"Response: {e.read().decode('utf-8')[:1500]}")
+except urllib.error.URLError as e:
+ print(f"URL Error: {e.reason}")
\ No newline at end of file
diff --git a/test/test_direct_no_cert.py b/test/test_direct_no_cert.py
new file mode 100644
index 0000000..951aec9
--- /dev/null
+++ b/test/test_direct_no_cert.py
@@ -0,0 +1,83 @@
+import urllib.request
+import urllib.parse
+import urllib.error
+
+URL = "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
+
+soap_request = """
+
+
+
+
+
+
+ TEST EMPRESA SL
+ 53950250R
+
+
+
+
+ 1.0
+
+ 53950250R
+ FV2026/TEST001
+ 17-04-2026
+
+ TEST EMPRESA SL
+ F1
+ Factura de prueba test
+
+
+ 01
+ S1
+ 01
+ 100.00
+ 21.00
+
+
+ 21.00
+ 121.00
+
+ S
+
+
+ TEST API
+ 53950250R
+ TEST-API
+ 1
+ 1.0
+ 1
+ S
+
+ 17-04-2026T12:00:00
+ SHA-256
+ 0A1B2C3D4E5F6
+
+
+
+
+"""
+
+ctx = urllib.request.ssl.create_default_context()
+ctx.check_hostname = False
+ctx.verify_mode = urllib.request.ssl.CERT_NONE
+
+headers = {
+ "Content-Type": "text/xml; charset=utf-8",
+ "SOAPAction": ""
+}
+
+print("Testing AEAT without certificate (just headers)...")
+
+req = urllib.request.Request(URL, data=soap_request.encode('utf-8'), headers=headers)
+
+try:
+ opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ctx))
+ response = opener.open(req, timeout=30)
+ print(f"Status: {response.status}")
+ print(f"Response: {response.read().decode('utf-8')[:1500]}")
+except urllib.error.HTTPError as e:
+ print(f"HTTP Error: {e.code}")
+ print(f"Body: {e.read().decode('utf-8')[:1500]}")
+except Exception as e:
+ print(f"Error: {type(e).__name__}: {e}")
\ No newline at end of file
diff --git a/test_directo_aeat.py b/test/test_directo_aeat.py
similarity index 100%
rename from test_directo_aeat.py
rename to test/test_directo_aeat.py
diff --git a/test/test_invoice.py b/test/test_invoice.py
new file mode 100644
index 0000000..5644f49
--- /dev/null
+++ b/test/test_invoice.py
@@ -0,0 +1,51 @@
+import base64
+import json
+from urllib.request import urlopen, Request
+from urllib.error import URLError
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.backends import default_backend
+
+API_URL = "http://localhost:6789"
+
+print("=" * 60)
+print("Test: Enviar Factura")
+print("=" * 60)
+
+invoice = {
+ "tipo": "alta",
+ "factura": {
+ "emisor_nif": "53950250R",
+ "num_serie": "FV2026/001",
+ "fecha_expedicion": "17-04-2026",
+ "tipo_factura": "F1",
+ "descripcion": "Factura de prueba",
+ "iva": [
+ {"base": 100.00, "cuota": 21.00, "tipo": 21.0}
+ ],
+ "importe_total": 121.00
+ },
+ "sistema": {
+ "nombre": "VeriFactu API",
+ "nif_proveedor": "53950250R",
+ "version": "1.0"
+ }
+}
+
+print("\nEnviando factura...")
+
+req = Request(
+ f"{API_URL}/api/v1/facturas",
+ data=json.dumps(invoice).encode(),
+ method="POST"
+)
+req.add_header("Content-Type", "application/json")
+
+try:
+ with urlopen(req, timeout=30) as response:
+ result = json.loads(response.read().decode())
+ print(json.dumps(result, indent=2))
+except URLError as e:
+ print(f"Error: {e}")
+except Exception as e:
+ print(f"Error: {e}")
\ No newline at end of file
diff --git a/test/test_openssl.py b/test/test_openssl.py
new file mode 100644
index 0000000..4918fff
--- /dev/null
+++ b/test/test_openssl.py
@@ -0,0 +1,20 @@
+import subprocess
+import os
+
+p12 = "C:\\Users\\jmest\\GolandProjects\\VerifactuMidAPI\\data\\certs\\personal.p12"
+pwd = "Mecedora12"
+out = "C:\\Users\\jmest\\GolandProjects\\VerifactuMidAPI\\data\\certs\\combined.pem"
+
+cmd = f'openssl pkcs12 -in "{p12}" -passin pass:{pwd} -nodes -out "{out}"'
+print(f"Running: {cmd}")
+
+try:
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ print(f"Return code: {result.returncode}")
+ if result.returncode != 0:
+ print(f"Error: {result.stderr}")
+ else:
+ print(f"Success! Output: {out}")
+ print(f"File size: {os.path.getsize(out)}")
+except Exception as e:
+ print(f"Exception: {e}")
\ No newline at end of file
diff --git a/test/test_personal.py b/test/test_personal.py
new file mode 100644
index 0000000..c3e69fe
--- /dev/null
+++ b/test/test_personal.py
@@ -0,0 +1,41 @@
+import base64
+import json
+from urllib.request import urlopen, Request
+from urllib.error import URLError
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.backends import default_backend
+
+API_URL = "http://localhost:6789"
+
+# Get public key
+req = Request(f"{API_URL}/api/v1/auth/public-key", method="GET")
+with urlopen(req, timeout=10) as response:
+ result = json.loads(response.read().decode())
+ pub_key_pem = base64.b64decode(result["public_key"])
+
+# Encrypt password
+public_key = serialization.load_pem_public_key(pub_key_pem, default_backend())
+password = "Mecedora12"
+encrypted = public_key.encrypt(password.encode(), padding.PKCS1v15())
+encrypted_b64 = base64.b64encode(encrypted).decode()
+
+# Register certificate
+data = {
+ "cert_name": "personal",
+ "cert_path": "C:/Users/jmest/GolandProjects/VerifactuMidAPI/data/certs/personal.p12",
+ "password_encrypted": encrypted_b64
+}
+
+req = Request(
+ f"{API_URL}/api/v1/auth/register",
+ data=json.dumps(data).encode(),
+ method="POST"
+)
+req.add_header("Content-Type", "application/json")
+try:
+ with urlopen(req, timeout=30) as response:
+ result = json.loads(response.read().decode())
+ print(json.dumps(result, indent=2))
+except URLError as e:
+ print(f"Error: {e}")
\ No newline at end of file
diff --git a/test/test_simulate.py b/test/test_simulate.py
new file mode 100644
index 0000000..5d92e16
--- /dev/null
+++ b/test/test_simulate.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+"""
+Test infrastructure for VeriFactu API certificate validation.
+"""
+
+import base64
+import json
+import os
+import sys
+from pathlib import Path
+from urllib.request import urlopen, Request
+from urllib.error import URLError
+
+API_URL = "http://localhost:6789"
+
+try:
+ from cryptography.hazmat.primitives import serialization
+ from cryptography.hazmat.primitives.asymmetric import padding
+ from cryptography.hazmat.backends import default_backend
+ HAS_CRYPTO = True
+except ImportError:
+ HAS_CRYPTO = False
+
+
+class VeriFactuTester:
+ def __init__(self):
+ self.api_url = API_URL
+ self.certs_dir = Path(__file__).parent / "certs"
+
+ def check_health(self):
+ """Check if API is running."""
+ try:
+ req = Request(f"{self.api_url}/api/v1/health", method="GET")
+ with urlopen(req, timeout=5) as response:
+ return json.loads(response.read().decode())
+ except:
+ return None
+
+ def get_public_key(self):
+ """Get public key from API."""
+ try:
+ req = Request(f"{self.api_url}/api/v1/auth/public-key", method="GET")
+ with urlopen(req, timeout=10) as response:
+ result = json.loads(response.read().decode())
+ return base64.b64decode(result["public_key"])
+ except Exception as e:
+ print(f"ERROR getting public key: {e}")
+ return None
+
+ def encrypt_password(self, public_key_pem, password):
+ """Encrypt password with public key."""
+ if not HAS_CRYPTO:
+ print("WARNING: cryptography not available")
+ return base64.b64encode(password.encode()).decode()
+
+ try:
+ public_key = serialization.load_pem_public_key(public_key_pem, default_backend())
+ encrypted = public_key.encrypt(
+ password.encode(),
+ padding.PKCS1v15()
+ )
+ return base64.b64encode(encrypted).decode()
+ except Exception as e:
+ print(f"ERROR encrypting password: {e}")
+ return None
+
+ def register_certificate(self, cert_path, encrypted_password, cert_name):
+ """Register certificate via API."""
+ data = {
+ "cert_name": cert_name,
+ "cert_path": cert_path,
+ "password_encrypted": encrypted_password
+ }
+
+ try:
+ req = Request(
+ f"{self.api_url}/api/v1/auth/register",
+ data=json.dumps(data).encode(),
+ method="POST"
+ )
+ req.add_header("Content-Type", "application/json")
+ with urlopen(req, timeout=30) as response:
+ return json.loads(response.read().decode())
+ except URLError as e:
+ return {"error": str(e), "success": False}
+ except Exception as e:
+ return {"error": str(e), "success": False}
+
+ def test_certificate(self, cert_file, password, expected_result, test_name):
+ """Test a single certificate."""
+ print(f"\n--- Testing: {test_name} ---")
+
+ pub_key = self.get_public_key()
+ if not pub_key:
+ print("ERROR: Cannot get public key")
+ return False
+
+ enc_password = self.encrypt_password(pub_key, password)
+ if not enc_password:
+ print("ERROR: Cannot encrypt password")
+ return False
+
+ result = self.register_certificate(cert_file, enc_password, test_name)
+
+ print(f"API Response: {json.dumps(result, indent=2)}")
+
+ return result
+
+
+if __name__ == "__main__":
+ print("This module should be imported, not run directly.")
+ print("Use: python test/run_tests.py")
\ No newline at end of file
diff --git a/test/test_validate.py b/test/test_validate.py
new file mode 100644
index 0000000..80f3499
--- /dev/null
+++ b/test/test_validate.py
@@ -0,0 +1,55 @@
+import base64
+import json
+from urllib.request import urlopen, Request
+from urllib.error import URLError
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.backends import default_backend
+
+API_URL = "http://localhost:6789"
+
+print("=" * 60)
+print("Test: Enviar Factura con Validation")
+print("=" * 60)
+
+invoice = {
+ "tipo": "alta",
+ "factura": {
+ "emisor_nif": "53950250R",
+ "num_serie": "FV2026/001",
+ "fecha_expedicion": "17-04-2026",
+ "tipo_factura": "F1",
+ "descripcion": "Factura de prueba",
+ "destinatario": {
+ "nombre": "Cliente Test SL",
+ "nif": "B12345678"
+ },
+ "iva": [
+ {"base": 100.00, "cuota": 21.00, "tipo": 21.0}
+ ],
+ "importe_total": 121.00
+ },
+ "sistema": {
+ "nombre": "VeriFactu API",
+ "nif_proveedor": "53950250R",
+ "version": "1.0"
+ }
+}
+
+print("\nEnviando factura...")
+
+req = Request(
+ f"{API_URL}/api/v1/facturas",
+ data=json.dumps(invoice).encode(),
+ method="POST"
+)
+req.add_header("Content-Type", "application/json")
+
+try:
+ with urlopen(req, timeout=30) as response:
+ result = json.loads(response.read().decode())
+ print(json.dumps(result, indent=2))
+except URLError as e:
+ print(f"Error HTTP: {e}")
+except Exception as e:
+ print(f"Error: {type(e).__name__}: {e}")
\ No newline at end of file
diff --git a/test/validate_temp.py b/test/validate_temp.py
new file mode 100644
index 0000000..a25b1af
--- /dev/null
+++ b/test/validate_temp.py
@@ -0,0 +1,44 @@
+import sys
+import datetime
+import os
+from cryptography import x509
+from cryptography.hazmat.primitives.serialization import pkcs12
+from cryptography.hazmat.backends import default_backend
+
+cert_path = sys.argv[1]
+password = sys.argv[2]
+
+try:
+ if not os.path.exists(cert_path):
+ print("NOT_FOUND")
+ sys.exit(1)
+
+ with open(cert_path, "rb") as f:
+ p12_data = f.read()
+
+ private_key, cert, additional_certs = pkcs12.load_key_and_certificates(
+ p12_data, password.encode(), default_backend()
+ )
+
+ now = datetime.datetime.now(datetime.timezone.utc)
+ not_after = cert.not_valid_after_utc.replace(tzinfo=datetime.timezone.utc)
+ not_before = cert.not_valid_before_utc.replace(tzinfo=datetime.timezone.utc)
+
+ if now > not_after:
+ print("EXPIRED")
+ sys.exit(1)
+
+ if now < not_before:
+ print("NOT_YET_VALID")
+ sys.exit(1)
+
+ days_until = (not_after - now).days
+
+ print(f"OK:{days_until}")
+
+except FileNotFoundError:
+ print("NOT_FOUND")
+ sys.exit(1)
+except Exception as e:
+ print("INVALID")
+ sys.exit(1)
\ No newline at end of file