bssh 1.5.0

Parallel SSH command execution tool for cluster management
Documentation
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Tests for jump host timeout calculation
//!
//! These tests verify the security fixes implemented by pr-reviewer:
//! - Integer overflow prevention using saturating arithmetic
//! - Maximum timeout cap of 600 seconds (10 minutes)
//! - Correct timeout scaling based on hop count

/// Calculate timeout for jump host connections
///
/// This replicates the timeout calculation logic from interactive.rs
/// to ensure security properties are maintained.
fn calculate_jump_host_timeout(hop_count: usize) -> u64 {
    const BASE_TIMEOUT: u64 = 30;
    const PER_HOP_TIMEOUT: u64 = 15;
    const MAX_TIMEOUT_SECS: u64 = 600; // 10 minutes max

    // SECURITY: Use saturating arithmetic to prevent integer overflow
    BASE_TIMEOUT
        .saturating_add(PER_HOP_TIMEOUT.saturating_mul(hop_count as u64))
        .min(MAX_TIMEOUT_SECS)
}

#[test]
fn test_timeout_calculation_no_hops() {
    // Direct connection (0 hops) should use base timeout
    let timeout = calculate_jump_host_timeout(0);
    assert_eq!(
        timeout, 30,
        "Direct connection should use 30 second timeout"
    );
}

#[test]
fn test_timeout_calculation_single_hop() {
    // 1 hop: 30 + (15 * 1) = 45 seconds
    let timeout = calculate_jump_host_timeout(1);
    assert_eq!(timeout, 45, "Single hop should be 45 seconds");
}

#[test]
fn test_timeout_calculation_multiple_hops() {
    // Test various hop counts
    assert_eq!(calculate_jump_host_timeout(2), 60); // 30 + 30 = 60
    assert_eq!(calculate_jump_host_timeout(3), 75); // 30 + 45 = 75
    assert_eq!(calculate_jump_host_timeout(5), 105); // 30 + 75 = 105
    assert_eq!(calculate_jump_host_timeout(10), 180); // 30 + 150 = 180
}

#[test]
fn test_timeout_calculation_max_allowed_hops() {
    // Maximum allowed hops (10) should not exceed max timeout
    let timeout = calculate_jump_host_timeout(10);
    assert_eq!(timeout, 180); // 30 + 150 = 180 (under 600 max)
    assert!(
        timeout <= 600,
        "Timeout should never exceed 600 seconds, got {timeout}"
    );
}

#[test]
fn test_timeout_calculation_cap_at_600() {
    // Test that timeout is capped at 600 seconds (10 minutes)
    // With BASE=30 and PER_HOP=15, we need 38 hops to reach 600
    // 30 + (15 * 38) = 30 + 570 = 600

    let timeout_38_hops = calculate_jump_host_timeout(38);
    assert_eq!(timeout_38_hops, 600, "38 hops should hit max timeout");

    let timeout_39_hops = calculate_jump_host_timeout(39);
    assert_eq!(
        timeout_39_hops, 600,
        "39 hops should be capped at max timeout"
    );

    let timeout_100_hops = calculate_jump_host_timeout(100);
    assert_eq!(
        timeout_100_hops, 600,
        "100 hops should be capped at max timeout"
    );
}

#[test]
fn test_timeout_calculation_prevents_integer_overflow() {
    // SECURITY TEST: Verify saturating arithmetic prevents overflow
    // Test with extremely large hop counts that would overflow without saturation

    // usize::MAX would cause overflow without saturation
    let timeout = calculate_jump_host_timeout(usize::MAX);
    assert_eq!(
        timeout, 600,
        "Extreme hop count should be capped at max timeout, not overflow"
    );

    // Test various large numbers
    let timeout = calculate_jump_host_timeout(1_000_000);
    assert_eq!(timeout, 600, "Very large hop count should be capped");

    let timeout = calculate_jump_host_timeout(u64::MAX as usize);
    assert_eq!(timeout, 600, "u64::MAX hop count should be capped");
}

#[test]
fn test_timeout_calculation_boundary_conditions() {
    // Test values around the 600 second boundary
    // 30 + (15 * hop_count) <= 600
    // 15 * hop_count <= 570
    // hop_count <= 38

    // Just under the boundary (38 hops = exactly 600)
    assert_eq!(calculate_jump_host_timeout(37), 585); // 30 + 555 = 585
    assert_eq!(calculate_jump_host_timeout(38), 600); // 30 + 570 = 600

    // Just over the boundary (should cap at 600)
    assert_eq!(calculate_jump_host_timeout(39), 600); // Would be 615, capped at 600
    assert_eq!(calculate_jump_host_timeout(40), 600); // Would be 630, capped at 600
}

#[test]
fn test_timeout_calculation_consistency() {
    // Verify timeout calculation is monotonic and consistent
    let mut prev_timeout = 0;

    for hop_count in 0..=50 {
        let timeout = calculate_jump_host_timeout(hop_count);

        // Timeout should never decrease
        assert!(
            timeout >= prev_timeout,
            "Timeout should be monotonic: hop_count={hop_count}, timeout={timeout}, prev={prev_timeout}"
        );

        // Timeout should never exceed max
        assert!(
            timeout <= 600,
            "Timeout should never exceed 600: hop_count={hop_count}, timeout={timeout}"
        );

        prev_timeout = timeout;
    }

    // After enough hops, timeout should stabilize at max
    assert_eq!(prev_timeout, 600, "Should reach and maintain max timeout");
}

#[test]
fn test_timeout_calculation_realistic_scenarios() {
    // Test realistic jump host scenarios

    // Single bastion (most common)
    assert_eq!(calculate_jump_host_timeout(1), 45);

    // Double bastion (common in high-security environments)
    assert_eq!(calculate_jump_host_timeout(2), 60);

    // Triple bastion (less common but used)
    assert_eq!(calculate_jump_host_timeout(3), 75);

    // Maximum allowed by MAX_JUMP_HOSTS (10)
    let max_allowed_timeout = calculate_jump_host_timeout(10);
    assert!(
        max_allowed_timeout <= 600,
        "Even max allowed hops should be reasonable"
    );
    assert_eq!(max_allowed_timeout, 180); // Well under 600 seconds
}

#[test]
fn test_timeout_formula_correctness() {
    // Verify the formula: timeout = BASE + (PER_HOP * hop_count), capped at MAX

    for hop_count in 0..=100 {
        let timeout = calculate_jump_host_timeout(hop_count);
        let expected_uncapped = 30 + (15 * hop_count as u64);
        let expected = expected_uncapped.min(600);

        assert_eq!(
            timeout, expected,
            "Formula mismatch at hop_count={hop_count}: got {timeout}, expected {expected}"
        );
    }
}