torrust-actix 4.2.3

A rich, fast and efficient Bittorrent Tracker.
mod common;

use actix_web::{
    test,
    App
};
use std::net::IpAddr;
use std::sync::Arc;
use torrust_actix::http::http::{
    http_service_cors,
    http_service_routes
};
use torrust_actix::http::structs::http_service_data::HttpServiceData;

fn is_failure_response(body: &[u8]) -> bool {
    body.windows(14).any(|w| w == b"failure reason")
}

fn response_has_key(body: &[u8], needle: &[u8]) -> bool {
    body.windows(needle.len()).any(|w| w == needle)
}

macro_rules! make_app {
    ($rtctorrent:expr) => {{
        let tracker = common::create_test_tracker().await;
        let http_config = Arc::new(
            common::create_test_http_config_with_rtctorrent($rtctorrent)
                .as_ref()
                .clone(),
        );
        let app = test::init_service(
            App::new()
                .wrap(http_service_cors())
                .configure(http_service_routes(Arc::new(HttpServiceData {
                    torrent_tracker: tracker.clone(),
                    http_trackers_config: http_config,
                }))),
        )
        .await;
        (app, tracker)
    }};
}

#[actix_web::test]
async fn test_http_announce_endpoint() {
    let tracker: common::TestTracker = common::create_test_tracker().await;
    let http_config = std::sync::Arc::new(common::create_test_http_config().as_ref().clone());
    let app = test::init_service(
        App::new()
            .wrap(http_service_cors())
            .configure(http_service_routes(std::sync::Arc::new(HttpServiceData {
                torrent_tracker: tracker.clone(),
                http_trackers_config: http_config,
            }))),
    )
    .await;
    let info_hash = common::random_info_hash();
    let peer_id = common::random_peer_id();
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=1000",
            hex::encode(info_hash.0),
            hex::encode(peer_id.0)
        ))
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert!(resp.status().is_success() || resp.status().is_client_error(),
            "Announce endpoint should respond");
}

#[actix_web::test]
async fn test_http_scrape_endpoint() {
    let tracker: common::TestTracker = common::create_test_tracker().await;
    let http_config = std::sync::Arc::new(common::create_test_http_config().as_ref().clone());
    let app = test::init_service(
        App::new()
            .wrap(http_service_cors())
            .configure(http_service_routes(std::sync::Arc::new(HttpServiceData {
                torrent_tracker: tracker.clone(),
                http_trackers_config: http_config,
            }))),
    )
    .await;
    let info_hash = common::random_info_hash();
    let req = test::TestRequest::get()
        .uri(&format!("/scrape?info_hash={}", hex::encode(info_hash.0)))
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert!(resp.status().is_success() || resp.status().is_client_error(),
            "Scrape endpoint should respond");
}

#[actix_web::test]
async fn test_http_cors_headers() {
    let tracker: common::TestTracker = common::create_test_tracker().await;
    let http_config = std::sync::Arc::new(common::create_test_http_config().as_ref().clone());
    let app = test::init_service(
        App::new()
            .wrap(http_service_cors())
            .configure(http_service_routes(std::sync::Arc::new(HttpServiceData {
                torrent_tracker: tracker,
                http_trackers_config: http_config,
            }))),
    )
    .await;
    let req = test::TestRequest::default()
        .method(actix_web::http::Method::OPTIONS)
        .uri("/announce")
        .insert_header(("Origin", "http://localhost"))
        .insert_header(("Access-Control-Request-Method", "GET"))
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert!(resp.headers().contains_key("access-control-allow-origin") ||
            resp.headers().contains_key("access-control-allow-methods"),
            "CORS headers should be present");
}

#[actix_web::test]
async fn test_http_invalid_endpoint() {
    let tracker: common::TestTracker = common::create_test_tracker().await;
    let http_config = std::sync::Arc::new(common::create_test_http_config().as_ref().clone());
    let app = test::init_service(
        App::new()
            .wrap(http_service_cors())
            .configure(http_service_routes(std::sync::Arc::new(HttpServiceData {
                torrent_tracker: tracker,
                http_trackers_config: http_config,
            }))),
    )
    .await;
    let req = test::TestRequest::get()
        .uri("/nonexistent")
        .peer_addr("127.0.0.1:12345".parse().unwrap())
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert!(resp.status().is_client_error(), "Should return 404 for invalid endpoint");
}

