diff --git a/logging_utils.sh b/logging_utils.sh new file mode 100644 index 0000000..d15885d --- /dev/null +++ b/logging_utils.sh @@ -0,0 +1,154 @@ +#!/bin/bash +# Shared logging utilities for kagglelink scripts +# +# This library provides consistent logging functions with emojis, +# timestamps, and error categorization for all kagglelink scripts. +# +# Usage: +# source logging_utils.sh +# log_info "Starting operation..." +# log_success "Operation completed" +# log_error "Something went wrong" + +# Store step start times for elapsed time calculation +declare -A _STEP_START_TIMES + +# Log an informational message with ⏳ emoji and timestamp +# Args: +# $1: Message to log +# Output: Formatted message to stdout +log_info() { + echo "⏳ [$(date +%H:%M:%S)] $1" +} + +# Log a success message with ✅ emoji and timestamp +# Args: +# $1: Message to log +# Output: Formatted message to stdout +log_success() { + echo "✅ [$(date +%H:%M:%S)] $1" +} + +# Log an error message with ❌ emoji and timestamp to stderr +# Args: +# $1: Message to log +# Output: Formatted error message to stderr +log_error() { + echo "❌ [$(date +%H:%M:%S)] ERROR: $1" >&2 +} + +# Start tracking a step for elapsed time calculation +# Args: +# $1: Step name +# Output: Informational log message +log_step_start() { + local step_name="$1" + _STEP_START_TIMES["$step_name"]=$(date +%s) + log_info "$step_name..." +} + +# Complete a step and display elapsed time +# Args: +# $1: Step name +# Output: Success message with elapsed time +log_step_complete() { + local step_name="$1" + local start_time="${_STEP_START_TIMES[$step_name]}" + if [ -n "$start_time" ]; then + local elapsed=$(($(date +%s) - start_time)) + log_success "$step_name completed (${elapsed}s)" + else + log_success "$step_name completed" + fi +} + +# Categorize and display error with contextual guidance +# Args: +# $1: Error type (prerequisite, network, upstream) +# $2: Error message +# $3: Suggested action +# Output: Formatted error with category-specific emoji and guidance to stderr +categorize_error() { + local error_type="$1" + local message="$2" + local suggestion="$3" + + case "$error_type" in + "prerequisite") + log_error "$message" + echo " 💡 Action required: $suggestion" >&2 + ;; + "network") + log_error "$message" + echo " 🌐 Check connectivity: $suggestion" >&2 + ;; + "upstream") + log_error "$message" + echo " 🔧 Upstream issue: $suggestion" >&2 + ;; + *) + log_error "$message" + ;; + esac +} + +# Display success banner with Zrok share token and connection instructions +# Args: +# $1: Zrok share token +# Output: Formatted success banner to stdout +show_success_banner() { + local share_token="$1" + + if command -v gum &>/dev/null; then + local header + header=$(gum style --foreground 212 --border double --border-foreground 212 --padding "1 2" --align center --width 60 "✅ Setup Complete!") + local message + message=$(gum style --foreground 255 --align center --width 60 "Your Kaggle instance is ready for remote access!") + + local token_label + token_label=$(gum style --foreground 99 "📡 Zrok Share Token:") + local token_value + token_value=$(gum style --foreground 212 --bold "$share_token") + local token_section + token_section=$(gum join --vertical --align center "$token_label" "$token_value") + local token_box + token_box=$(gum style --border rounded --padding "1 2" --border-foreground 99 --width 60 --align center "$token_section") + + local instr_label + instr_label=$(gum style --foreground 255 "🖥️ On your LOCAL machine, run:") + local cmd1 + cmd1=$(gum style --foreground 212 "zrok access private $share_token") + local cmd2_label + cmd2_label=$(gum style --foreground 255 "Then connect via SSH:") + local cmd2 + cmd2=$(gum style --foreground 212 "ssh -p 9191 root@127.0.0.1") + + local cmds_content + cmds_content=$(gum join --vertical --align center "$instr_label" " " "$cmd1" " " "$cmd2_label" " " "$cmd2") + local cmds_box + cmds_box=$(gum style --border rounded --padding "1 2" --border-foreground 255 --width 60 --align center "$cmds_content") + + printf "\n" + gum join --vertical --align center "$header" " " "$message" " " "$token_box" " " "$cmds_box" + else + echo "" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ ✅ Setup Complete! ║" + echo "╠════════════════════════════════════════════════════════════════╣" + echo "║ ║" + echo "║ Your Kaggle instance is ready for remote access! ║" + echo "║ ║" + echo "║ 📡 Zrok Share Token: $share_token" + echo "║ ║" + echo "║ 🖥️ On your LOCAL machine, run: ║" + echo "║ ║" + echo "║ zrok access private $share_token" + echo "║ ║" + echo "║ Then connect via SSH: ║" + echo "║ ║" + echo "║ ssh -p 9191 root@127.0.0.1 ║" + echo "║ ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo "" + fi +} diff --git a/setup.sh b/setup.sh index 5c07748..b911b59 100755 --- a/setup.sh +++ b/setup.sh @@ -2,6 +2,70 @@ set -e +# ============================================================================ +# Inline Logging Functions (embedded for bootstrap phase) +# ============================================================================ +# These are embedded directly in setup.sh because this script is downloaded +# standalone before the repository is cloned. Other scripts (setup_kaggle_zrok.sh, +# start_zrok.sh) source logging_utils.sh from the cloned repository. + +# Store step start times for elapsed time calculation +declare -A _STEP_START_TIMES + +log_info() { + echo "⏳ [$(date +%H:%M:%S)] $1" +} + +log_success() { + echo "✅ [$(date +%H:%M:%S)] $1" +} + +log_error() { + echo "❌ [$(date +%H:%M:%S)] ERROR: $1" >&2 +} + +log_step_start() { + local step_name="$1" + _STEP_START_TIMES["$step_name"]=$(date +%s) + log_info "$step_name..." +} + +log_step_complete() { + local step_name="$1" + local start_time="${_STEP_START_TIMES[$step_name]}" + if [ -n "$start_time" ]; then + local elapsed=$(($(date +%s) - start_time)) + log_success "$step_name completed (${elapsed}s)" + else + log_success "$step_name completed" + fi +} + +categorize_error() { + local error_type="$1" + local message="$2" + local suggestion="$3" + + case "$error_type" in + "prerequisite") + log_error "$message" + echo " 💡 Action required: $suggestion" >&2 + ;; + "network") + log_error "$message" + echo " 🌐 Check connectivity: $suggestion" >&2 + ;; + "upstream") + log_error "$message" + echo " 🔧 Upstream issue: $suggestion" >&2 + ;; + *) + log_error "$message" + ;; + esac +} +# ============================================================================ + # Version and branch configuration KAGGLELINK_VERSION="1.1.0" KAGGLELINK_BRANCH="${BRANCH:-main}" @@ -9,18 +73,13 @@ KAGGLELINK_BRANCH="${BRANCH:-main}" # Security: Validate KAGGLELINK_BRANCH to prevent argument injection # Branch names must not start with '-' to prevent git argument injection if [[ "$KAGGLELINK_BRANCH" =~ ^- ]]; then - echo "❌ Error: Invalid branch name '$KAGGLELINK_BRANCH'" - echo " Branch names cannot start with '-' (security: prevents argument injection)" + categorize_error "prerequisite" "Invalid branch name '$KAGGLELINK_BRANCH'" "Branch names cannot start with '-' (security: prevents argument injection)" exit 1 fi # Reliability: Check for git installation if ! command -v git &> /dev/null; then - echo "❌ Error: git is not installed" - echo " Please install git and try again" - echo " - Debian/Ubuntu: sudo apt-get install git" - echo " - RHEL/CentOS: sudo yum install git" - echo " - macOS: brew install git" + categorize_error "prerequisite" "git is not installed" "Install git: apt-get install git (Debian/Ubuntu), yum install git (RHEL/CentOS), or brew install git (macOS)" exit 1 fi @@ -117,45 +176,35 @@ fi # Validate that AUTH_KEYS_URL uses HTTPS (security requirement) if [[ ! "$AUTH_KEYS_URL" =~ ^https:// ]]; then - echo "❌ Error: Keys URL must use HTTPS (not HTTP)" - echo " Insecure URL: $AUTH_KEYS_URL" + categorize_error "prerequisite" "Keys URL must use HTTPS (not HTTP): $AUTH_KEYS_URL" "Use HTTPS URL instead" if [[ "$AUTH_KEYS_URL" =~ ^http:// ]]; then - echo " Use: ${AUTH_KEYS_URL/http:/https:}" - else - echo " URL must start with https://" + echo " Suggested: ${AUTH_KEYS_URL/http:/https:}" >&2 fi exit 1 fi -echo "⏳ Cloning repository..." +log_step_start "Cloning repository" if [ -d "$INSTALL_DIR" ]; then - echo "Repository directory already exists. Removing it..." + log_info "Repository directory already exists. Removing it..." rm -rf "$INSTALL_DIR" fi if ! git clone -b "$KAGGLELINK_BRANCH" "$REPO_URL" "$INSTALL_DIR"; then - echo "❌ Error: Failed to clone branch '$KAGGLELINK_BRANCH'" - echo " Possible reasons:" - echo " - Branch does not exist" - echo " - Network connectivity issues" - echo " - GitHub is unreachable" + categorize_error "network" "Failed to clone branch '$KAGGLELINK_BRANCH'" "Check branch exists and network connectivity" exit 1 fi -echo "✅ Cloned repository (branch: ${KAGGLELINK_BRANCH})" +log_step_complete "Cloning repository" -echo "⏳ Changing to repository directory..." +log_info "Changing to repository directory..." cd "$INSTALL_DIR" -echo "⏳ Making scripts executable..." +log_info "Making scripts executable..." chmod +x setup_kaggle_zrok.sh start_zrok.sh -echo "⏳ Setting up SSH with your public keys..." +log_step_start "Setting up SSH with your public keys" ./setup_kaggle_zrok.sh "$AUTH_KEYS_URL" +log_step_complete "Setting up SSH with your public keys" -echo "⏳ Starting zrok service with your token..." +log_info "Starting zrok service with your token..." +# Note: start_zrok.sh is a blocking process that will display success banner ./start_zrok.sh "$ZROK_TOKEN" - -echo "✅ Setup complete!" -echo "✅ You should now be able to connect to your Kaggle instance via SSH." -echo "✅ If you see a URL above, use that to connect from your local machine." -echo "✅ For more information, visit: https://github.com/bhdai/kagglelink" diff --git a/setup_kaggle_zrok.sh b/setup_kaggle_zrok.sh index 6694aa2..7da9144 100644 --- a/setup_kaggle_zrok.sh +++ b/setup_kaggle_zrok.sh @@ -2,6 +2,11 @@ set -e +# Source logging utilities +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=logging_utils.sh +source "$SCRIPT_DIR/logging_utils.sh" + if [ "$#" -ne 1 ]; then echo "Usage: ./setup_kaggle_zrok.sh " exit 1 @@ -10,16 +15,16 @@ fi AUTH_KEYS_URL=$1 setup_ssh_directory() { - echo "Setting up SSH directory in user's home..." + log_info "Setting up SSH directory in user's home..." # If running as root, $HOME/.ssh becomes /root/.ssh local ssh_dir_path="$HOME/.ssh" mkdir -p "$ssh_dir_path" if wget -qO "$ssh_dir_path/authorized_keys" "$AUTH_KEYS_URL"; then chmod 700 "$ssh_dir_path" chmod 600 "$ssh_dir_path/authorized_keys" - echo "SSH directory and authorized_keys set up in $ssh_dir_path" + log_success "SSH directory and authorized_keys set up in $ssh_dir_path" else - echo "Failed to download authorized keys from $AUTH_KEYS_URL to $ssh_dir_path/authorized_keys." + categorize_error "network" "Failed to download authorized keys from $AUTH_KEYS_URL" "Check URL is accessible and internet connectivity" exit 1 fi } @@ -30,26 +35,26 @@ copy_vscode_dir() { [ -d "/kaggle/.vscode" ] && rm -rf "/kaggle/.vscode" mkdir -p "/kaggle/.vscode" cp -r "$vscode_dir_in_repo/"* "/kaggle/.vscode/" - echo ".vscode folder copied to /kaggle directory." + log_info ".vscode folder copied to /kaggle directory." mkdir -p "/kaggle/tmp" [ -d "/kaggle/working/.vscode" ] && rm -rf "/kaggle/working/.vscode" mkdir -p "/kaggle/working/.vscode" cp -r "$vscode_dir_in_repo/"* "/kaggle/working/.vscode/" - echo ".vscode folder copied to /kaggle/working directory." + log_info ".vscode folder copied to /kaggle/working directory." - echo "Contents of /kaggle/.vscode:" + log_info "Contents of /kaggle/.vscode:" ls -l "/kaggle/.vscode" - echo "Contents of /kaggle/working/.vscode:" + log_info "Contents of /kaggle/working/.vscode:" ls -l "/kaggle/working/.vscode" else - echo ".vscode directory not found in repository at $vscode_dir_in_repo." + log_error ".vscode directory not found in repository at $vscode_dir_in_repo." fi } configure_sshd() { mkdir -p /var/run/sshd - echo "Configuring sshd..." + log_info "Configuring sshd..." cat <>/etc/ssh/sshd_config Port 22 Protocol 2 @@ -73,22 +78,22 @@ ClientAliveInterval 60 ClientAliveCountMax 2 EOF echo "" >>/etc/ssh/sshd_config - echo "sshd_config updated. Note: Appended settings. Ensure no conflicting duplicates exist if run multiple times." + log_success "sshd_config updated. Note: Appended settings. Ensure no conflicting duplicates exist if run multiple times." - echo "Configuring debconf for non-interactive mode..." + log_info "Configuring debconf for non-interactive mode..." echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections - echo "debconf configured to use Noninteractive frontend." + log_success "debconf configured to use Noninteractive frontend." # Disable pam_systemd for container compatibility - echo "Disabling pam_systemd..." + log_info "Disabling pam_systemd..." sed -i 's/^session.*pam_systemd.so/#&/' /etc/pam.d/common-session # Disable man-db postinst to prevent crashes - echo "Disabling man-db postinst script..." + log_info "Disabling man-db postinst script..." dpkg-divert --quiet --local --rename --add /var/lib/dpkg/info/man-db.postinst ln -sf /bin/true /var/lib/dpkg/info/man-db.postinst - echo "Container compatibility fixes applied." + log_success "Container compatibility fixes applied." } setup_environment_variables() { @@ -142,40 +147,49 @@ EOT } install_packages() { - echo "Installing openssh-server..." + log_step_start "Installing packages" + + # Install gum + log_info "Installing gum..." + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg + echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list + sudo apt-get update - sudo apt-get install -y openssh-server nvtop screen lshw + sudo apt-get install -y openssh-server nvtop screen lshw gum + log_step_complete "Installing packages" + log_info "Installing uv..." curl -LsSf https://astral.sh/uv/install.sh | sh } install_zrok() { - echo "Downloading latest zrok release" + log_step_start "Downloading and installing Zrok" curl -s https://api.github.com/repos/openziti/zrok/releases/latest | grep "browser_download_url.*linux_amd64.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -qi - - echo "Extracting Zrok" + log_info "Extracting Zrok" if ! tar -xzf zrok_*_linux_amd64.tar.gz -C /usr/local/bin/; then - echo "ERROR: Failed to extract Zrok" + categorize_error "network" "Failed to extract Zrok" "Check downloaded tar file integrity" exit 1 fi rm zrok_*_linux_amd64.tar.gz # check if zrok is installed correctly if ! zrok version &>/dev/null; then - echo "Error: Zrok install failed" + categorize_error "upstream" "Zrok install failed" "Try manual installation or check Zrok service" exit 1 fi - echo "Zrok installed successfully:" - zrok version + log_step_complete "Downloading and installing Zrok" + log_success "Zrok version: $(zrok version)" } setup_install_extensions_command() { - echo "Setting up 'install_extensions' command..." + log_info "Setting up 'install_extensions' command..." # SCRIPT_DIR will point to the directory where setup_kaggle_zrok.sh is located local SCRIPT_DIR SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) @@ -186,16 +200,15 @@ setup_install_extensions_command() { mkdir -p /usr/local/bin # Ensure target directory exists cp "$install_script_source" "$install_script_target" chmod +x "$install_script_target" - echo "'install_extensions' command is now available from $install_script_target." - echo "You can run 'install_extensions' in your terminal after SSHing." + log_success "'install_extensions' command is now available from $install_script_target." else - echo "Warning: $install_script_source not found. 'install_extensions' command not set up." + log_error "$install_script_source not found. 'install_extensions' command not set up." fi } start_ssh_service() { service ssh start - echo "SSH service should be running." + log_success "SSH service is running." } copy_screenrc() { @@ -203,9 +216,9 @@ copy_screenrc() { local dest="$HOME/.screenrc" if [ -f "$src" ]; then cp "$src" "$dest" - echo ".screenrc installed to $dest" + log_info ".screenrc installed to $dest" else - echo "Warning: $src not found; skipping .screenrc install." + log_error "$src not found; skipping .screenrc install." fi } @@ -222,4 +235,4 @@ copy_screenrc() { start_ssh_service ) -echo "Setup script completed. SSH service is running. Use start_zrok.sh to start zrok service." +log_success "Setup script completed. SSH service is running. Use start_zrok.sh to start zrok service." diff --git a/start_zrok.sh b/start_zrok.sh index 160754a..28f9329 100755 --- a/start_zrok.sh +++ b/start_zrok.sh @@ -2,6 +2,11 @@ set -e +# Source logging utilities +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=logging_utils.sh +source "$SCRIPT_DIR/logging_utils.sh" + if [ "$#" -ne 1 ]; then echo "Usage: ./start_zrok.sh " exit 1 @@ -10,27 +15,86 @@ fi ZROK_TOKEN=$1 cleanup() { - echo "Disabling zrok environment..." + log_info "Disabling zrok environment..." zrok disable - echo "Cleanup complete." + log_success "Cleanup complete." } # trap the exit signal to run the cleanup function trap cleanup EXIT -echo "Starting zrok service..." +log_info "Starting zrok service..." if [ -z "$ZROK_TOKEN" ]; then - echo "Error: ZROK_TOKEN not provided." + categorize_error "prerequisite" "ZROK_TOKEN not provided" "Provide token via -t flag" exit 1 fi -echo "Enabling zrok with provided token..." -zrok enable "$ZROK_TOKEN" || { - echo "Failed to enable zrok with provided token." +log_step_start "Enabling zrok with provided token" +if ! zrok enable "$ZROK_TOKEN"; then + categorize_error "upstream" "Failed to enable zrok with provided token" "Verify token is valid or try again later" exit 1 -} +fi +log_step_complete "Enabling zrok with provided token" + +# CRITICAL: Start zrok share in background to capture token BEFORE blocking +log_info "Starting zrok tunnel (capturing share token)..." +SHARE_OUTPUT=$(mktemp) +SHARE_OUTPUT_RAW=$(mktemp) + +# Redirect all output to raw file for debugging +zrok share private --headless --backend-mode tcpTunnel localhost:22 > "$SHARE_OUTPUT_RAW" 2>&1 & +ZROK_PID=$! + +# Give zrok more time to establish tunnel and output token (increased from 2s to 8s) +sleep 8 + +# Poll for share token with timeout (max 60 seconds) +SHARE_TOKEN="" +for i in {1..60}; do + # Copy current output for parsing + cp "$SHARE_OUTPUT_RAW" "$SHARE_OUTPUT" 2>/dev/null || true + + # Try multiple regex patterns to find the token + # Pattern 1: JSON format (non-TTY) - look inside "msg" field for "zrok access private TOKEN" + SHARE_TOKEN=$(grep -oP '"msg":"[^"]*zrok access private \K[a-zA-Z0-9]+' "$SHARE_OUTPUT" 2>/dev/null || true) + + # Pattern 2: Plain text format (TTY) - look for "zrok access private TOKEN" + if [ -z "$SHARE_TOKEN" ]; then + SHARE_TOKEN=$(grep -oP 'zrok access private \K[a-zA-Z0-9]+' "$SHARE_OUTPUT" 2>/dev/null || true) + fi + + # Pattern 3: Look for token on line containing "allow other to access" + if [ -z "$SHARE_TOKEN" ]; then + SHARE_TOKEN=$(grep "allow other to access" "$SHARE_OUTPUT" 2>/dev/null | grep -oP 'zrok access private \K[a-zA-Z0-9]+' || true) + fi + + if [ -n "$SHARE_TOKEN" ]; then + log_success "Token captured: $SHARE_TOKEN (attempt $i)" + break + fi + + # Debug output every 10 seconds + if [ $((i % 10)) -eq 0 ]; then + log_info "Still waiting for token... (${i}s elapsed)" + fi + + sleep 1 +done + +# Clean up temp files +rm -f "$SHARE_OUTPUT" "$SHARE_OUTPUT_RAW" + +if [ -z "$SHARE_TOKEN" ]; then + categorize_error "upstream" "Failed to capture share token within timeout" "Check Zrok service status and logs" + kill $ZROK_PID 2>/dev/null || true + exit 1 +fi + +# Display success banner NOW (before blocking on tunnel) +show_success_banner "$SHARE_TOKEN" -echo "Starting zrok share in headless mode..." -echo "Starting zrok share now..." -zrok share private --headless --backend-mode tcpTunnel localhost:22 +# Keep tunnel alive - wait on background process (blocks here) +log_info "Tunnel is active. Keeping connection alive..." +log_info "Press Ctrl+C to stop the tunnel and clean up." +wait $ZROK_PID diff --git a/tests/unit/test_argument_parsing.bats b/tests/unit/test_argument_parsing.bats index 91a7cc1..3a242d4 100644 --- a/tests/unit/test_argument_parsing.bats +++ b/tests/unit/test_argument_parsing.bats @@ -9,6 +9,7 @@ load '../test_helper/common' setup() { create_test_dir # Copy setup.sh to temp dir for isolated testing + # Note: setup.sh has inline logging functions, no need to copy logging_utils.sh cp "${PROJECT_ROOT}/setup.sh" "${TEST_TEMP_DIR}/" cd "${TEST_TEMP_DIR}" } diff --git a/tests/unit/test_logging.bats b/tests/unit/test_logging.bats new file mode 100644 index 0000000..d3a0802 --- /dev/null +++ b/tests/unit/test_logging.bats @@ -0,0 +1,125 @@ +#!/usr/bin/env bats + +# Unit Tests for Logging Utilities +# Story: 1-3-unified-logging-and-user-feedback-system +# Tests AC1-3 and AC5 + +load '../test_helper/common.bash' + +setup() { + # Create temporary test directory + TEST_TEMP_DIR="$(mktemp -d)" + + # Set PROJECT_ROOT for sourcing + PROJECT_ROOT="${BATS_TEST_DIRNAME}/../.." +} + +teardown() { + # Clean up temporary directory + rm -rf "$TEST_TEMP_DIR" +} + +# Task 7.1: Test log_info outputs ⏳ emoji and timestamp +@test "P0: log_info should include ⏳ emoji and timestamp" { + source "${PROJECT_ROOT}/logging_utils.sh" + + run log_info "Test message" + + [ "$status" -eq 0 ] + [[ "$output" == *"⏳"* ]] + [[ "$output" =~ \[[0-9]{2}:[0-9]{2}:[0-9]{2}\] ]] + [[ "$output" == *"Test message"* ]] +} + +# Task 7.2: Test log_success outputs ✅ emoji +@test "P0: log_success should include ✅ emoji and timestamp" { + source "${PROJECT_ROOT}/logging_utils.sh" + + run log_success "Operation completed" + + [ "$status" -eq 0 ] + [[ "$output" == *"✅"* ]] + [[ "$output" =~ \[[0-9]{2}:[0-9]{2}:[0-9]{2}\] ]] + [[ "$output" == *"Operation completed"* ]] +} + +# Task 7.3: Test log_error outputs ❌ emoji to stderr +@test "P0: log_error should output ❌ emoji to stderr" { + source "${PROJECT_ROOT}/logging_utils.sh" + + run bash -c "source ${PROJECT_ROOT}/logging_utils.sh; log_error 'Error occurred' 2>&1 1>/dev/null" + + [ "$status" -eq 0 ] + [[ "$output" == *"❌"* ]] + [[ "$output" == *"ERROR:"* ]] + [[ "$output" == *"Error occurred"* ]] +} + +# Task 7.4: Test elapsed time calculation +@test "P0: log_step_start and log_step_complete should calculate elapsed time" { + # Run in a bash subshell to maintain context between start and complete + run bash -c " + source ${PROJECT_ROOT}/logging_utils.sh + log_step_start 'Test Step' > /dev/null + sleep 1 + log_step_complete 'Test Step' + " + + [ "$status" -eq 0 ] + [[ "$output" == *"✅"* ]] + [[ "$output" == *"Test Step completed"* ]] + [[ "$output" =~ \([0-9]+s\) ]] +} + +# Test categorize_error function with different types +@test "P0: categorize_error should format prerequisite errors" { + source "${PROJECT_ROOT}/logging_utils.sh" + + run bash -c "source ${PROJECT_ROOT}/logging_utils.sh; categorize_error 'prerequisite' 'git is not installed' 'Install git: apt-get install git' 2>&1" + + [ "$status" -eq 0 ] + [[ "$output" == *"❌"* ]] + [[ "$output" == *"git is not installed"* ]] + [[ "$output" == *"💡 Action required:"* ]] + [[ "$output" == *"Install git: apt-get install git"* ]] +} + +@test "P0: categorize_error should format network errors" { + source "${PROJECT_ROOT}/logging_utils.sh" + + run bash -c "source ${PROJECT_ROOT}/logging_utils.sh; categorize_error 'network' 'Failed to download keys' 'Check URL is accessible' 2>&1" + + [ "$status" -eq 0 ] + [[ "$output" == *"❌"* ]] + [[ "$output" == *"Failed to download keys"* ]] + [[ "$output" == *"🌐 Check connectivity:"* ]] + [[ "$output" == *"Check URL is accessible"* ]] +} + +@test "P0: categorize_error should format upstream errors" { + source "${PROJECT_ROOT}/logging_utils.sh" + + run bash -c "source ${PROJECT_ROOT}/logging_utils.sh; categorize_error 'upstream' 'Zrok API failed' 'Try again later' 2>&1" + + [ "$status" -eq 0 ] + [[ "$output" == *"❌"* ]] + [[ "$output" == *"Zrok API failed"* ]] + [[ "$output" == *"🔧 Upstream issue:"* ]] + [[ "$output" == *"Try again later"* ]] +} + +# Task 7.5: Test success banner format +@test "P0: show_success_banner should display formatted banner with token" { + source "${PROJECT_ROOT}/logging_utils.sh" + + test_token="abc123xyz" + run show_success_banner "$test_token" + + [ "$status" -eq 0 ] + [[ "$output" == *"✅ Setup Complete!"* ]] + [[ "$output" == *"$test_token"* ]] + [[ "$output" == *"zrok access private $test_token"* ]] + [[ "$output" == *"ssh -p 9191 root@127.0.0.1"* ]] + [[ "$output" == *"╔"* ]] # Box drawing characters + [[ "$output" == *"╚"* ]] +} diff --git a/tests/unit/test_url_validation.bats b/tests/unit/test_url_validation.bats index 80be210..c0297e1 100755 --- a/tests/unit/test_url_validation.bats +++ b/tests/unit/test_url_validation.bats @@ -18,8 +18,14 @@ setup() { if [[ "$*" == *"clone"* ]]; then target="${@: -1}" mkdir -p "$target" + # Copy logging_utils.sh to mocked repo directory + cp /workspace/logging_utils.sh "$target/" 2>/dev/null || true echo '#!/bin/bash' > "$target/setup_kaggle_zrok.sh" + echo 'source "$(dirname "$0")/logging_utils.sh" 2>/dev/null || true' >> "$target/setup_kaggle_zrok.sh" + echo 'exit 0' >> "$target/setup_kaggle_zrok.sh" echo '#!/bin/bash' > "$target/start_zrok.sh" + echo 'source "$(dirname "$0")/logging_utils.sh" 2>/dev/null || true' >> "$target/start_zrok.sh" + echo 'exit 0' >> "$target/start_zrok.sh" chmod +x "$target/setup_kaggle_zrok.sh" "$target/start_zrok.sh" exit 0 fi @@ -77,8 +83,8 @@ teardown() { @test "P1: error message should explain why HTTP is rejected" { run bash "${PROJECT_ROOT}/setup.sh" -k "http://example.com/keys" -t "test-token" [ "$status" -ne 0 ] - # Should have actionable error message - [[ "$output" == *"Error"* ]] + # Should have actionable error message (case-insensitive check for ERROR or Error) + [[ "$output" =~ [Ee][Rr][Rr][Oo][Rr] ]] [[ "$output" == *"HTTPS"* ]] || [[ "$output" == *"secure"* ]] }