//! 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(())
}