From 4225af5b052e9bea26998fb8448c8294290c7da3 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 14 May 2026 20:50:55 +0200 Subject: [PATCH] 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/ --- documentacion/README.md | 9 +- documentacion/api.md | 8 +- documentacion/formato_datos.md | 2 +- documentacion/verifactu.md | 2 +- test/check_password.py | 26 +++ test/convert_cert.py | 34 +++ test/generate_certs.py | 89 ++++++++ test/invoice.json | 27 +++ test/run_tests.py | 139 ++++++++++++ test/simulate.py | 212 ++++++++++++++++++ test/test_cert.py | 68 ++++++ test/test_direct_aeat.py | 79 +++++++ test/test_direct_no_cert.py | 83 +++++++ .../test_directo_aeat.py | 0 test/test_invoice.py | 51 +++++ test/test_openssl.py | 20 ++ test/test_personal.py | 41 ++++ test/test_simulate.py | 112 +++++++++ test/test_validate.py | 55 +++++ test/validate_temp.py | 44 ++++ 20 files changed, 1092 insertions(+), 9 deletions(-) create mode 100644 test/check_password.py create mode 100644 test/convert_cert.py create mode 100644 test/generate_certs.py create mode 100644 test/invoice.json create mode 100644 test/run_tests.py create mode 100644 test/simulate.py create mode 100644 test/test_cert.py create mode 100644 test/test_direct_aeat.py create mode 100644 test/test_direct_no_cert.py rename test_directo_aeat.py => test/test_directo_aeat.py (100%) create mode 100644 test/test_invoice.py create mode 100644 test/test_openssl.py create mode 100644 test/test_personal.py create mode 100644 test/test_simulate.py create mode 100644 test/test_validate.py create mode 100644 test/validate_temp.py 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