#!/usr/bin/env bash # End-to-end checks of Etappe 4d (Divi theme options: read, change, unknown font with suggestions, reset to default, # snapshot + restore; custom font upload from a public URL) through the OAuth-protected endpoint against the real # Divi. Runs INSIDE the DDEV web container: # cd /home/janniclas/Projekte/WordPress && timeout 900 ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-theme-fonts-smoke.sh # Needs network access from the container (fetches one small woff2 from fonts.gstatic.com). The EXIT trap puts the # options et_divi and et_uploaded_fonts back byte-exact (serialized, base64 through the environment), removes the # uploaded font with Divi's own FontsUtility::font_remove (files included) and fails the script when a restore fails. set -euo pipefail cd /var/www/html # shellcheck source=lib.sh source wp-content/plugins/WP-LIT-MCP/tests/Integration/lib.sh ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1) RUN="$(date +%s)-$$" FONT_NAME="LIT Smoke $RUN" FONT_URL="https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2" # $1 = option name. An option that does not exist is captured as the sentinel string, so restore() can delete it again # (update_option(false) would leave an empty row behind, and Divi's font_add then fails on that ''). capture() { wp eval "echo base64_encode(serialize(get_option('$1', '__lit_absent__')));"; } ORIGINAL_THEME=$(capture et_divi) ORIGINAL_FONTS=$(capture et_uploaded_fonts) [ -n "$ORIGINAL_THEME" ] || { echo "FAIL: could not capture et_divi"; exit 1; } [ -n "$ORIGINAL_FONTS" ] || { echo "FAIL: could not capture et_uploaded_fonts"; exit 1; } curl -fsS -m 20 -o /dev/null "$FONT_URL" || { echo "FAIL: $FONT_URL is not reachable from the container"; exit 1; } restore() { # $1 = option name, $2 = name of the environment variable that holds the capture local LIT_CAPTURE="$2"; export LIT_CAPTURE LIT_OPTION="$1" wp eval '$n = getenv("LIT_OPTION"); $v = unserialize(base64_decode(getenv(getenv("LIT_CAPTURE")))); if ($v === "__lit_absent__") { delete_option($n); exit(get_option($n, "__lit_absent__") === $v ? 0 : 1); } update_option($n, $v); exit(serialize(get_option($n)) === serialize($v) ? 0 : 1);' >/dev/null 2>&1 \ || { echo "FAIL: $1 not restored"; return 1; } } cleanup() { local failed=0 # Remove the font through Divi (deletes the files), then put the option back exactly as it was. LIT_FONT="$FONT_NAME" wp eval 'ET\Builder\VisualBuilder\Fonts\FontsUtility::font_remove(getenv("LIT_FONT"));' >/dev/null 2>&1 || true restore et_uploaded_fonts LIT_FONTS || failed=1 restore et_divi LIT_THEME || failed=1 wp eval 'ET_Core_PageResource::remove_static_resources("all", "all", true);' >/dev/null 2>&1 || true cleanup_app_passwords_for "$ADMIN_LOGIN" return "$failed" } export LIT_THEME="$ORIGINAL_THEME" LIT_FONTS="$ORIGINAL_FONTS" trap 'rc=$?; cleanup || rc=1; guard_exit "$rc" || exit $?' EXIT connect "$ADMIN_LOGIN" # confirm : dry run must answer confirm_required; then the confirmed call; prints the result. confirm() { local dry rev dry=$(ability "$1" "$2") [ "$(error_code "$dry")" = "confirm_required" ] || fail "$1 dry run: $dry" rev=$(echo "$dry" | jq -r '.__error | fromjson | .design_rev') ability "$1" "$(echo "$2" | jq -c --arg r "$rev" '. + {confirm: true, design_rev: $r}')" } theme_option() { # $1 = key -> the stored value ('' when the key is not set) wp eval "\$o = get_option('et_divi'); echo array_key_exists('$1', \$o) ? var_export(\$o['$1'], true) : '';" } # 1. Read: the tool lists the options with effective values and a design_rev. GET=$(ability lit/divi-theme-options-get '{}') echo "$GET" | jq -e '.design_rev | type == "string"' >/dev/null || fail "theme-options-get without design_rev: $GET" echo "$GET" | jq -e '[.. | objects | select(has("effective"))] | length >= 20' >/dev/null \ || fail "theme-options-get does not list the 20 options with effective values: $GET" pass "theme options listed" # 2. Change two options (snapshot is taken), stale design_rev refused, values stored with Divi's types. UPDATE=$(jq -cn '{set: {body_font_size: 18, content_width: "1200"}}') DONE=$(confirm lit/divi-theme-options-update "$UPDATE") [ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "theme options update: $DONE" THEME_SNAPSHOT=$(echo "$DONE" | jq -r '.snapshot_id') [ "$(theme_option body_font_size)" = "18" ] || fail "body_font_size not stored" [ "$(theme_option content_width)" = "1200" ] || fail "content_width not stored as an integer" # A different value: an unchanged set is answered as "no change" before design_rev is looked at. STALE=$(ability lit/divi-theme-options-update "$(echo "$UPDATE" | jq -c '.set.body_font_size = 19 | . + {confirm: true, design_rev: "veraltet"}')") [ "$(error_code "$STALE")" = "rev_mismatch" ] || fail "stale design_rev: $STALE" pass "theme options changed ($THEME_SNAPSHOT), stale design_rev refused" # 3. Validation. Range and unknown keys are already refused by the input schema (before the plugin runs); unknown font # names and "none" reach the plugin, which collects them with suggestions. Nothing is written in any case. BAD=$(ability lit/divi-theme-options-update '{"set": {"heading_font": "Robotto", "body_font": "none"}}') [ "$(error_code "$BAD")" = "validation_failed" ] || fail "invalid input accepted: $BAD" echo "$BAD" | grep -q 'Roboto' || fail "no font suggestion in: $BAD" echo "$BAD" | grep -q 'body_font' || fail "none not refused with its path: $BAD" BAD=$(ability lit/divi-theme-options-update '{"set": {"body_font_size": 99}}') echo "$BAD" | jq -e 'has("__error")' >/dev/null || fail "out-of-range size accepted: $BAD" BAD=$(ability lit/divi-theme-options-update '{"set": {"colour": 1}}') echo "$BAD" | jq -e 'has("__error")' >/dev/null || fail "unknown key accepted: $BAD" [ "$(theme_option body_font_size)" = "18" ] || fail "invalid call changed body_font_size" [ "$(theme_option heading_font)" = "" ] || fail "invalid call changed heading_font" pass "unknown font with suggestions, none, range and unknown key refused" # 4. A known Google font is accepted; null resets an option to Divi's default (key removed). DONE=$(confirm lit/divi-theme-options-update '{"set": {"heading_font": "Roboto", "content_width": null}}') [ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "font + reset: $DONE" [ "$(theme_option heading_font)" = "'Roboto'" ] || fail "heading_font not stored" [ "$(theme_option content_width)" = "" ] || fail "content_width not removed by null" pass "Google font stored, null resets to Divi's default" # 5. Restore the snapshot of step 2: body_font_size and content_width are back as before step 2; heading_font too. LIST=$(ability lit/divi-design-snapshots '{"area": "theme_options"}') echo "$LIST" | jq -e --arg s "$THEME_SNAPSHOT" 'any(.snapshots[]; .id == $s)' >/dev/null || fail "snapshot not listed: $LIST" DONE=$(confirm lit/divi-design-restore "$(jq -cn --arg s "$THEME_SNAPSHOT" '{snapshot_id: $s}')") [ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "restore: $DONE" # The snapshot of step 2 was taken before any change of this run: the three keys must equal their original state # (present with the same typed value, or absent) – compared against the captured et_divi, not against assumptions. for KEY in body_font_size content_width heading_font; do LIT_KEY="$KEY" wp eval '$o = unserialize(base64_decode(getenv("LIT_THEME"))); $n = get_option("et_divi"); $k = getenv("LIT_KEY"); exit(array_key_exists($k, $o) === array_key_exists($k, $n) && ($o[$k] ?? null) === ($n[$k] ?? null) ? 0 : 1);' \ || fail "$KEY not restored to its state before the first change" done pass "theme_options snapshot restored (body_font_size, content_width, heading_font as before)" # 6. Font upload: Probelauf names the file (format, bytes, sha256) and installs nothing. UPLOAD=$(jq -cn --arg n "$FONT_NAME" --arg u "$FONT_URL" '{name: $n, files: {woff2: $u}, weights: "all"}') DRY=$(ability lit/divi-fonts-upload "$UPLOAD") [ "$(error_code "$DRY")" = "confirm_required" ] || fail "font dry run: $DRY" echo "$DRY" | grep -q 'woff2' || fail "dry run does not name the format: $DRY" LIT_FONT="$FONT_NAME" wp eval 'exit(isset(get_option("et_uploaded_fonts", [])[getenv("LIT_FONT")]) ? 1 : 0);' \ || fail "dry run installed the font" pass "font dry run installs nothing" # 7. Confirmed upload: the font is in et_uploaded_fonts, the file exists, the name is now reserved. DONE=$(ability lit/divi-fonts-upload "$(echo "$UPLOAD" | jq -c '. + {confirm: true}')") [ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "font upload: $DONE" LIT_FONT="$FONT_NAME" wp eval '$f = get_option("et_uploaded_fonts", [])[getenv("LIT_FONT")] ?? null; exit($f && isset($f["font_url"]["woff2"]) ? 0 : 1);' \ || fail "font not stored in et_uploaded_fonts" AGAIN=$(ability lit/divi-fonts-upload "$(echo "$UPLOAD" | jq -c '. + {confirm: true}')") [ "$(error_code "$AGAIN")" != "" ] || fail "a second upload of the same name was accepted: $AGAIN" pass "font installed through Divi, same name refused" # 8. The uploaded font is a valid theme font now; Arial (web-safe) is refused as a new font name. DONE=$(confirm lit/divi-theme-options-update "$(jq -cn --arg n "$FONT_NAME" '{set: {body_font: $n}}')") [ "$(echo "$DONE" | jq -r '.ok')" = "true" ] || fail "uploaded font as body_font: $DONE" # A taken name (web-safe, Google, uploaded) is a conflict, see DiviFontsUploadAbility. RESERVED=$(ability lit/divi-fonts-upload "$(jq -cn --arg u "$FONT_URL" '{name: "Arial", files: {woff2: $u}, weights: "all"}')") [ "$(error_code "$RESERVED")" = "conflict" ] || fail "web-safe name accepted: $RESERVED" pass "uploaded font usable as theme font, web-safe name refused" # 9. A file whose content does not match its format key is refused (a PNG offered as woff2). WRONG=$(ability lit/divi-fonts-upload "$(jq -cn --arg n "$FONT_NAME Falsch" '{name: $n, files: {woff2: "https://s.w.org/images/wmark.png"}, weights: "all"}')") [ "$(error_code "$WRONG")" = "font_invalid" ] || fail "wrong content accepted: $WRONG" pass "content/format mismatch refused (font_invalid)" finish