lesavka/testing/tests/server_install_script_contract.rs

333 lines
14 KiB
Rust
Raw Normal View History

//! Contract tests for server install-time operational defaults.
//!
//! Scope: statically guard the generated `/etc/lesavka/server.env` values.
//! Targets: `scripts/install/server.sh`.
//! Why: HDMI capture adapter settings should be reproducible after reboot or
//! reinstall instead of living as one-off shell state.
const SERVER_INSTALL: &str = include_str!("../../scripts/install/server.sh");
#[test]
fn server_install_pins_hdmi_camera_and_display_defaults() {
for expected in [
"LESAVKA_CAM_OUTPUT=%s",
"LESAVKA_CAM_WIDTH=%s",
"LESAVKA_CAM_HEIGHT=%s",
"LESAVKA_CAM_FPS=%s",
"LESAVKA_HDMI_WIDTH=%s",
"LESAVKA_HDMI_HEIGHT=%s",
"LESAVKA_HDMI_SINK=%s",
"LESAVKA_HDMI_FBDEV=%s",
"LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s",
"LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=%s",
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s",
"LESAVKA_UPSTREAM_PAIR_SLACK_US=%s",
"LESAVKA_UPSTREAM_STALE_DROP_MS=%s",
2026-04-28 02:29:32 -03:00
"LESAVKA_SERVER_BIND_ADDR=%s",
"/etc/lesavka/uvc.env",
"LESAVKA_UVC_MAXPACKET=",
"LESAVKA_UVC_INTERVAL=",
"LESAVKA_UVC_WIDTH=",
"LESAVKA_UVC_HEIGHT=",
"LESAVKA_UVC_CODEC=",
"LESAVKA_UVC_CONTROL_READ_ONLY=",
2026-04-30 08:16:57 -03:00
"LESAVKA_REQUIRE_TLS=%s",
"LESAVKA_TLS_CERT=%s",
"LESAVKA_TLS_KEY=%s",
"LESAVKA_TLS_CLIENT_CA=%s",
] {
assert!(
SERVER_INSTALL.contains(expected),
"install script should emit {expected}"
);
}
assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_WIDTH:-1920}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_HEIGHT:-1080}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_FPS:-30}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_CAM_OUTPUT:-uvc}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_UVC_CODEC:-mjpeg}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_WIDTH:-1920}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_HEIGHT:-1080}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_SINK:-fbdevsink}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS:-1000}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:--45000}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_STALE_DROP_MS:-80}"));
2026-04-28 02:29:32 -03:00
assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_MAXPACKET:-1024}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_INTERVAL:-500000}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_WIDTH:-640}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEIGHT:-480}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_CONTROL_READ_ONLY:-0}"));
assert!(
!SERVER_INSTALL.contains("LESAVKA_UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}"),
"install script should not let ambient LESAVKA_UVC_CODEC leak into persisted defaults"
);
2026-04-28 02:29:32 -03:00
assert!(
!SERVER_INSTALL
.contains("LESAVKA_SERVER_BIND_ADDR=${LESAVKA_SERVER_BIND_ADDR:-0.0.0.0:50051}"),
2026-04-28 02:29:32 -03:00
"install script should not let ambient LESAVKA_SERVER_BIND_ADDR leak into persisted defaults"
);
assert!(
SERVER_INSTALL.contains("EnvironmentFile=-/etc/lesavka/uvc.env"),
"install script should feed live UVC runtime settings into services"
);
assert!(
!SERVER_INSTALL.contains("Environment=LESAVKA_UVC_CODEC=mjpeg"),
"install script should not pin UVC codec to mjpeg in the systemd units"
);
assert!(
SERVER_INSTALL.contains("LESAVKA_UVC_FALLBACK=0"),
"forced gadget rebuilds should fail loud instead of silently dropping UVC"
);
assert!(
SERVER_INSTALL.contains("validate_uvc_gadget_ready"),
"install script should verify that the UVC gadget comes back before declaring success"
);
assert!(
SERVER_INSTALL.contains("uvc_gadget_present"),
"install script should detect when the live gadget is missing the expected UVC function"
);
assert!(
SERVER_INSTALL.contains(
"UVC function is missing from the live gadget; forcing a rebuild before server start."
),
"install script should force a rebuild when the live gadget is attached but missing UVC"
);
assert!(
SERVER_INSTALL.contains("lesavka-core gadget rebuilt directly."),
"install script should trust the direct forced rebuild instead of immediately rerunning the oneshot core unit"
);
assert!(
SERVER_INSTALL.contains("sudo systemctl stop lesavka-uvc"),
"install script should stop the UVC helper before directly rebuilding the gadget underneath it"
);
assert!(
!SERVER_INSTALL.contains("sudo systemctl restart lesavka-core"),
"install script should not immediately rerun lesavka-core after a successful direct forced rebuild"
);
assert!(
SERVER_INSTALL.contains("video-output node did not appear after rebuild"),
"install script should explain why it refuses a half-applied UVC install"
);
assert!(
!SERVER_INSTALL.contains("RefuseManualStop=yes"),
"install script should not generate a UVC unit that blocks legitimate refreshes"
);
assert!(
SERVER_INSTALL.contains("KillMode=control-group"),
"install script should stop the whole UVC helper cgroup instead of leaving child processes behind"
);
assert!(
!SERVER_INSTALL.contains("KillMode=process"),
"install script should not leave the server or helper units in process-only kill mode"
);
assert!(
SERVER_INSTALL.contains("lesavka-uvc already active; runtime settings unchanged."),
"install script should avoid unnecessary UVC restarts when nothing changed"
);
assert!(
SERVER_INSTALL.contains("sudo systemctl start lesavka-uvc"),
"install script should start the UVC helper so the host enumerates the UVC function"
);
assert!(
SERVER_INSTALL.contains("lesavka-uvc started to attach the UVC gadget to the host."),
"install script should report when it starts the UVC helper for enumeration"
);
assert!(
SERVER_INSTALL.contains("Wants=lesavka-uvc.service"),
"server unit should pull in the external UVC helper on UVC installs"
);
assert!(
!SERVER_INSTALL.contains("Environment=LESAVKA_ALLOW_GADGET_CYCLE=1"),
"server unit should not auto-cycle the gadget while the external UVC helper owns it"
);
assert!(
SERVER_INSTALL.contains("/var/log/lesavka/server.log"),
"install script should keep server logs out of sticky /tmp"
);
assert!(
SERVER_INSTALL.contains("LESAVKA_SERVER_LOG_PATH=/var/log/lesavka/server.log"),
"server unit should point tracing at the non-sticky log path"
);
assert!(
SERVER_INSTALL.contains("clear_stale_server_listener"),
"install script should clear stale server listeners before restart"
);
assert!(
SERVER_INSTALL.contains("lsof -tiTCP:"),
"install script should inspect the bind port before starting a fresh server"
);
assert!(
SERVER_INSTALL.contains("/proc/net/tcp"),
"install script should fall back to procfs listener detection when lsof misses the owner"
);
assert!(
SERVER_INSTALL.contains("socket:["),
"install script should map listening socket inodes back to owning PIDs"
);
assert!(
SERVER_INSTALL.contains("ss -K state connected"),
"install script should clear hidden connected socket state before retrying the server port"
);
assert!(
SERVER_INSTALL.contains("ss -K state listening"),
"install script should ask the kernel to drop a hidden listening socket when no PID is visible"
);
assert!(
SERVER_INSTALL.contains("list_server_bound_inactive_lines"),
"install script should check for sockets that remain bound after listener teardown"
);
assert!(
SERVER_INSTALL.contains("bound-inactive tcp sockets"),
"install script should explain when a hidden socket stops listening but still blocks the port"
);
assert!(
SERVER_INSTALL.contains("unexpected process"),
"install script should fail loud instead of killing an unrelated process on the server port"
);
assert!(
SERVER_INSTALL.contains("kernel dropped the hidden TCP"),
"install script should report when kernel-level stale-socket cleanup succeeds"
);
assert!(
SERVER_INSTALL.contains("no owning PID could be identified"),
"install script should fail loud when a port is listening but procfs cannot identify the owner"
);
assert!(
SERVER_INSTALL.contains("validate_server_ready"),
"install script should verify that lesavka-server reaches a running state"
);
assert!(
SERVER_INSTALL.contains("active and listening"),
"install script should require the TCP listener before declaring server readiness"
);
assert!(
SERVER_INSTALL.contains("did not open TCP"),
"install script should explain active-but-not-listening server failures"
);
assert!(
SERVER_INSTALL.contains("failed to reach active/running state"),
"install script should explain server startup failures instead of claiming success"
);
}
2026-04-30 08:16:57 -03:00
#[test]
fn server_install_generates_mtls_identity_and_client_bundle() {
for expected in [
"ensure_server_tls_pki",
"openssl genrsa",
"extendedKeyUsage = serverAuth",
"extendedKeyUsage = clientAuth",
"LESAVKA_CLIENT_BUNDLE",
"lesavka-client-pki.tar.gz",
"LESAVKA_REQUIRE_TLS=%s",
"LESAVKA_TLS_CLIENT_CA=%s",
] {
assert!(
SERVER_INSTALL.contains(expected),
"server install script should include TLS/mTLS contract fragment {expected}"
);
}
assert!(
SERVER_INSTALL.contains("LESAVKA_REQUIRE_TLS:-1"),
"installed servers should require TLS by default"
);
assert!(
SERVER_INSTALL.contains("38.28.125.112"),
"server cert SAN defaults should include the current public relay endpoint"
);
}
#[test]
fn server_install_reports_installed_version_and_revision() {
assert!(
SERVER_INSTALL.contains("Installed:"),
"install script should print the installed build identity at the end"
);
assert!(
SERVER_INSTALL.contains("manifest_package_version"),
"install script should read the package version from the synced source manifest"
);
assert!(
SERVER_INSTALL.contains("\"$SRC_DIR/server/Cargo.toml\""),
"install script should report the server package semver from the server manifest"
);
assert!(
SERVER_INSTALL.contains("git -C \"$SCRIPT_REPO_ROOT\" rev-parse --short HEAD"),
"install script should print the installed git revision for operator clarity"
);
assert!(
SERVER_INSTALL.contains("Camera output:"),
"install script should print the persisted camera output in the footer"
);
2026-04-28 02:29:32 -03:00
assert!(
SERVER_INSTALL.contains("Server bind:"),
"install script should print the persisted server bind address in the footer"
);
assert!(
SERVER_INSTALL.contains("UVC codec:"),
"install script should print the persisted UVC codec in the footer"
);
}
#[test]
fn server_install_keeps_uac_sanity_helper_available() {
assert!(
SERVER_INSTALL.contains("run_uac_output_sanity.sh"),
"install script should ship the root UAC sanity helper"
);
assert!(
SERVER_INSTALL.contains("/usr/local/bin/lesavka-uac-sanity"),
"install script should install the UAC sanity helper to a stable path"
);
}
2026-04-24 22:43:25 -03:00
#[test]
fn server_install_grants_operator_audio_group_access() {
assert!(
SERVER_INSTALL.contains("usermod -aG audio"),
"install script should grant the invoking operator access to ALSA devices"
);
assert!(
SERVER_INSTALL.contains("SUDO_USER"),
"install script should target the sudo-invoking operator instead of a hard-coded account"
);
}
#[test]
fn server_install_builds_eye_links_from_capture_nodes_only() {
assert!(
SERVER_INSTALL.contains("/sys/class/video4linux/$(basename \"$dev\")/index"),
"install script should inspect the kernel video index so it only links real capture nodes"
);
assert!(
SERVER_INSTALL.contains("GC_CAPTURE_PAIRS"),
"install script should assemble eye-link candidates from udev-tagged capture devices"
);
assert!(
SERVER_INSTALL.contains("discover_gc_capture_pairs"),
"install script should retry capture-node discovery instead of trusting a single early snapshot"
);
assert!(
SERVER_INSTALL.contains("for _ in {1..20}; do"),
"install script should wait briefly for both GC311 capture nodes to finish enumerating"
);
assert!(
SERVER_INSTALL.contains("borrowing relay GPIO power for capture discovery"),
"install script should temporarily power capture cards for discovery when relay GPIO is available"
);
assert!(
SERVER_INSTALL.contains("returning relay GPIO power control to Lesavka auto mode"),
"install script should restore relay power control after capture discovery"
);
assert!(
SERVER_INSTALL.contains("trap restore_capture_power_after_discovery EXIT"),
"install script should restore borrowed capture power even if discovery exits early"
);
assert!(
SERVER_INSTALL.contains("[ \"$LEFT_TAG\" = \"$RIGHT_TAG\" ]"),
"install script should refuse duplicated path tags instead of assigning both eyes to one card"
);
}