#[actix_web::test]
async fn test_http_announce_with_ipv6() {
    let tracker: common::TestTracker = common::create_test_tracker().await;
    let info_hash = common::random_info_hash();
    let peer_id = common::random_peer_id();
    let ipv6_peer = common::create_test_peer(
        peer_id,
        IpAddr::V6("2001:db8::1".parse().unwrap()),
        6881,
    );
    tracker.add_torrent_peer(info_hash, peer_id, ipv6_peer, false);
    let result = tracker.get_torrent_peers(
        info_hash,
        0,
        torrust_actix::tracker::enums::torrent_peers_type::TorrentPeersType::IPv6,
        None,
    );
    assert!(result.is_some());
    let peers = result.unwrap();
    assert_eq!(peers.peers_ipv6.len(), 1, "Should have 1 IPv6 peer");
}

#[actix_web::test]
async fn test_http_server_cleanup_optimization() {
    let tracker: common::TestTracker = common::create_test_tracker().await;
    let http_config = std::sync::Arc::new(common::create_test_http_config().as_ref().clone());
    let service_data = std::sync::Arc::new(HttpServiceData {
        torrent_tracker: tracker.clone(),
        http_trackers_config: http_config.clone(),
    });
    let app = test::init_service(
        App::new()
            .wrap(http_service_cors())
            .configure(http_service_routes(service_data)),
    )
    .await;
    let req = test::TestRequest::get()
        .uri("/scrape")
        .to_request();
    let resp = test::call_service(&app, req).await;
    assert!(resp.status().is_success() || resp.status().is_client_error(),
            "Refactored server should work correctly");
}

#[actix_web::test]
async fn test_rtc_disabled_rejects_rtctorrent_request() {
    let (app, _tracker) = make_app!(false);
    let info_hash = common::random_info_hash();
    let peer_id   = common::random_peer_id();
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&peer_id.0),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    let resp = test::call_service(&app, req).await;
    let body = test::read_body(resp).await;
    assert!(is_failure_response(&body), "RTC request with rtctorrent=false config should return failure");
    assert!(body.windows(22).any(|w| w == b"rtctorrent not enabled"),
            "Failure reason should say 'rtctorrent not enabled'");
}

#[actix_web::test]
async fn test_standard_announce_unaffected_by_rtctorrent_flag() {
    for rtctorrent_flag in [false, true] {
        let (app, _tracker) = make_app!(rtctorrent_flag);
        let info_hash = common::random_info_hash();
        let peer_id   = common::random_peer_id();
        let req = test::TestRequest::get()
            .uri(&format!(
                "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=1000&compact=1",
                common::percent_encode(&info_hash.0),
                common::percent_encode(&peer_id.0),
            ))
            .peer_addr("127.0.0.1:0".parse().unwrap())
            .to_request();
        let resp = test::call_service(&app, req).await;
        let body = test::read_body(resp).await;
        assert!(
            !is_failure_response(&body),
            "Standard announce should succeed regardless of rtctorrent config (flag={})", rtctorrent_flag
        );
        assert!(response_has_key(&body, b"interval"), "Standard response must contain 'interval'");
    }
}

#[actix_web::test]
async fn test_rtc_enabled_seeder_announce_returns_rtc_fields() {
    let (app, _tracker) = make_app!(true);
    let info_hash = common::random_info_hash();
    let peer_id   = common::random_peer_id();
    let offer     = "v=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=offer\r\n";
    let encoded_offer = common::percent_encode(offer.as_bytes());
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1&rtcoffer={}",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&peer_id.0),
            encoded_offer,
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    let resp = test::call_service(&app, req).await;
    let body = test::read_body(resp).await;
    assert!(!is_failure_response(&body), "RTC seeder announce should succeed");
    assert!(response_has_key(&body, b"rtc_peers"),   "Response must contain 'rtc_peers'");
    assert!(response_has_key(&body, b"rtc_answers"), "Response must contain 'rtc_answers'");
    assert!(response_has_key(&body, b"rtc interval"), "Response must contain 'rtc interval'");
}

