🐸 Frogue(Fed)

~ololduck/Frogue

Raw

//! Integration tests for handling deleted remote actors that return 410 Gone.

mod support;

use std::time::Duration;

use support::ap::RemoteActor;
use support::{Client, Server, free_port};

/// The password the tests register and log in with.
const PASSWORD: &str = "correct horse battery staple";

/// Register a user through the web form.
fn register(client: &mut Client, name: &str) {
    let email = format!("{name}@example.org");
    let response = client.post_form(
        "/register",
        &[("name", name), ("email", &email), ("password", PASSWORD)],
    );
    assert_eq!(
        response.status, 303,
        "registering should log in\n{}",
        response.body
    );
}

/// Test: Inbox responds with 410 when the signer's actor fetch returns 410 Gone
#[tokio::test]
async fn inbox_returns_410_for_gone_signer() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor that returns 410 Gone for actor fetches
    let recorder_port = free_port();
    let recorder = RemoteActor::serve_gone(recorder_port);

    // Send a signed activity from the gone actor
    let activity_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activity/1", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "type": "Note",
            "content": "Hello from a deleted account"
        }
    });
    let body = serde_json::to_string(&activity_json).unwrap();

    let (signature, signature_input, date, content_digest) = support::ap::signed_post_rfc9421(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );

    let response = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Signature-Input", &signature_input),
            ("Date", &date),
            ("Content-Digest", &content_digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    assert_eq!(
        response.status, 410,
        "Inbox should return 410 Gone when signer's actor fetch returns 410"
    );

    Ok(())
}

/// Test: Tombstone caching - second request doesn't re-fetch the gone actor
#[tokio::test]
async fn tombstone_prevents_refetch() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_federated(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let mut alice_client = Client::new(port);
    register(&mut alice_client, "alice");

    // Start a remote actor that returns 410 Gone
    let recorder_port = free_port();
    let recorder = RemoteActor::serve_gone(recorder_port);

    // First request - triggers the fetch and creates tombstone
    let activity_json = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activity/1", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "type": "Note",
            "content": "First request"
        }
    });
    let body = serde_json::to_string(&activity_json).unwrap();

    let (signature, signature_input, date, content_digest) = support::ap::signed_post_rfc9421(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body,
    );

    let response1 = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body,
        &[
            ("Signature", &signature),
            ("Signature-Input", &signature_input),
            ("Date", &date),
            ("Content-Digest", &content_digest),
            ("Content-Type", "application/activity+json"),
        ],
    );

    assert_eq!(response1.status, 410, "First request should return 410");

    // Wait a bit for the fetch to complete
    std::thread::sleep(Duration::from_millis(100));

    // Count GET requests after first request
    let get_count_after_first = recorder.recorded_get_requests().len();

    // Second request - should use tombstone, not re-fetch
    let activity_json2 = serde_json::json!({
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": format!("{}/activity/2", recorder.actor_url),
        "type": "Create",
        "actor": recorder.actor_url,
        "object": {
            "type": "Note",
            "content": "Second request"
        }
    });
    let body2 = serde_json::to_string(&activity_json2).unwrap();

    let (signature2, signature_input2, date2, content_digest2) = support::ap::signed_post_rfc9421(
        &recorder.private_key_pem,
        &recorder.key_id,
        &format!("127.0.0.1:{port}"),
        "/~alice/inbox",
        &body2,
    );

    let response2 = alice_client.post_json_with_headers(
        "/~alice/inbox",
        &body2,
        &[
            ("Signature", &signature2),
            ("Signature-Input", &signature_input2),
            ("Date", &date2),
            ("Content-Digest", &content_digest2),
            ("Content-Type", "application/activity+json"),
        ],
    );

    assert_eq!(
        response2.status, 410,
        "Second request should also return 410"
    );

    // Wait a bit
    std::thread::sleep(Duration::from_millis(100));

    // Verify no additional GET requests were made (tombstone was used)
    let get_count_after_second = recorder.recorded_get_requests().len();
    assert_eq!(
        get_count_after_first, get_count_after_second,
        "Second request should not trigger a new actor fetch (tombstone should be used). First: {}, Second: {}",
        get_count_after_first, get_count_after_second
    );

    Ok(())
}