rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Unit tests for `ProxyConfig`, `ProxyAddr`, `ProxyKind`, and `UdpSupport`.
//!
//! These tests cover configuration construction and validation only.
//! Wire-level behaviour (actual SOCKS5/HTTP tunnelling) requires a live proxy
//! and belongs in manual or integration tests.

#[cfg(feature = "proxy")]
mod proxy {
    use rust_mc_status::proxy::{ProxyConfig, ProxyKind, UdpSupport};
    use rust_mc_status::proxy::socks5::ProxyAddr;
    use rust_mc_status::McError;
    use rust_mc_status::error::ConfigError;

    // ─── ProxyAddr::parse ─────────────────────────────────────────────────────

    #[test]
    fn proxy_addr_parse_host_and_port() {
        let addr = ProxyAddr::parse("127.0.0.1:1080").unwrap();
        assert_eq!(addr.host, "127.0.0.1");
        assert_eq!(addr.port, 1080);
    }

    #[test]
    fn proxy_addr_parse_hostname() {
        let addr = ProxyAddr::parse("proxy.example.com:3128").unwrap();
        assert_eq!(addr.host, "proxy.example.com");
        assert_eq!(addr.port, 3128);
    }

    #[test]
    fn proxy_addr_parse_max_port() {
        let addr = ProxyAddr::parse("host:65535").unwrap();
        assert_eq!(addr.port, 65535);
    }

    #[test]
    fn proxy_addr_display() {
        let addr = ProxyAddr::parse("127.0.0.1:1080").unwrap();
        assert_eq!(addr.to_string(), "127.0.0.1:1080");
    }

    #[test]
    fn proxy_addr_parse_missing_port_is_error() {
        let e = ProxyAddr::parse("127.0.0.1").unwrap_err();
        assert!(
            matches!(e, McError::Config(ConfigError::InvalidAddress(_))),
            "expected ConfigError::InvalidAddress, got {e:?}"
        );
    }

    #[test]
    fn proxy_addr_parse_invalid_port_is_error() {
        let e = ProxyAddr::parse("127.0.0.1:99999").unwrap_err();
        assert!(
            matches!(e, McError::Config(ConfigError::InvalidPort(_))),
            "expected ConfigError::InvalidPort, got {e:?}"
        );
    }

    #[test]
    fn proxy_addr_parse_non_numeric_port_is_error() {
        let e = ProxyAddr::parse("host:abc").unwrap_err();
        assert!(matches!(e, McError::Config(ConfigError::InvalidPort(_))));
    }

    // ─── ProxyConfig::socks5 ──────────────────────────────────────────────────

    #[test]
    fn socks5_kind_is_socks5() {
        let cfg = ProxyConfig::socks5("127.0.0.1:1080");
        assert_eq!(cfg.kind(), ProxyKind::Socks5);
    }

    #[test]
    fn socks5_udp_support_is_no_by_default() {
        let cfg = ProxyConfig::socks5("127.0.0.1:1080");
        assert_eq!(cfg.udp_support(), UdpSupport::No);
    }

    #[test]
    fn socks5_auth_is_none_by_default() {
        let cfg = ProxyConfig::socks5("127.0.0.1:1080");
        assert!(cfg.auth().is_none());
    }

    #[test]
    fn socks5_addr_parsed_correctly() {
        let cfg = ProxyConfig::socks5("proxy.example.com:1080");
        assert_eq!(cfg.addr().host, "proxy.example.com");
        assert_eq!(cfg.addr().port, 1080);
    }

    // ─── ProxyConfig::socks5_with_udp ─────────────────────────────────────────

    #[test]
    fn socks5_with_udp_kind_is_socks5() {
        let cfg = ProxyConfig::socks5_with_udp("127.0.0.1:1080");
        assert_eq!(cfg.kind(), ProxyKind::Socks5);
    }

    #[test]
    fn socks5_with_udp_udp_support_is_yes() {
        let cfg = ProxyConfig::socks5_with_udp("127.0.0.1:1080");
        assert_eq!(cfg.udp_support(), UdpSupport::Yes);
    }

    #[test]
    fn socks5_with_udp_addr_parsed() {
        let cfg = ProxyConfig::socks5_with_udp("10.0.0.1:9050");
        assert_eq!(cfg.addr().port, 9050);
    }

    // ─── ProxyConfig::http ────────────────────────────────────────────────────

    #[test]
    fn http_kind_is_http() {
        let cfg = ProxyConfig::http("squid.example.com:3128");
        assert_eq!(cfg.kind(), ProxyKind::Http);
    }

    #[test]
    fn http_udp_support_is_no() {
        // HTTP CONNECT is TCP-only — UDP must never be enabled
        let cfg = ProxyConfig::http("squid.example.com:3128");
        assert_eq!(cfg.udp_support(), UdpSupport::No,
            "HTTP CONNECT proxies cannot support UDP");
    }

    #[test]
    fn http_auth_is_none_by_default() {
        let cfg = ProxyConfig::http("squid.example.com:3128");
        assert!(cfg.auth().is_none());
    }

    #[test]
    fn http_addr_parsed_correctly() {
        let cfg = ProxyConfig::http("corporate-proxy.example.com:8080");
        assert_eq!(cfg.addr().host, "corporate-proxy.example.com");
        assert_eq!(cfg.addr().port, 8080);
    }

