Update documentation and organize test files

- Fix JSON syntax errors in api.md
- Fix typos in verifactu.md and formato_datos.md
- Update README.md with current status (anulation implemented)
- Move all Python test files to test/ folder
- Remove certificates from data/certs/ and test/certs/
This commit is contained in:
admin 2026-05-14 20:50:55 +02:00
parent 1ad84a3b14
commit 4225af5b05
20 changed files with 1092 additions and 9 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

26
test/check_password.py Normal file
View File

@ -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!")

34
test/convert_cert.py Normal file
View File

@ -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}")

89
test/generate_certs.py Normal file
View File

@ -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")

27
test/invoice.json Normal file
View File

@ -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"
}
}

139
test/run_tests.py Normal file
View File

@ -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())

212
test/simulate.py Normal file
View File

@ -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()

68
test/test_cert.py Normal file
View File

@ -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")

79
test/test_direct_aeat.py Normal file
View File

@ -0,0 +1,79 @@
import urllib.request
import urllib.parse
URL = "https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP"
soap_request = """<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/>
<soapenv:Body>
<RegFactuSistemaFacturacion xmlns:sum="SuministroLR">
<sum:Cabecera>
<sum1:ObligadoEmision xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd">
<sum1:NombreRazon>TEST</sum1:NombreRazon>
<sum1:NIF>53950250R</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAlta xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd">
<sum1:IDVersion>1.0</sum1:IDVersion>
<sum1:IDFactura>
<sum1:IDEmisorFactura>53950250R</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>FV2026/001</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>17-04-2026</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<sum1:NombreRazonEmisor>TEST EMPRESA</sum1:NombreRazonEmisor>
<sum1:TipoFactura>F1</sum1:TipoFactura>
<sum1:DescripcionOperacion>Factura de prueba</sum1:DescripcionOperacion>
<sum1:Desglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>01</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>100.00</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>21.00</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
</sum1:Desglose>
<sum1:CuotaTotal>21.00</sum1:CuotaTotal>
<sum1:ImporteTotal>121.00</sum1:ImporteTotal>
<sum1:Encadenamiento>
<sum1:PrimerRegistro>S</sum1:PrimerRegistro>
</sum1:Encadenamiento>
<sum1:SistemaInformatico>
<sum1:NombreRazon>TEST</sum1:NombreRazon>
<sum1:NIF>53950250R</sum1:NIF>
<sum1:NombreSistemaInformatico>TEST</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>1</sum1:IdSistemaInformatico>
<sum1:Version>1.0</sum1:Version>
<sum1:NumeroInstalacion>1</sum1:NumeroInstalacion>
<sum1:TipoUsoPosibleSoloVerifactu>S</sum1:TipoUsoPosibleSoloVerifactu>
</sum1:SistemaInformatico>
<sum1:FechaHoraHusoGenRegistro>17-04-2026T12:00:00</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoHuella>SHA-256</sum1:TipoHuella>
<sum1:Huella>TESTHASH</sum1:Huella>
</sum1:RegistroAlta>
</sum:RegistroFactura>
</RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>"""
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}")

View File

@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header/>
<soapenv:Body>
<RegFactuSistemaFacturacion xmlns:sum="SuministroLR">
<sum:Cabecera>
<sum1:ObligadoEmision xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd">
<sum1:NombreRazon>TEST EMPRESA SL</sum1:NombreRazon>
<sum1:NIF>53950250R</sum1:NIF>
</sum1:ObligadoEmision>
</sum:Cabecera>
<sum:RegistroFactura>
<sum1:RegistroAlta xmlns:sum1="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd">
<sum1:IDVersion>1.0</sum1:IDVersion>
<sum1:IDFactura>
<sum1:IDEmisorFactura>53950250R</sum1:IDEmisorFactura>
<sum1:NumSerieFactura>FV2026/TEST001</sum1:NumSerieFactura>
<sum1:FechaExpedicionFactura>17-04-2026</sum1:FechaExpedicionFactura>
</sum1:IDFactura>
<sum1:NombreRazonEmisor>TEST EMPRESA SL</sum1:NombreRazonEmisor>
<sum1:TipoFactura>F1</sum1:TipoFactura>
<sum1:DescripcionOperacion>Factura de prueba test</sum1:DescripcionOperacion>
<sum1:Desglose>
<sum1:DetalleDesglose>
<sum1:ClaveRegimen>01</sum1:ClaveRegimen>
<sum1:CalificacionOperacion>S1</sum1:CalificacionOperacion>
<sum1:TipoImpositivo>01</sum1:TipoImpositivo>
<sum1:BaseImponibleOimporteNoSujeto>100.00</sum1:BaseImponibleOimporteNoSujeto>
<sum1:CuotaRepercutida>21.00</sum1:CuotaRepercutida>
</sum1:DetalleDesglose>
</sum1:Desglose>
<sum1:CuotaTotal>21.00</sum1:CuotaTotal>
<sum1:ImporteTotal>121.00</sum1:ImporteTotal>
<sum1:Encadenamiento>
<sum1:PrimerRegistro>S</sum1:PrimerRegistro>
</sum1:Encadenamiento>
<sum1:SistemaInformatico>
<sum1:NombreRazon>TEST API</sum1:NombreRazon>
<sum1:NIF>53950250R</sum1:NIF>
<sum1:NombreSistemaInformatico>TEST-API</sum1:NombreSistemaInformatico>
<sum1:IdSistemaInformatico>1</sum1:IdSistemaInformatico>
<sum1:Version>1.0</sum1:Version>
<sum1:NumeroInstalacion>1</sum1:NumeroInstalacion>
<sum1:TipoUsoPosibleSoloVerifactu>S</sum1:TipoUsoPosibleSoloVerifactu>
</sum1:SistemaInformatico>
<sum1:FechaHoraHusoGenRegistro>17-04-2026T12:00:00</sum1:FechaHoraHusoGenRegistro>
<sum1:TipoHuella>SHA-256</sum1:TipoHuella>
<sum1:Huella>0A1B2C3D4E5F6</sum1:Huella>
</sum1:RegistroAlta>
</sum:RegistroFactura>
</RegFactuSistemaFacturacion>
</soapenv:Body>
</soapenv:Envelope>"""
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}")

51
test/test_invoice.py Normal file
View File

@ -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}")

20
test/test_openssl.py Normal file
View File

@ -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}")

41
test/test_personal.py Normal file
View File

@ -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}")

112
test/test_simulate.py Normal file
View File

@ -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")

55
test/test_validate.py Normal file
View File

@ -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}")

44
test/validate_temp.py Normal file
View File

@ -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)