diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4bfb4a1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,48 @@ +# EditorConfig for LyreBirdAudio +# https://editorconfig.org + +root = true + +# Default settings for all files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +# Shell scripts +[*.sh] +indent_style = space +indent_size = 4 + +# BATS test files +[*.bats] +indent_style = space +indent_size = 4 + +# Markdown files +[*.md] +trim_trailing_whitespace = false +indent_size = 2 + +# YAML files +[*.{yml,yaml}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Makefile (requires tabs) +[Makefile] +indent_style = tab + +# Configuration files +[*.conf] +indent_size = 4 + +# Git config files +[.git*] +indent_size = 4 diff --git a/.github/workflows/bash-ci.yml b/.github/workflows/bash-ci.yml index a19ba50..7e27cb7 100644 --- a/.github/workflows/bash-ci.yml +++ b/.github/workflows/bash-ci.yml @@ -85,10 +85,10 @@ jobs: while IFS= read -r script; do if bash -n "$script" 2>&1; then - echo "✓ $script" + echo "[PASS] $script" success=$((success + 1)) else - echo "✗ $script" + echo "[FAIL] $script" errors=$((errors + 1)) fi done < <(find . -name '*.sh' -type f | sort) @@ -181,9 +181,9 @@ jobs: notes=$(shellcheck --format=json "$script" 2>/dev/null | jq '[.[] | select(.level == "info" or .level == "style")] | length' || echo 0) if [[ "$errors" == "0" && "$warnings" == "0" ]]; then - status="✓" + status="PASS" else - status="✗" + status="FAIL" fi echo "| ${status} ${script} | ${errors} | ${warnings} | ${notes} |" >> "$GITHUB_STEP_SUMMARY" @@ -226,7 +226,7 @@ jobs: # -d shows diff, -i 4 = 4 space indent, -bn = binary ops at start of line # -ci = indent case statements if diff=$(shfmt -d -i 4 -bn -ci "$script" 2>&1); then - echo "✓" + echo "[PASS]" else echo "needs formatting" echo "$diff" @@ -339,11 +339,11 @@ jobs: if [[ -n "$dangerous_eval" ]]; then echo "$dangerous_eval" - echo " ⚠ Found potentially dangerous eval - review for command injection" + echo " [WARN] Found potentially dangerous eval - review for command injection" warnings=$((warnings + 1)) warning_details+=("Dangerous eval patterns found") else - echo " ✓ No dangerous eval patterns (safe patterns like 'eval set --' are OK)" + echo " [OK] No dangerous eval patterns (safe patterns like 'eval set --' are OK)" fi echo "" @@ -356,11 +356,11 @@ jobs: if [[ -n "$insecure_dl" ]]; then echo "$insecure_dl" - echo " ⚠ Found downloads with disabled certificate verification" + echo " [WARN] Found downloads with disabled certificate verification" warnings=$((warnings + 1)) warning_details+=("Insecure download flags") else - echo " ✓ No insecure download flags found" + echo " [OK] No insecure download flags found" fi echo "" @@ -375,11 +375,11 @@ jobs: if [[ -n "$world_writable" ]]; then echo "$world_writable" - echo " ⚠ Found world-writable permissions" + echo " [WARN] Found world-writable permissions" warnings=$((warnings + 1)) warning_details+=("World-writable permissions") else - echo " ✓ No world-writable permissions found" + echo " [OK] No world-writable permissions found" fi echo "" @@ -396,11 +396,11 @@ jobs: if [[ -n "$unquoted_cmd" ]]; then echo "$unquoted_cmd" - echo " ⚠ Found unquoted command substitution in file operations" + echo " [WARN] Found unquoted command substitution in file operations" warnings=$((warnings + 1)) warning_details+=("Unquoted command substitution") else - echo " ✓ No unquoted command substitution risks found" + echo " [OK] No unquoted command substitution risks found" fi echo "" @@ -419,11 +419,11 @@ jobs: if [[ -n "$hardcoded" ]]; then echo "$hardcoded" - echo " ⚠ Found potential hardcoded secrets" + echo " [WARN] Found potential hardcoded secrets" warnings=$((warnings + 1)) warning_details+=("Hardcoded secrets") else - echo " ✓ No hardcoded secrets found" + echo " [OK] No hardcoded secrets found" fi echo "" @@ -441,11 +441,11 @@ jobs: if [[ -n "$insecure_tmp" ]]; then echo "$insecure_tmp" - echo " ⚠ Found predictable temp file names (consider using mktemp)" + echo " [WARN] Found predictable temp file names (consider using mktemp)" warnings=$((warnings + 1)) warning_details+=("Predictable temp files") else - echo " ✓ No insecure temp file patterns found" + echo " [OK] No insecure temp file patterns found" fi echo "" @@ -472,12 +472,12 @@ jobs: echo "" echo "| Check | Status |" echo "|-------|--------|" - echo "| Dangerous eval patterns | ✓ |" - echo "| Insecure downloads (-k/--insecure) | ✓ |" - echo "| World-writable permissions (777/666) | ✓ |" - echo "| Unquoted command substitutions | ✓ |" - echo "| Hardcoded secrets | ✓ |" - echo "| Predictable temp files | ✓ |" + echo "| Dangerous eval patterns | OK |" + echo "| Insecure downloads (-k/--insecure) | OK |" + echo "| World-writable permissions (777/666) | OK |" + echo "| Unquoted command substitutions | OK |" + echo "| Hardcoded secrets | OK |" + echo "| Predictable temp files | OK |" echo "" echo "_Note: This scan uses pattern matching and may have false positives/negatives._" echo "_ShellCheck provides more comprehensive variable quoting analysis._" @@ -496,7 +496,7 @@ jobs: run: | { # Header - echo "# 🔍 Bash CI Pipeline Results" + echo "# Bash CI Pipeline Results" echo "" echo "---" echo "" @@ -507,9 +507,9 @@ jobs: # Determine overall result if [[ "${{ needs.bash-syntax.result }}" == "success" && "${{ needs.shellcheck.result }}" == "success" ]]; then - echo "### ✅ All Required Checks Passed" + echo "### All Required Checks Passed" else - echo "### ❌ Required Checks Failed" + echo "### Required Checks Failed" fi echo "" @@ -518,21 +518,21 @@ jobs: echo "" echo "| Check | Type | Status | Description |" echo "|-------|------|--------|-------------|" - echo "| **Bash Syntax** | 🔴 Required | ${{ needs.bash-syntax.result == 'success' && '✅ Passed' || '❌ Failed' }} | Validates script syntax with \`bash -n\` |" - echo "| **ShellCheck** | 🔴 Required | ${{ needs.shellcheck.result == 'success' && '✅ Passed' || '❌ Failed' }} | Static analysis for bugs and issues |" - echo "| **shfmt** | 🟡 Advisory | ${{ needs.shfmt.result == 'success' && '✅ Passed' || '⚠️ Warnings' }} | Code formatting consistency |" - echo "| **bashate** | 🟡 Advisory | ${{ needs.bashate.result == 'success' && '✅ Passed' || '⚠️ Warnings' }} | OpenStack style guidelines |" - echo "| **Security** | 🟡 Advisory | ${{ needs.security-scan.result == 'success' && '✅ Passed' || '⚠️ Warnings' }} | Security anti-pattern detection |" + echo "| **Bash Syntax** | Required | ${{ needs.bash-syntax.result == 'success' && 'Passed' || 'Failed' }} | Validates script syntax with \`bash -n\` |" + echo "| **ShellCheck** | Required | ${{ needs.shellcheck.result == 'success' && 'Passed' || 'Failed' }} | Static analysis for bugs and issues |" + echo "| **shfmt** | Advisory | ${{ needs.shfmt.result == 'success' && 'Passed' || 'Warnings' }} | Code formatting consistency |" + echo "| **bashate** | Advisory | ${{ needs.bashate.result == 'success' && 'Passed' || 'Warnings' }} | OpenStack style guidelines |" + echo "| **Security** | Advisory | ${{ needs.security-scan.result == 'success' && 'Passed' || 'Warnings' }} | Security anti-pattern detection |" echo "" # Legend echo "
" - echo "📋 Check Type Legend" + echo "Check Type Legend" echo "" echo "| Type | Meaning |" echo "|------|---------|" - echo "| 🔴 Required | Must pass for PR to be merged |" - echo "| 🟡 Advisory | Informational only, does not block merge |" + echo "| Required | Must pass for PR to be merged |" + echo "| Advisory | Informational only, does not block merge |" echo "" echo "
" echo "" @@ -588,7 +588,7 @@ jobs: echo "" # Quick Fix Guide - echo "## 🛠️ Quick Fix Guide" + echo "## Quick Fix Guide" echo "" echo "
" echo "How to fix common issues" @@ -635,17 +635,17 @@ jobs: failed=0 if [[ "${{ needs.bash-syntax.result }}" != "success" ]]; then - echo "::error::❌ Bash syntax validation failed" + echo "::error::Bash syntax validation failed" failed=1 else - echo "✅ Bash syntax: passed" + echo "Bash syntax: passed" fi if [[ "${{ needs.shellcheck.result }}" != "success" ]]; then - echo "::error::❌ ShellCheck found errors or warnings" + echo "::error::ShellCheck found errors or warnings" failed=1 else - echo "✅ ShellCheck: passed" + echo "ShellCheck: passed" fi echo "" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75fe718..46f8972 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ brew install shellcheck bats-core ```bash # Check all scripts pass syntax validation for script in *.sh; do - bash -n "$script" && echo "✓ $script" + bash -n "$script" && echo "[OK] $script" done # Run ShellCheck on all scripts @@ -196,11 +196,57 @@ if ! command -v ffmpeg &>/dev/null; then fi ``` +#### Logging + +LyreBirdAudio provides standardized logging through `lyrebird-common.sh`. Both patterns are acceptable: + +```bash +# Wrapper functions (preferred for new code) +log_info "Starting service..." +log_warn "Configuration missing, using defaults" +log_error "Failed to connect" +log_debug "Variable value: ${value}" + +# Direct log calls (also acceptable) +log INFO "Starting service..." +log WARN "Configuration missing" +``` + +The logging functions write to both stderr and a log file when available. + +#### Variable Naming Prefixes + +Configuration variables follow these prefix conventions: + +- **`LYREBIRD_*`**: LyreBirdAudio-specific settings (alerts, storage, orchestrator) +- **`MEDIAMTX_*`**: MediaMTX-related settings (API, ports, paths) + +```bash +# LyreBirdAudio settings +LYREBIRD_ALERT_ENABLED=true +LYREBIRD_RECORDING_DIR=/var/lib/recordings + +# MediaMTX settings +MEDIAMTX_API_PORT=9997 +MEDIAMTX_CONFIG_DIR=/etc/mediamtx +``` + #### Comments - Comment complex logic, not obvious code - Use TODO for future work - Explain regex patterns and complex commands +- Use section headers to organize code: + +```bash +# ============================================================================ +# Section Name +# ============================================================================ + +# Inline comment for complex logic +``` + +Example: ```bash # Match USB device path format: /sys/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.3 diff --git a/README.md b/README.md index 3393f7c..b697fb5 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,7 @@ sudo ./lyrebird-diagnostics.sh quick # rtsp://your-ip:8554/device-name ``` -**Reboot Recommended:** After initial and after each individual USB device mapping, reboot for udev rules to take full effect. You will manually have to start the Orcestrator again after each reboot: cd LyreBirdAudio && sudo ./lyrebird-orchestrator.sh +**Reboot Recommended:** After initial and after each individual USB device mapping, reboot for udev rules to take full effect. You will manually have to start the Orchestrator again after each reboot: cd LyreBirdAudio && sudo ./lyrebird-orchestrator.sh ### Post-Installation @@ -1426,7 +1426,7 @@ This modular design prevents duplicate business logic and ensures maintainabilit |--------|---------|---------| | lyrebird-orchestrator.sh | 2.1.2 | Unified management interface | | lyrebird-updater.sh | 1.5.1 | Version management with rollback | -| mediamtx-stream-manager.sh | 1.4.2 | Stream lifecycle management | +| mediamtx-stream-manager.sh | 1.4.3 | Stream lifecycle management | | usb-audio-mapper.sh | 1.2.1 | USB device persistence via udev | | lyrebird-mic-check.sh | 1.0.0 | Hardware capability detection | | lyrebird-diagnostics.sh | 1.0.2 | System diagnostics | diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..76abbd8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,91 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | --------- | +| 2.x.x | Yes | +| 1.x.x | No | + +## Reporting a Vulnerability + +We take security seriously. If you discover a security vulnerability in LyreBirdAudio, please report it responsibly. + +### How to Report + +1. **Do NOT** open a public GitHub issue for security vulnerabilities +2. Email security concerns to the maintainer (see GitHub profile) +3. Include as much detail as possible: + - Description of the vulnerability + - Steps to reproduce + - Potential impact + - Suggested fix (if any) + +### What to Expect + +- **Acknowledgment**: Within 48 hours of your report +- **Initial Assessment**: Within 7 days +- **Resolution Timeline**: Depends on severity + - Critical: 24-48 hours + - High: 7 days + - Medium: 30 days + - Low: Next release + +### Security Best Practices + +When deploying LyreBirdAudio, follow these security recommendations: + +#### Network Security + +- Run MediaMTX behind a reverse proxy with TLS termination +- Restrict API access to localhost or trusted networks +- Use firewall rules to limit RTSP port exposure +- Consider VPN for remote stream access + +#### File System Security + +- Run scripts with minimal required privileges +- Avoid running as root when possible +- Use appropriate file permissions (640 for configs, 750 for scripts) +- Store recordings in a dedicated partition + +#### Configuration Security + +- Never commit webhook URLs or API keys to version control +- Use environment variables for sensitive configuration +- Rotate credentials regularly +- Monitor logs for unauthorized access attempts + +#### Webhook Security + +- Use HTTPS endpoints for webhook delivery +- Verify webhook signatures when possible +- Implement rate limiting +- Monitor for failed delivery attempts + +## Security Features + +LyreBirdAudio includes several security-conscious features: + +1. **SHA256 Verification**: All MediaMTX downloads are verified against checksums +2. **Secure Temp Files**: Uses `mktemp` for temporary file creation +3. **Input Sanitization**: RTSP paths and user inputs are validated +4. **No Hardcoded Credentials**: Configuration is environment-driven +5. **Atomic Operations**: File operations use atomic patterns where possible +6. **Signal Handling**: Graceful shutdown and cleanup on termination +7. **Path Validation**: Dangerous operations validate path safety + +## Disclosure Policy + +We follow a coordinated disclosure process: + +1. Reporter contacts maintainers privately +2. Issue is confirmed and assessed +3. Fix is developed and tested +4. Security advisory is prepared +5. Patch is released +6. Advisory is published after users have time to update + +## Acknowledgments + +We appreciate security researchers who help keep LyreBirdAudio secure. Responsible disclosures will be acknowledged in release notes (with permission). diff --git a/docs/ADR.md b/docs/ADR.md new file mode 100644 index 0000000..488deda --- /dev/null +++ b/docs/ADR.md @@ -0,0 +1,240 @@ +# Architecture Decision Records (ADR) + +This document records significant architectural decisions made during the development of LyreBirdAudio. + +## ADR-001: Bash as Primary Implementation Language + +**Date**: April 2025 +**Status**: Accepted + +### Context +We needed to choose an implementation language for LyreBirdAudio's automation and management scripts. + +### Decision +Use Bash (4.0+) as the primary implementation language for all scripts. + +### Rationale +- **Universal Availability**: Bash is pre-installed on virtually all Linux distributions +- **No Dependencies**: No additional runtime or package installation required +- **System Integration**: Native access to system commands, process management, and file operations +- **Sysadmin Familiarity**: Target users are comfortable reading and modifying Bash scripts +- **Transparency**: Easy to audit and understand what the scripts do + +### Consequences +- Limited to Linux/Unix systems (acceptable for target use case) +- More verbose than Python for complex logic +- Requires careful handling of edge cases (quoting, spaces in paths) +- Must enforce `set -euo pipefail` for safety + +--- + +## ADR-002: MediaMTX as RTSP Server + +**Date**: April 2025 +**Status**: Accepted + +### Context +We needed an RTSP server that could handle audio streams from USB microphones. + +### Decision +Use MediaMTX (formerly rtsp-simple-server) as the RTSP streaming server. + +### Rationale +- **Single Binary**: No complex installation or dependencies +- **Low Resource Usage**: Suitable for Raspberry Pi and similar devices +- **REST API**: Enables programmatic stream management +- **Active Development**: Regular updates and security patches +- **Multi-Protocol**: Supports RTSP, RTMP, HLS, WebRTC + +### Consequences +- Dependency on external project's release schedule +- Must handle API version changes +- Need to implement download verification (SHA256) + +--- + +## ADR-003: FFmpeg for Audio Transcoding + +**Date**: April 2025 +**Status**: Accepted + +### Context +Need to capture audio from ALSA devices and stream to MediaMTX. + +### Decision +Use FFmpeg as the audio capture and transcoding pipeline. + +### Rationale +- **ALSA Support**: Native Linux audio device support +- **Codec Flexibility**: Supports all common audio codecs +- **Stability**: Battle-tested in production environments +- **Configurability**: Extensive options for quality tuning + +### Consequences +- FFmpeg must be installed (usually available in package managers) +- FFmpeg process management complexity +- Need to handle FFmpeg crashes and restarts + +--- + +## ADR-004: Shared Library Pattern (lyrebird-common.sh) + +**Date**: April 2025 +**Status**: Accepted + +### Context +Multiple scripts needed common functionality (logging, colors, error handling). + +### Decision +Create a shared library (`lyrebird-common.sh`) that scripts can source. + +### Rationale +- **DRY Principle**: Avoid duplicating utility functions +- **Consistency**: Uniform logging format and error handling +- **Backward Compatibility**: Scripts can define functions before sourcing (override pattern) +- **Optional**: Scripts work without the library (fallback definitions) + +### Consequences +- Must maintain backward compatibility +- Order of sourcing matters +- Need to guard against multiple inclusion + +--- + +## ADR-005: Atomic File Operations + +**Date**: April 2025 +**Status**: Accepted + +### Context +Scripts modify configuration files and state files that could be read by other processes. + +### Decision +Use atomic file operations: write to `.tmp` file, then `mv` to final location. + +### Rationale +- **No Partial Reads**: Readers never see half-written files +- **Crash Safety**: Original file preserved if write fails +- **POSIX Guarantee**: `mv` on same filesystem is atomic + +### Consequences +- Slightly more complex code +- Need to clean up stale `.tmp` files +- Requires write access to target directory + +--- + +## ADR-006: USB Device Persistence via udev + +**Date**: April 2025 +**Status**: Accepted + +### Context +USB audio devices have non-deterministic device names (`/dev/snd/...`) that change on reboot. + +### Decision +Use udev rules to create persistent symlinks based on USB topology (bus/port path). + +### Rationale +- **Kernel Integration**: udev is the standard Linux device manager +- **Persistence**: Symlinks survive reboots and device reconnection +- **No Daemon**: Rules are processed by udev automatically + +### Consequences +- Requires root to install udev rules +- May need reboot for rules to take effect +- USB hub changes require remapping + +--- + +## ADR-007: Webhook-Based Alerting + +**Date**: April 2025 +**Status**: Accepted + +### Context +Need to notify users of stream failures and system issues. + +### Decision +Implement webhook-based alerting with support for Discord, Slack, Pushover, and generic endpoints. + +### Rationale +- **Flexibility**: Users choose their preferred notification platform +- **No Infrastructure**: No email server or SMS gateway required +- **Extensibility**: Easy to add new webhook formats +- **Modern**: Integrates with existing monitoring stacks + +### Consequences +- Requires network connectivity for alerts +- Need to implement rate limiting +- Must handle webhook delivery failures + +--- + +## ADR-008: Prometheus Metrics Format + +**Date**: April 2025 +**Status**: Accepted + +### Context +Need to expose operational metrics for monitoring dashboards. + +### Decision +Export metrics in Prometheus/OpenMetrics text format. + +### Rationale +- **Industry Standard**: Prometheus is widely adopted +- **Simple Format**: Plain text, easy to debug +- **Ecosystem**: Works with Grafana, AlertManager, etc. +- **Pull Model**: No push infrastructure required + +### Consequences +- Must maintain metric naming conventions +- Need HTTP server or textfile collector integration +- Metric cardinality must be controlled + +--- + +## ADR-009: Lockfile-Based Concurrency Control + +**Date**: April 2025 +**Status**: Accepted + +### Context +Multiple script invocations could interfere with each other. + +### Decision +Use lockfiles with timeout and stale lock detection. + +### Rationale +- **Simple**: Well-understood mechanism +- **Debuggable**: Can inspect lock state +- **Atomic**: Uses `flock` for atomic acquisition + +### Consequences +- Lockfiles can become stale on crashes +- Need to implement timeout and cleanup +- Path to lockfile must be consistent + +--- + +## ADR-010: Signal Handler Pattern + +**Date**: April 2025 +**Status**: Accepted + +### Context +Scripts must clean up resources (processes, temp files) on termination. + +### Decision +Use `trap` to register cleanup functions for EXIT, INT, and TERM signals. + +### Rationale +- **Reliable Cleanup**: Runs regardless of exit cause +- **Process Management**: Can kill child processes +- **State Reset**: Can remove PID files and locks + +### Consequences +- Cleanup must be idempotent (may run multiple times) +- Signal handlers should be simple (avoid complex logic) +- Exit code must be preserved diff --git a/install_mediamtx.sh b/install_mediamtx.sh index b92a92c..9251c54 100644 --- a/install_mediamtx.sh +++ b/install_mediamtx.sh @@ -1308,7 +1308,7 @@ update_mediamtx() { if new_version=$("${INSTALL_DIR}/mediamtx" --version 2>&1 | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | head -1); then if [[ "${new_version}" != "${current_version}" ]]; then upgrade_success=true - log_info "Built-in upgrade completed successfully (${current_version} → ${new_version})" + log_info "Built-in upgrade completed successfully (${current_version} -> ${new_version})" else log_warn "Built-in upgrade reported success but version unchanged" fi @@ -1540,7 +1540,7 @@ show_status() { # Installation if [[ -f "${INSTALL_DIR}/mediamtx" ]]; then - echo -e "Installation: ${GREEN}✓ Installed${NC}" + echo -e "Installation: ${GREEN}[OK] Installed${NC}" echo "Location: ${INSTALL_DIR}/mediamtx" local version="unknown" @@ -1549,16 +1549,16 @@ show_status() { fi echo "Version: ${version}" else - echo -e "Installation: ${RED}✗ Not installed${NC}" + echo -e "Installation: ${RED}[--] Not installed${NC}" fi # Configuration echo "" if [[ -f "${CONFIG_DIR}/${CONFIG_NAME}" ]]; then - echo -e "Configuration: ${GREEN}✓ Present${NC}" + echo -e "Configuration: ${GREEN}[OK] Present${NC}" echo "Config file: ${CONFIG_DIR}/${CONFIG_NAME}" else - echo -e "Configuration: ${YELLOW}⚠ Missing${NC}" + echo -e "Configuration: ${YELLOW}[!] Missing${NC}" fi # Service and process status @@ -1567,23 +1567,23 @@ show_status() { management_mode=$(detect_management_mode) if command -v systemctl &>/dev/null && [[ -f "${SERVICE_DIR}/${SERVICE_NAME}" ]]; then - echo -e "Service: ${GREEN}✓ Configured${NC}" + echo -e "Service: ${GREEN}[OK] Configured${NC}" else - echo -e "Service: ${YELLOW}⚠ Not configured${NC}" + echo -e "Service: ${YELLOW}[!] Not configured${NC}" fi case "${management_mode}" in systemd) - echo -e "Status: ${GREEN}● Running (systemd)${NC}" + echo -e "Status: ${GREEN}[OK] Running (systemd)${NC}" ;; stream-manager) - echo -e "Status: ${YELLOW}⚠ Running (stream manager)${NC}" + echo -e "Status: ${YELLOW}[!] Running (stream manager)${NC}" ;; manual) - echo -e "Status: ${YELLOW}⚠ Running (manual)${NC}" + echo -e "Status: ${YELLOW}[!] Running (manual)${NC}" ;; none) - echo -e "Status: ${RED}○ Not running${NC}" + echo -e "Status: ${RED}[--] Not running${NC}" ;; esac @@ -1613,17 +1613,17 @@ show_status() { if [[ -n "${port_user}" ]]; then if [[ "${port_user}" == "mediamtx" ]]; then - echo -e " ${label} (${port}): ${GREEN}✓ In use by MediaMTX${NC}" + echo -e " ${label} (${port}): ${GREEN}[OK] In use by MediaMTX${NC}" else - echo -e " ${label} (${port}): ${GREEN}✓ In use by ${port_user}${NC}" + echo -e " ${label} (${port}): ${GREEN}[OK] In use by ${port_user}${NC}" fi elif [[ -n "${port_status}" ]]; then - echo -e " ${label} (${port}): ${GREEN}✓ In use${NC}" + echo -e " ${label} (${port}): ${GREEN}[OK] In use${NC}" else if [[ "${management_mode}" == "stream-manager" ]] || [[ "${management_mode}" == "systemd" ]]; then - echo -e " ${label} (${port}): ${YELLOW}⚠ Status unknown${NC}" + echo -e " ${label} (${port}): ${YELLOW}[?] Status unknown${NC}" else - echo -e " ${label} (${port}): ${YELLOW}○ Not in use${NC}" + echo -e " ${label} (${port}): ${YELLOW}[--] Not in use${NC}" fi fi done @@ -1678,7 +1678,7 @@ verify_installation() { echo "" if [[ ${errors} -eq 0 ]] && [[ ${warnings} -eq 0 ]]; then - log_info "✓ Installation verified successfully" + log_info "Installation verified successfully" return 0 elif [[ ${errors} -eq 0 ]]; then log_warn "Installation verified with ${warnings} warning(s)" diff --git a/lyrebird-alerts.sh b/lyrebird-alerts.sh index ea37ece..d6aab8a 100755 --- a/lyrebird-alerts.sh +++ b/lyrebird-alerts.sh @@ -109,6 +109,13 @@ LYREBIRD_ALERT_RETRIES="${LYREBIRD_ALERT_RETRIES:-3}" LYREBIRD_HOSTNAME="${LYREBIRD_HOSTNAME:-$(hostname -s 2>/dev/null || echo 'unknown')}" LYREBIRD_LOCATION="${LYREBIRD_LOCATION:-}" +# Validate timeout bounds (5-120 seconds) +if [[ "$LYREBIRD_ALERT_TIMEOUT" -lt 5 ]]; then + LYREBIRD_ALERT_TIMEOUT=5 +elif [[ "$LYREBIRD_ALERT_TIMEOUT" -gt 120 ]]; then + LYREBIRD_ALERT_TIMEOUT=120 +fi + # Additional webhook URLs (space-separated) LYREBIRD_WEBHOOK_URLS="${LYREBIRD_WEBHOOK_URLS:-}" @@ -124,7 +131,8 @@ LYREBIRD_NTFY_SERVER="${LYREBIRD_NTFY_SERVER:-https://ntfy.sh}" # Alert Levels #============================================================================= -# shellcheck disable=SC2034 # These constants are exported for use by other scripts +# Alert level constants - SC2034: These are exported for use by sourcing scripts +# shellcheck disable=SC2034 readonly ALERT_LEVEL_INFO="info" # shellcheck disable=SC2034 readonly ALERT_LEVEL_WARNING="warning" @@ -141,14 +149,20 @@ declare -A ALERT_COLORS=( [critical]=15548997 # Red ) -# Emoji for each level -declare -A ALERT_EMOJI=( - [info]="ℹ️" - [warning]="⚠️" - [error]="❌" - [critical]="🚨" +# Text prefixes for each alert level +declare -A ALERT_PREFIX=( + [info]="[INFO]" + [warning]="[WARN]" + [error]="[ERROR]" + [critical]="[CRITICAL]" ) +# Get alert prefix for level +get_alert_prefix() { + local level="$1" + echo "${ALERT_PREFIX[$level]:-[INFO]}" +} + #============================================================================= # Alert Types (for deduplication keys) #============================================================================= @@ -173,6 +187,37 @@ readonly ALERT_TYPE_CUSTOM="custom" # Helper Functions #============================================================================= +# URL-encode a string (handles all special characters) +url_encode() { + local string="$1" + # Use jq if available, otherwise fall back to printf-based encoding + if command -v jq &>/dev/null; then + printf '%s' "$string" | jq -sRr @uri + else + # Fallback: encode using printf with hex conversion + local length="${#string}" + local i char + for ((i = 0; i < length; i++)); do + char="${string:i:1}" + case "$char" in + [a-zA-Z0-9.~_-]) printf '%s' "$char" ;; + *) printf '%%%02X' "'$char" ;; + esac + done + fi +} + +# Mask sensitive URLs for safe debug output (shows first 30 chars + ***) +mask_url() { + local url="$1" + local visible_len=30 + if [[ ${#url} -gt $visible_len ]]; then + echo "${url:0:$visible_len}***" + else + echo "$url" + fi +} + # Load configuration file if it exists load_config() { if [[ -f "$ALERT_CONFIG_FILE" ]]; then @@ -306,7 +351,8 @@ format_discord() { local message="$3" local alert_type="$4" local color="${ALERT_COLORS[$level]:-3447003}" - local emoji="${ALERT_EMOJI[$level]:-ℹ️}" + local emoji + emoji=$(get_alert_prefix "$level") local timestamp timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" @@ -341,7 +387,8 @@ format_slack() { local title="$2" local message="$3" local alert_type="$4" - local emoji="${ALERT_EMOJI[$level]:-ℹ️}" + local emoji + emoji=$(get_alert_prefix "$level") # Escape special characters for JSON message="${message//\\/\\\\}" @@ -416,12 +463,13 @@ format_pushover() { *) priority="0" ;; esac - # URL-encode message (simple version) - message="${message// /%20}" - title="${title// /%20}" + # Properly URL-encode message and title (handles &, =, and all special chars) + local encoded_message encoded_title + encoded_message=$(url_encode "$message") + encoded_title=$(url_encode "$title") # Return form-encoded data - echo "token=${LYREBIRD_PUSHOVER_TOKEN}&user=${LYREBIRD_PUSHOVER_USER}&title=${title}&message=${message}&priority=${priority}" + echo "token=${LYREBIRD_PUSHOVER_TOKEN}&user=${LYREBIRD_PUSHOVER_USER}&title=${encoded_title}&message=${encoded_message}&priority=${priority}" } #============================================================================= @@ -439,7 +487,7 @@ send_webhook() { while ((attempt < retries)); do ((attempt++)) - log_debug "Sending webhook (attempt ${attempt}/${retries}): ${webhook_url}" + log_debug "Sending webhook (attempt ${attempt}/${retries}): $(mask_url "$webhook_url")" local http_code local curl_args=(-s -w '%{http_code}' -o /dev/null --connect-timeout "$timeout" --max-time "$((timeout * 2))") @@ -488,9 +536,9 @@ send_webhook() { return 0 fi - # Retry with backoff + # Retry with backoff and jitter (prevents thundering herd) if ((attempt < retries)); then - local delay=$((attempt * 2)) + local delay=$(( (attempt * 2) + (RANDOM % 3) )) log_debug "Webhook failed, retrying in ${delay}s..." sleep "$delay" fi @@ -591,6 +639,8 @@ send_alert() { if $any_success; then # Update rate limit only on success update_rate_limit "$alert_hash" + # Clean up old state files periodically + cleanup_state return 0 else return 3 @@ -980,8 +1030,7 @@ cmd_send() { cmd_test() { echo "Sending test alert..." - # Temporarily enable if disabled - local was_enabled="${LYREBIRD_ALERT_ENABLED}" + # Force enable for test (no need to save/restore - script exits after test) LYREBIRD_ALERT_ENABLED="true" if send_alert "info" "Test Alert from LyreBirdAudio" \ @@ -993,8 +1042,6 @@ cmd_test() { echo "Failed to send test alert. Check your configuration." return 1 fi - - LYREBIRD_ALERT_ENABLED="$was_enabled" } #============================================================================= diff --git a/lyrebird-common.sh b/lyrebird-common.sh index 454cc2b..4d64ef2 100755 --- a/lyrebird-common.sh +++ b/lyrebird-common.sh @@ -680,9 +680,9 @@ if ! declare -f lyrebird_with_spinner &>/dev/null; then "$@" || exit_code=$? if ((exit_code == 0)); then - lyrebird_spinner_stop "✓ ${message} - Done" + lyrebird_spinner_stop "[OK] ${message} - Done" else - lyrebird_spinner_stop "✗ ${message} - Failed" + lyrebird_spinner_stop "[FAIL] ${message} - Failed" fi return "$exit_code" diff --git a/lyrebird-metrics.sh b/lyrebird-metrics.sh index 73b891b..a0f9c84 100755 --- a/lyrebird-metrics.sh +++ b/lyrebird-metrics.sh @@ -31,7 +31,7 @@ readonly SCRIPT_NAME # Configuration readonly MEDIAMTX_API_HOST="${MEDIAMTX_HOST:-localhost}" readonly MEDIAMTX_API_PORT="${MEDIAMTX_API_PORT:-9997}" -# shellcheck disable=SC2034 # Used by external scripts or for future features +# shellcheck disable=SC2034 # Exported for use by install_mediamtx.sh and external scripts readonly MEDIAMTX_RTSP_PORT="${MEDIAMTX_PORT:-8554}" readonly HEARTBEAT_FILE="${HEARTBEAT_FILE:-/run/mediamtx-audio.heartbeat}" readonly PID_FILE="${PID_FILE:-/run/mediamtx-audio.pid}" @@ -133,6 +133,8 @@ collect_mediamtx_metrics() { if [[ -n "$start_time_ticks" ]] && [[ -f "/proc/uptime" ]]; then local clk_tck clk_tck=$(getconf CLK_TCK 2>/dev/null || echo 100) + # Guard against division by zero + [[ "$clk_tck" -eq 0 ]] && clk_tck=100 local uptime_sec uptime_sec=$(awk '{print int($1)}' /proc/uptime) local proc_age=$((uptime_sec - (start_time_ticks / clk_tck))) @@ -265,6 +267,25 @@ collect_system_metrics() { fi } +# Helper function for API calls with retry for transient network failures +# Usage: api_call_with_retry [timeout] [retries] +api_call_with_retry() { + local url="$1" + local timeout="${2:-5}" + local retries="${3:-2}" + local attempt=0 + local result="" + + while ((attempt < retries)); do + ((attempt++)) + result=$(curl -s --connect-timeout "$timeout" "$url" 2>/dev/null) && break + # Brief pause before retry + ((attempt < retries)) && sleep 1 + done + + echo "$result" +} + # Collect MediaMTX API metrics (if available) # Enhanced in v1.1.0 with full MediaMTX v1.15.5 API coverage collect_api_metrics() { @@ -274,10 +295,10 @@ collect_api_metrics() { local api_url="http://${MEDIAMTX_API_HOST}:${MEDIAMTX_API_PORT}" - # Check API availability and get instance info + # Check API availability and get instance info (with retry for transient failures) local api_up=0 local info_json - info_json=$(curl -s --connect-timeout 2 "${api_url}/v3/info" 2>/dev/null) + info_json=$(api_call_with_retry "${api_url}/v3/info" 2 2) if [[ -n "$info_json" ]] && echo "$info_json" | grep -q '"version"'; then api_up=1 fi @@ -484,6 +505,19 @@ generate_all_metrics() { # Simple HTTP server using netcat (for basic metrics serving) serve_metrics() { local port="${1:-9100}" + local nc_pid="" + + # Cleanup function to kill any lingering nc processes + cleanup_server() { + log "Shutting down metrics server..." + [[ -n "$nc_pid" ]] && kill "$nc_pid" 2>/dev/null || true + # Kill any orphaned nc processes on our port + pkill -f "nc.*-l.*$port" 2>/dev/null || true + exit 0 + } + + # Set up signal handlers for graceful shutdown + trap cleanup_server EXIT INT TERM if ! has_command nc && ! has_command netcat; then log "ERROR: nc (netcat) required for --serve mode" diff --git a/lyrebird-orchestrator.sh b/lyrebird-orchestrator.sh index 696725e..998d871 100644 --- a/lyrebird-orchestrator.sh +++ b/lyrebird-orchestrator.sh @@ -252,11 +252,11 @@ log() { # Output functions with consistent formatting success() { - echo -e "${GREEN}✓${NC} $*" + echo -e "${GREEN}[OK]${NC} $*" } error() { - echo -e "${RED}✗${NC} $*" + echo -e "${RED}[ERROR]${NC} $*" } warning() { @@ -264,7 +264,7 @@ warning() { } info() { - echo -e "${CYAN}→${NC} $*" + echo -e "${CYAN}[INFO]${NC} $*" } pause() { diff --git a/lyrebird-storage.sh b/lyrebird-storage.sh index 6745bad..e71454d 100755 --- a/lyrebird-storage.sh +++ b/lyrebird-storage.sh @@ -38,21 +38,48 @@ readonly SCRIPT_NAME readonly RECORDING_DIR="${LYREBIRD_RECORDING_DIR:-/var/lib/mediamtx-ffmpeg/recordings}" readonly LOG_DIR="${LYREBIRD_LOG_DIR:-/var/log/lyrebird}" readonly MEDIAMTX_LOG="${MEDIAMTX_LOG:-/var/log/mediamtx.out}" +readonly MEDIAMTX_LOG_DIR="${MEDIAMTX_LOG%/*}" # Directory containing MediaMTX log readonly TEMP_DIR="${LYREBIRD_TEMP_DIR:-/tmp}" readonly BUFFER_DIR="${LYREBIRD_BUFFER_DIR:-/dev/shm/lyrebird-buffer}" -# Retention policies (in days) -readonly RECORDING_RETENTION_DAYS="${RECORDING_RETENTION_DAYS:-30}" -readonly LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-7}" -readonly TEMP_RETENTION_HOURS="${TEMP_RETENTION_HOURS:-24}" - -# Disk thresholds (percentage) -readonly DISK_WARNING_PERCENT="${DISK_WARNING_PERCENT:-80}" -readonly DISK_CRITICAL_PERCENT="${DISK_CRITICAL_PERCENT:-90}" -readonly DISK_EMERGENCY_PERCENT="${DISK_EMERGENCY_PERCENT:-95}" - -# Minimum free space (MB) -readonly MIN_FREE_SPACE_MB="${MIN_FREE_SPACE_MB:-500}" +# Retention policies (in days) - must be positive integers +_rec_ret=${RECORDING_RETENTION_DAYS:-30} +_log_ret=${LOG_RETENTION_DAYS:-7} +_tmp_ret=${TEMP_RETENTION_HOURS:-24} + +# Ensure retention values are positive (minimum 1) +(( _rec_ret < 1 )) && _rec_ret=1 +(( _log_ret < 1 )) && _log_ret=1 +(( _tmp_ret < 1 )) && _tmp_ret=1 + +readonly RECORDING_RETENTION_DAYS="$_rec_ret" +readonly LOG_RETENTION_DAYS="$_log_ret" +readonly TEMP_RETENTION_HOURS="$_tmp_ret" +unset _rec_ret _log_ret _tmp_ret + +# Disk thresholds (percentage) - validate bounds 0-100 +_disk_warning=${DISK_WARNING_PERCENT:-80} +_disk_critical=${DISK_CRITICAL_PERCENT:-90} +_disk_emergency=${DISK_EMERGENCY_PERCENT:-95} + +# Clamp percentage values to valid range +(( _disk_warning < 0 )) && _disk_warning=0 +(( _disk_warning > 100 )) && _disk_warning=100 +(( _disk_critical < 0 )) && _disk_critical=0 +(( _disk_critical > 100 )) && _disk_critical=100 +(( _disk_emergency < 0 )) && _disk_emergency=0 +(( _disk_emergency > 100 )) && _disk_emergency=100 + +readonly DISK_WARNING_PERCENT="$_disk_warning" +readonly DISK_CRITICAL_PERCENT="$_disk_critical" +readonly DISK_EMERGENCY_PERCENT="$_disk_emergency" +unset _disk_warning _disk_critical _disk_emergency + +# Minimum free space (MB) - must be positive +_min_free=${MIN_FREE_SPACE_MB:-500} +(( _min_free < 0 )) && _min_free=0 +readonly MIN_FREE_SPACE_MB="$_min_free" +unset _min_free # Log file size limits (bytes) readonly MAX_LOG_SIZE="${MAX_LOG_SIZE:-104857600}" # 100MB @@ -60,6 +87,36 @@ readonly MAX_LOG_SIZE="${MAX_LOG_SIZE:-104857600}" # 100MB # Dry run mode (set to true to see what would be deleted) DRY_RUN="${DRY_RUN:-false}" +# ============================================================================ +# Safety Validation +# ============================================================================ + +# Validate that a path is under allowed parent directories for deletion +# This prevents accidental deletion of system files if misconfigured +validate_safe_path() { + local path="$1" + local allowed_parents=("/dev/shm" "/tmp" "/var/tmp" "/run") + + # Resolve to absolute path + local resolved_path + resolved_path=$(realpath -m "$path" 2>/dev/null) || resolved_path="$path" + + for parent in "${allowed_parents[@]}"; do + if [[ "$resolved_path" == "$parent"* ]]; then + return 0 + fi + done + + return 1 +} + +# Validate BUFFER_DIR at startup +if [[ -n "${BUFFER_DIR:-}" ]] && ! validate_safe_path "$BUFFER_DIR"; then + echo "ERROR: BUFFER_DIR '$BUFFER_DIR' is not under allowed directories (/dev/shm, /tmp, /var/tmp, /run)" >&2 + echo "This is a safety check to prevent accidental deletion of system files." >&2 + exit 1 +fi + # ============================================================================ # Helper Functions # ============================================================================ @@ -75,6 +132,22 @@ log_warn() { log WARN "$@"; } log_error() { log ERROR "$@"; } log_debug() { [[ "${DEBUG:-false}" == "true" ]] && log DEBUG "$@" || true; } +# ============================================================================ +# Signal Handling +# ============================================================================ + +# Cleanup function for graceful shutdown +_storage_cleanup() { + local exit_code=$? + log_debug "Storage script cleanup triggered (exit code: $exit_code)" + # Remove any temporary files we may have created + rm -f -- /tmp/lyrebird-storage-*.tmp 2>/dev/null || true + exit "$exit_code" +} + +# Set up signal handlers for graceful shutdown +trap _storage_cleanup EXIT INT TERM + # Format bytes to human readable format_bytes() { local bytes="$1" @@ -127,7 +200,7 @@ safe_delete() { log_info "[DRY RUN] Would delete ${desc}: $path" else log_debug "Deleting ${desc}: $path" - rm -rf "$path" + rm -rf -- "$path" fi } @@ -181,14 +254,14 @@ cleanup_logs() { fi # Clean rotated MediaMTX logs - if [[ -d "$(dirname "$MEDIAMTX_LOG")" ]]; then + if [[ -d "$MEDIAMTX_LOG_DIR" ]]; then while IFS= read -r -d '' file; do local size size=$(stat -c%s "$file" 2>/dev/null || echo 0) safe_delete "$file" "mediamtx log" ((++count)) ((freed_bytes += size)) - done < <(find "$(dirname "$MEDIAMTX_LOG")" -type f -name "mediamtx*.out.*" -mtime "+${LOG_RETENTION_DAYS}" -print0 2>/dev/null) + done < <(find "$MEDIAMTX_LOG_DIR" -type f -name "mediamtx*.out.*" -mtime "+${LOG_RETENTION_DAYS}" -print0 2>/dev/null) fi log_info "Cleaned $count log file(s), freed $(format_bytes $freed_bytes)" @@ -228,6 +301,10 @@ cleanup_temp() { truncate_large_logs() { log_info "Checking for oversized log files" + # Clean up any stale .tmp files from interrupted previous runs + find "$MEDIAMTX_LOG_DIR" -name "*.tmp" -mmin +5 -delete 2>/dev/null || true + [[ -d "$LOG_DIR" ]] && find "$LOG_DIR" -name "*.tmp" -mmin +5 -delete 2>/dev/null || true + # Check MediaMTX log if [[ -f "$MEDIAMTX_LOG" ]]; then local size @@ -282,13 +359,15 @@ emergency_cleanup() { # Delete all rotated logs find "$LOG_DIR" -name "*.gz" -delete 2>/dev/null || true - find "$(dirname "$MEDIAMTX_LOG")" -name "*.gz" -delete 2>/dev/null || true + find "$MEDIAMTX_LOG_DIR" -name "*.gz" -delete 2>/dev/null || true # Clear temp directory find "$TEMP_DIR" -maxdepth 1 -name "lyrebird-*" -type f -delete 2>/dev/null || true - # Clear buffer directory - [[ -d "$BUFFER_DIR" ]] && rm -rf "${BUFFER_DIR:?}"/* 2>/dev/null || true + # Clear buffer directory (with safety validation) + if [[ -d "$BUFFER_DIR" ]] && validate_safe_path "$BUFFER_DIR"; then + rm -rf "${BUFFER_DIR:?}"/* 2>/dev/null || true + fi } # ============================================================================ diff --git a/tests/test_integration.bats b/tests/test_integration.bats new file mode 100644 index 0000000..6622d1a --- /dev/null +++ b/tests/test_integration.bats @@ -0,0 +1,214 @@ +#!/usr/bin/env bats +# test_integration.bats - Integration tests for LyreBirdAudio +# Part of LyreBirdAudio - RTSP Audio Streaming Suite +# +# These tests verify end-to-end functionality with mock devices and services. +# They require a more complete test environment than unit tests. +# +# Prerequisites: +# - Mock audio device available (or virtual audio device) +# - MediaMTX not running (tests manage their own instance) +# - Write access to /tmp +# +# Run with: bats tests/test_integration.bats + +# ============================================================================ +# Test Setup and Teardown +# ============================================================================ + +setup() { + TEST_DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" && pwd )" + PROJECT_ROOT="$( cd "$TEST_DIR/.." && pwd )" + + # Source common library if available + if [[ -f "$PROJECT_ROOT/lyrebird-common.sh" ]]; then + source "$PROJECT_ROOT/lyrebird-common.sh" + fi + + # Create isolated test environment + export TEST_TMP=$(mktemp -d) + export TEST_CONFIG_DIR="$TEST_TMP/config" + export TEST_STATE_DIR="$TEST_TMP/state" + export TEST_LOG_DIR="$TEST_TMP/logs" + + mkdir -p "$TEST_CONFIG_DIR" "$TEST_STATE_DIR" "$TEST_LOG_DIR" + + # Mock device configuration + export MOCK_DEVICE_NAME="test-audio-device" + export MOCK_DEVICE_PATH="/dev/null" # Safe placeholder +} + +teardown() { + # Clean up test environment + rm -rf "$TEST_TMP" 2>/dev/null || true + + # Ensure no orphaned processes from tests + pkill -f "test-mediamtx" 2>/dev/null || true +} + +# ============================================================================ +# Stream Lifecycle Tests +# ============================================================================ + +@test "INTEGRATION: stream manager starts with valid config" { + skip "Integration test - requires mock audio device" + + # Create minimal test configuration + cat > "$TEST_CONFIG_DIR/audio-devices.conf" << 'EOF' +# Test device configuration +DEVICE_test_device="/dev/null" +EOF + + # Start stream manager in test mode + run timeout 5 "$PROJECT_ROOT/mediamtx-stream-manager.sh" --config-dir "$TEST_CONFIG_DIR" status + + # Verify it runs without crashing + [[ "$status" -eq 0 ]] || [[ "$status" -eq 1 ]] # 0=running, 1=not running (both valid) +} + +@test "INTEGRATION: stream manager handles missing device gracefully" { + skip "Integration test - requires mock audio device" + + # Create config with non-existent device + cat > "$TEST_CONFIG_DIR/audio-devices.conf" << 'EOF' +DEVICE_nonexistent="/dev/nonexistent-device-12345" +EOF + + run "$PROJECT_ROOT/mediamtx-stream-manager.sh" --config-dir "$TEST_CONFIG_DIR" start + + # Should fail gracefully, not crash + [[ "$status" -ne 0 ]] + [[ "$output" =~ "not found" ]] || [[ "$output" =~ "error" ]] +} + +# ============================================================================ +# USB Hot-Plug Simulation Tests +# ============================================================================ + +@test "INTEGRATION: USB mapper detects simulated device addition" { + skip "Integration test - requires udev simulation" + + # This would require: + # 1. Creating a mock udev event + # 2. Triggering the USB mapper + # 3. Verifying the device was detected + + # Placeholder for future implementation + [[ true ]] +} + +@test "INTEGRATION: USB mapper handles device removal" { + skip "Integration test - requires udev simulation" + + # Placeholder for device removal test + [[ true ]] +} + +# ============================================================================ +# API Interaction Tests +# ============================================================================ + +@test "INTEGRATION: metrics collector connects to MediaMTX API" { + skip "Integration test - requires running MediaMTX instance" + + # Would need to: + # 1. Start a mock MediaMTX API server + # 2. Run metrics collection + # 3. Verify metrics were collected + + run "$PROJECT_ROOT/lyrebird-metrics.sh" --once + + # Should produce some output even if MediaMTX is not running + [[ -n "$output" ]] +} + +@test "INTEGRATION: alerts send to mock webhook endpoint" { + skip "Integration test - requires mock webhook server" + + # Would need to: + # 1. Start a mock HTTP server + # 2. Configure alerts to point to it + # 3. Trigger an alert + # 4. Verify the mock received the request + + [[ true ]] +} + +# ============================================================================ +# Error Recovery Tests +# ============================================================================ + +@test "INTEGRATION: orchestrator recovers from stream failure" { + skip "Integration test - requires running services" + + # This would test: + # 1. Starting a stream + # 2. Simulating stream failure + # 3. Verifying automatic recovery + + [[ true ]] +} + +@test "INTEGRATION: storage manager handles disk full condition" { + skip "Integration test - requires disk simulation" + + # Would need to: + # 1. Create a small test filesystem + # 2. Fill it up + # 3. Verify cleanup runs correctly + + [[ true ]] +} + +# ============================================================================ +# End-to-End Workflow Tests +# ============================================================================ + +@test "INTEGRATION: full installation workflow" { + skip "Integration test - requires root and network" + + # Would test the complete installation process: + # 1. Pre-flight checks + # 2. MediaMTX download/install + # 3. Configuration setup + # 4. Service creation + + [[ true ]] +} + +@test "INTEGRATION: update workflow preserves configuration" { + skip "Integration test - requires installed instance" + + # Would test: + # 1. Creating custom configuration + # 2. Running update + # 3. Verifying config preserved + + [[ true ]] +} + +# ============================================================================ +# Performance Tests +# ============================================================================ + +@test "INTEGRATION: stream startup time under threshold" { + skip "Integration test - requires performance testing setup" + + # Would measure: + # - Time to start first stream + # - Time to become ready + # - Memory usage + + [[ true ]] +} + +@test "INTEGRATION: concurrent stream handling" { + skip "Integration test - requires multiple mock devices" + + # Would test: + # - Starting multiple streams simultaneously + # - Resource allocation + # - No race conditions + + [[ true ]] +} diff --git a/usb-audio-mapper.sh b/usb-audio-mapper.sh index 659c388..8c352ff 100644 --- a/usb-audio-mapper.sh +++ b/usb-audio-mapper.sh @@ -17,8 +17,8 @@ # - Enhanced error handling and validation # - Added comprehensive exception handling -# Set bash pipefail for better error handling -set -o pipefail +# Set strict mode for better error handling +set -euo pipefail # Source shared library if available (backward compatible) # Provides: colors, logging, command_exists, compute_hash, exit codes