#[actix_web::test]
async fn test_rtc_leecher_receives_seeder_offer() {
    let (app, _tracker) = make_app!(true);
    let info_hash  = common::random_info_hash();
    let seeder_id  = common::random_peer_id();
    let leecher_id = common::random_peer_id();
    let offer      = "v=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=offer\r\n";
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1&rtcoffer={}",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&seeder_id.0),
            common::percent_encode(offer.as_bytes()),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    test::call_service(&app, req).await;
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6882&uploaded=0&downloaded=0&left=1000&compact=1&rtctorrent=1&rtcrequest=1",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&leecher_id.0),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    let resp = test::call_service(&app, req).await;
    let body = test::read_body(resp).await;
    assert!(!is_failure_response(&body), "Leecher announce should succeed");
    assert!(response_has_key(&body, b"rtc_peers"), "Leecher response must contain 'rtc_peers'");
    assert!(
        body.windows(20).any(|w| w == seeder_id.0),
        "Seeder peer_id should appear inside rtc_peers list"
    );
}

#[actix_web::test]
async fn test_rtc_seeder_receives_leecher_answer() {
    let (app, _tracker) = make_app!(true);
    let info_hash  = common::random_info_hash();
    let seeder_id  = common::random_peer_id();
    let leecher_id = common::random_peer_id();
    let offer  = "v=0\r\ns=offer\r\n";
    let answer = "v=0\r\ns=answer\r\n";
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1&rtcoffer={}",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&seeder_id.0),
            common::percent_encode(offer.as_bytes()),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    test::call_service(&app, req).await;
    let seeder_hex = hex::encode(seeder_id.0);
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6882&uploaded=0&downloaded=0&left=1000&compact=1&rtctorrent=1&rtcanswer={}&rtcanswerfor={}",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&leecher_id.0),
            common::percent_encode(answer.as_bytes()),
            seeder_hex,
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    test::call_service(&app, req).await;
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&seeder_id.0),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    let resp = test::call_service(&app, req).await;
    let body = test::read_body(resp).await;
    assert!(!is_failure_response(&body), "Seeder poll should succeed");
    assert!(response_has_key(&body, b"rtc_answers"), "Response must contain 'rtc_answers'");
    assert!(
        body.windows(20).any(|w| w == leecher_id.0),
        "Leecher peer_id should appear inside rtc_answers list"
    );
}

#[actix_web::test]
async fn test_rtc_answers_consumed_after_poll() {
    let (app, _tracker) = make_app!(true);
    let info_hash  = common::random_info_hash();
    let seeder_id  = common::random_peer_id();
    let leecher_id = common::random_peer_id();
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1&rtcoffer={}",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&seeder_id.0),
            common::percent_encode(b"v=0\r\ns=offer\r\n"),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    test::call_service(&app, req).await;
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6882&uploaded=0&downloaded=0&left=1000&compact=1&rtctorrent=1&rtcanswer={}&rtcanswerfor={}",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&leecher_id.0),
            common::percent_encode(b"v=0\r\ns=answer\r\n"),
            hex::encode(seeder_id.0),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    test::call_service(&app, req).await;
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&seeder_id.0),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    let resp = test::call_service(&app, req).await;
    let body = test::read_body(resp).await;
    assert!(
        body.windows(20).any(|w| w == leecher_id.0),
        "First poll: leecher answer should be present"
    );
    let req = test::TestRequest::get()
        .uri(&format!(
            "/announce?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left=0&compact=1&rtctorrent=1",
            common::percent_encode(&info_hash.0),
            common::percent_encode(&seeder_id.0),
        ))
        .peer_addr("127.0.0.1:0".parse().unwrap())
        .to_request();
    let resp = test::call_service(&app, req).await;
    let body = test::read_body(resp).await;
    assert!(
        !body.windows(20).any(|w| w == leecher_id.0),
        "Second poll: leecher answer should be consumed and absent"
    );
}