Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0f2a5d66a |
20
tests/libredesk/custom/test_health.py
Normal file
20
tests/libredesk/custom/test_health.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"""libredesk — health check: the app's /health endpoint responds 200 through Traefik.
|
||||||
|
|
||||||
|
This is the same endpoint the compose healthcheck hits internally (:9000/health); exercising it via
|
||||||
|
the public domain confirms the app is bound and the proxy route is wired.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
||||||
|
from harness import http as harness_http # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_libredesk_health(live_app):
|
||||||
|
"""GET /health → 200."""
|
||||||
|
url = f"https://{live_app}/health"
|
||||||
|
status, _ = harness_http.retry_http_get(url, expect_status=(200,), max_wait=120, interval=5)
|
||||||
|
assert status == 200, f"GET {url} HTTP {status} (expected 200)"
|
||||||
49
tests/libredesk/custom/test_ui.py
Normal file
49
tests/libredesk/custom/test_ui.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""libredesk — UI probe: the served root HTML is the real LibreDesk app, not a fallback page.
|
||||||
|
|
||||||
|
/health passing only proves the process is up. This asserts the front-end SPA is actually bound and
|
||||||
|
serving its own shell — a wedged backend or a misrouted proxy would 200 with generic/empty content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
|
||||||
|
from harness import http as harness_http # noqa: E402
|
||||||
|
|
||||||
|
_CTX = ssl.create_default_context()
|
||||||
|
_CTX.check_hostname = False
|
||||||
|
_CTX.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
|
||||||
|
def _get_body(url: str) -> tuple[int, str]:
|
||||||
|
req = urllib.request.Request(url, method="GET")
|
||||||
|
with urllib.request.urlopen(req, timeout=15, context=_CTX) as r:
|
||||||
|
return r.status, r.read().decode(errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def test_libredesk_serves_app_shell(live_app):
|
||||||
|
"""GET /; assert LibreDesk-specific brand or SPA-asset markers in the served HTML."""
|
||||||
|
url = f"https://{live_app}/"
|
||||||
|
|
||||||
|
def _ready():
|
||||||
|
try:
|
||||||
|
status, body = _get_body(url)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return None
|
||||||
|
return body if status in (200, 301, 302) else None
|
||||||
|
|
||||||
|
body = harness_http.assert_converges(_ready, f"GET {url}", max_wait=120, interval=5)
|
||||||
|
lower = body.lower()
|
||||||
|
|
||||||
|
# Brand markers: the app title / meta / bundle references carry "libredesk".
|
||||||
|
brand = [m for m in ("libredesk", "libre desk") if m in lower]
|
||||||
|
# SPA asset markers: the built front-end serves hashed JS/CSS bundles + a favicon.
|
||||||
|
assets = [m for m in ("/assets/", "favicon", ".js", ".css", "<div id=\"app\"", "id=app") if m in body]
|
||||||
|
|
||||||
|
assert brand or assets, (
|
||||||
|
f"GET {url} HTML has no LibreDesk brand or SPA-asset markers. Excerpt: {body[:300]!r}"
|
||||||
|
)
|
||||||
49
tests/libredesk/install_steps.sh
Executable file
49
tests/libredesk/install_steps.sh
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# libredesk — INSTALL-TIME secret hook.
|
||||||
|
#
|
||||||
|
# Runs during the install tier AFTER `abra app new` + EXTRA_ENV + `abra app secret generate --all`
|
||||||
|
# and BEFORE the single `abra app deploy` (lifecycle.py::_run_install_steps). LibreDesk validates
|
||||||
|
# two secrets with formats that abra's generic generator does not guarantee, so we insert compliant
|
||||||
|
# values here (at a bumped version — swarm forbids overwriting a secret at the same version) and
|
||||||
|
# point the .env at them, so the recipe deploys ONCE, healthy, with no reconverge:
|
||||||
|
# - enc_key : exactly 32 hex chars (AES-256 encryption_key) → openssl rand -hex 16
|
||||||
|
# - admin_pwd : 10–72 chars incl. upper+lower+number+special (the app rejects weaker "System"
|
||||||
|
# user passwords, which abra's alphanumeric generator can produce)
|
||||||
|
# db_password is fine as abra-generated (no special format), so it is left untouched.
|
||||||
|
#
|
||||||
|
# Env supplied by the harness:
|
||||||
|
# CCCI_APP_DOMAIN — the per-run libredesk app domain (== abra app name)
|
||||||
|
# CCCI_APP_ENV — path to the app's .env (the one `abra app deploy` reads)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${CCCI_APP_DOMAIN:?missing}"
|
||||||
|
ENV_PATH="${CCCI_APP_ENV:?missing}"
|
||||||
|
|
||||||
|
# Insert a compliant value for <secret> and repoint SECRET_<VAR>_VERSION at the bumped version.
|
||||||
|
insert_secret() {
|
||||||
|
local secret="$1" env_var="$2" value="$3"
|
||||||
|
local cur new num
|
||||||
|
cur=$(grep -E "^\s*${env_var}=" "$ENV_PATH" | tail -1 | cut -d= -f2 | tr -d '"\r' || echo "v1")
|
||||||
|
cur=${cur:-v1}
|
||||||
|
num=$(( ${cur#v} + 1 )); new="v${num}"
|
||||||
|
# abra forbids overwriting an existing version; insert at the fresh version. -C creates it, -o
|
||||||
|
# allows overwrite if a stale one exists, --no-input for non-interactive.
|
||||||
|
local log
|
||||||
|
log=$(abra app secret insert "$CCCI_APP_DOMAIN" "$secret" "$new" "$value" --no-input -C -o 2>&1) ||
|
||||||
|
log=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN $secret $new $value --no-input -C -o" /dev/null 2>&1) ||
|
||||||
|
{ echo " install_steps: abra app secret insert ${secret}@${new} failed: $log"; exit 1; }
|
||||||
|
sed -i "s|^\s*${env_var}=.*|${env_var}=${new}|" "$ENV_PATH"
|
||||||
|
echo " install_steps: ${secret} inserted at ${new} (was ${cur})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 32 hex chars = 16 bytes (AES-256 key LibreDesk expects for encryption_key).
|
||||||
|
ENC_KEY=$(openssl rand -hex 16)
|
||||||
|
|
||||||
|
# 10–72 chars with upper+lower+number+special. Build deterministically-compliant: a fixed
|
||||||
|
# upper+lower+digit+special prefix plus random hex entropy (lowercase letters + digits).
|
||||||
|
ADMIN_PWD="Ci$(openssl rand -hex 12)Aa1!"
|
||||||
|
|
||||||
|
insert_secret enc_key SECRET_ENC_KEY_VERSION "$ENC_KEY"
|
||||||
|
insert_secret admin_pwd SECRET_ADMIN_PWD_VERSION "$ADMIN_PWD"
|
||||||
|
|
||||||
|
echo " libredesk install_steps: enc_key (32-hex) + admin_pwd (complex) inserted; deploy will use them"
|
||||||
32
tests/libredesk/recipe_meta.py
Normal file
32
tests/libredesk/recipe_meta.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Per-recipe harness config for libredesk (LibreDesk — open-source, self-hosted customer-support /
|
||||||
|
# help desk; recipe repo recipe-maintainers/Libre-Desk, abra TYPE=libredesk).
|
||||||
|
#
|
||||||
|
# Stack (compose.yml): app (libredesk/libredesk:v2.4.0, serves on :9000 behind Traefik) + db
|
||||||
|
# (postgres:17-alpine, user/db=libredesk) + redis (redis:7-alpine). abra-entrypoint.sh self-installs
|
||||||
|
# the schema and runs migrations on boot (`--install --idempotent-install` then `--upgrade`), so the
|
||||||
|
# stack comes up ready with no post-deploy step.
|
||||||
|
#
|
||||||
|
# Secrets have STRICT formats that abra's generic generator does not satisfy, so install_steps.sh
|
||||||
|
# inserts compliant values before the single deploy (see that file):
|
||||||
|
# - enc_key must be 32 hex chars (AES-256 key) → openssl rand -hex 16
|
||||||
|
# - admin_pwd must be 10–72 chars, upper+lower+num+sym → the "System" user's password
|
||||||
|
# - db_password is abra-generated (no special format) and left as-is.
|
||||||
|
|
||||||
|
HEALTH_PATH = "/health" # the app's own healthcheck endpoint (compose hits :9000/health)
|
||||||
|
HEALTH_OK = (200,)
|
||||||
|
DEPLOY_TIMEOUT = 600 # app + postgres + redis; app runs install+migrate on first boot
|
||||||
|
HTTP_TIMEOUT = 300
|
||||||
|
BACKUP_CAPABLE = True # db carries backupbot.backup labels (pg_dump pre-hook / psql restore-hook)
|
||||||
|
|
||||||
|
# Only one published version exists so far (0.1.0+v2.4.0), so there is no PRIOR deployable base to
|
||||||
|
# upgrade FROM — the upgrade rung is intentionally N/A (a declared skip, never promoted to a pass).
|
||||||
|
# Drop this once a second version is published; the dynamic base then resolves it (last-green warm
|
||||||
|
# canonical → same-version step-back → main tip).
|
||||||
|
EXPECTED_NA = {
|
||||||
|
"upgrade": "only one published version so far (0.1.0+v2.4.0) — no prior deployable base to "
|
||||||
|
"upgrade FROM yet; drop this when a second version ships",
|
||||||
|
}
|
||||||
|
|
||||||
|
# canon §2.B: enroll as a DATA-WARM canonical (all recipes enrolled — operator 2026-06-17).
|
||||||
|
# The weekly sweep promotes this recipe's canonical to its latest green RELEASE TAG.
|
||||||
|
WARM_CANONICAL = True
|
||||||
Reference in New Issue
Block a user