Files
haproxy-openmanager/backend/tests/test_acme_duplicate_backend.py
taylanbakircioglu 07942a82e8 feat: ACME stability & enterprise audit (v1.4.0) — fixes #10 #11 #12
Issue #10 — Silent timezone failure on ACME certificate save
- Normalize tz-aware expiry_date to UTC tz-naive before INSERT/UPDATE in
  ssl_certificates (TIMESTAMP WITHOUT TIME ZONE) — restores ACME download path

Issue #11 — Duplicate _acme_challenge_backend in generated config
- Generator guard: skip auto-append when backend already rendered
- Agent-sync filter: _should_sync_backend() drops system-managed backends and
  detaches their server rows to prevent orphans
- Restore filter: IGNORED_BACKENDS skips reserved names during cluster restore
- Parser warning: reserved_backend_names blocks accidental manual import
- Cleanup migration: removes orphan rows + cascading server entries (idempotent)

Issue #12 — Validated orders required manual completion
- New 60s background task complete_pending_acme_orders, flag-independent,
  multi-replica safe via FOR UPDATE SKIP LOCKED + 30s updated_at watermark
- Per-order pg_advisory_lock(0x41434D45, order_id) serializes UI-Complete and
  auto-task races; idempotency guard returns existing certificate cleanly
- retry_order endpoint reports in_progress: true within 30s window so the UI
  surfaces an info toast instead of duplicating CA requests
- ACMEAutomation surfaces stuck orders (status=valid && !ssl_certificate_id)
  with a one-click Complete action and Cancel fallback; conditional 30s polling

Other hardening
- ACME state machine error_detail persisted as structured JSON across challenge,
  finalize, download stages for actionable post-mortems
- CertificateRequest Pydantic model: domain regex + min_length/max_length and
  cluster_ids defaulting to all ACME-enabled clusters when "global" is selected
- Renewal cluster fallback now requires acme_enabled=TRUE in addition to active
- _complete_certificate preserves manual cluster assignments on renewal,
  surfaces cluster_errors, raises explicit error on missing private key
- Audit logging covers acme_certificate_requested/revoked, ca_chain_imported,
  account created/deactivated/purged, order retried/cancelled
- Settings UI exposes acme.staging_url_override for private test CAs (Pebble)
- Schema additions: acme_challenges.attempts (default 0) and last_attempt_at,
  index idx_letsencrypt_orders_status_updated; all migrations idempotent

Tests (41/41 passing)
- test_acme_expiry_normalize, test_acme_duplicate_backend,
  test_acme_state_machine, test_acme_pydantic_validation,
  test_acme_audit_logging, test_acme_concurrency

CI / packaging
- docker-build.yml reads version.json and pushes additional product-version
  tag (e.g. 1.4.0) alongside latest and timestamp build id

Closes #10
Closes #11
Closes #12
2026-05-06 23:53:43 +03:00

60 lines
1.8 KiB
Python

"""
Issue #11 regression: duplicate `_acme_challenge_backend` in generated config.
Tests verify the layered defense:
1. agent.py _should_sync_backend filter
2. haproxy_config_parser.py reserved_backend_names check
"""
import pytest
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
class TestShouldSyncBackend:
def test_acme_challenge_backend_is_skipped(self):
from routers.agent import _should_sync_backend
assert _should_sync_backend('_acme_challenge_backend') is False
def test_user_backend_is_synced(self):
from routers.agent import _should_sync_backend
assert _should_sync_backend('my-app-backend') is True
assert _should_sync_backend('api-prod') is True
def test_underscore_prefixed_names_are_skipped(self):
from routers.agent import _should_sync_backend
assert _should_sync_backend('_internal') is False
assert _should_sync_backend('_anything') is False
class TestParserReservedNames:
def test_acme_challenge_backend_not_persisted(self):
from utils.haproxy_config_parser import HAProxyConfigParser
cfg = """
global
daemon
defaults
mode http
frontend test_fe
bind *:80
default_backend my-app
backend my-app
server s1 10.0.0.1:80 check
backend _acme_challenge_backend
mode http
server _acme_mgmt 10.0.0.99:8080
"""
parser = HAProxyConfigParser()
result = parser.parse(cfg)
backend_names = [b.name for b in result.backends]
assert 'my-app' in backend_names
assert '_acme_challenge_backend' not in backend_names
# Should have warning
assert any('_acme_challenge_backend' in w.lower() or 'reserved' in w.lower()
for w in result.warnings)