    // ─── with_auth ────────────────────────────────────────────────────────────

    #[test]
    fn socks5_with_auth_stores_credentials() {
        let cfg = ProxyConfig::socks5("127.0.0.1:1080")
            .with_auth("alice", "s3cr3t");
        let auth = cfg.auth().expect("auth should be present");
        assert_eq!(auth.username, "alice");
        assert_eq!(auth.password, "s3cr3t");
    }

    #[test]
    fn http_with_auth_stores_credentials() {
        let cfg = ProxyConfig::http("proxy:3128")
            .with_auth("bob", "hunter2");
        let auth = cfg.auth().expect("auth should be present");
        assert_eq!(auth.username, "bob");
        assert_eq!(auth.password, "hunter2");
    }

    #[test]
    fn with_auth_does_not_change_kind() {
        let cfg = ProxyConfig::socks5("127.0.0.1:1080")
            .with_auth("user", "pass");
        assert_eq!(cfg.kind(), ProxyKind::Socks5);
    }

    #[test]
    fn with_auth_does_not_change_udp_support() {
        let cfg = ProxyConfig::socks5_with_udp("127.0.0.1:1080")
            .with_auth("user", "pass");
        assert_eq!(cfg.udp_support(), UdpSupport::Yes);
    }

    #[test]
    fn auth_password_hidden_in_debug_output() {
        let cfg = ProxyConfig::socks5("127.0.0.1:1080")
            .with_auth("alice", "supersecret");
        let debug = format!("{:?}", cfg.auth().unwrap());
        assert!(!debug.contains("supersecret"),
            "password must not appear in Debug output: {debug}");
        assert!(debug.contains("***"),
            "password should be masked as ***: {debug}");
    }

    // ─── ProxyKind distinctness ───────────────────────────────────────────────

    #[test]
    fn proxy_kind_socks5_not_equal_to_http() {
        assert_ne!(ProxyKind::Socks5, ProxyKind::Http);
    }

    #[test]
    fn proxy_kind_copy() {
        let k = ProxyKind::Socks5;
        let k2 = k; // Copy
        assert_eq!(k, k2);
    }

    // ─── UdpSupport ───────────────────────────────────────────────────────────

    #[test]
    fn udp_support_default_is_no() {
        let default: UdpSupport = Default::default();
        assert_eq!(default, UdpSupport::No);
    }

    #[test]
    fn udp_support_yes_not_equal_no() {
        assert_ne!(UdpSupport::Yes, UdpSupport::No);
    }

    // ─── Bedrock rejects non-UDP proxy ────────────────────────────────────────
    //
    // We can't test the actual network call without a live server, but we can
    // verify the error is returned via the McClient when proxy has no UDP.
    // This is a lightweight check that the guard logic fires.

    #[tokio::test]
    async fn bedrock_via_non_udp_socks5_returns_udp_unsupported() {
        use rust_mc_status::{McClient, McError};
        use rust_mc_status::error::ProxyError;

        let client = McClient::builder()
            .timeout(std::time::Duration::from_millis(100))
            .proxy(ProxyConfig::socks5("127.0.0.1:1"))  // port 1 = unreachable, but error hits before connect
            .build();

        let err = client.bedrock("geo.hivebedrock.network")
            .await
            .expect_err("should fail with ProxyUdpUnsupported");

        assert!(
            matches!(err, McError::Proxy(ProxyError::UdpUnsupported(_))),
            "expected ProxyUdpUnsupported, got {err:?}"
        );
    }

    #[tokio::test]
    async fn bedrock_via_http_proxy_returns_udp_unsupported() {
        use rust_mc_status::{McClient, McError};
        use rust_mc_status::error::ProxyError;

        let client = McClient::builder()
            .timeout(std::time::Duration::from_millis(100))
            .proxy(ProxyConfig::http("127.0.0.1:1"))
            .build();

        let err = client.bedrock("geo.hivebedrock.network")
            .await
            .expect_err("HTTP proxy has no UDP");

        assert!(
            matches!(err, McError::Proxy(ProxyError::UdpUnsupported(_))),
            "expected ProxyUdpUnsupported, got {err:?}"
        );
    }

    #[tokio::test]
    async fn bedrock_via_socks5_with_udp_does_not_return_udp_error() {
        use rust_mc_status::{McClient, McError};
        use rust_mc_status::error::ProxyError;

        // port 1 = unreachable, so the ping fails — but NOT with UdpUnsupported
        let client = McClient::builder()
            .timeout(std::time::Duration::from_millis(100))
            .proxy(ProxyConfig::socks5_with_udp("127.0.0.1:1"))
            .build();

        let err = client.bedrock("geo.hivebedrock.network")
            .await
            .expect_err("should fail (unreachable proxy)");

        assert!(
            !matches!(err, McError::Proxy(ProxyError::UdpUnsupported(_))),
            "socks5_with_udp should NOT return UdpUnsupported, got {err:?}"
        );
    }

    // ─── Clone preserves all fields ───────────────────────────────────────────

    #[test]
    fn clone_preserves_kind() {
        let cfg = ProxyConfig::http("proxy:3128").with_auth("u", "p");
        let cloned = cfg.clone();
        assert_eq!(cloned.kind(), ProxyKind::Http);
        assert_eq!(cloned.addr().port, 3128);
        assert_eq!(cloned.auth().unwrap().username, "u");
    }
}