//! Integration tests for the ActivityPub channel. A `wiremock` server stands //! in for `mastodon.social` (or any AP server) and serves WebFinger + actor //! responses. use chrono::Utc; use kez_channels::activitypub::ActivityPubChannel; use kez_channels::{Channel, ChannelError}; use kez_core::{ClaimPayload, Identity, NostrSecret, SignedClaim}; use reqwest::Client; use serde_json::json; use wiremock::matchers::{header, method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; fn sign(subject: &str) -> SignedClaim { let secret = NostrSecret::generate(); let primary = Identity::parse(format!("nostr:{}", secret.npub())).unwrap(); let subject = Identity::parse(subject).unwrap(); SignedClaim::sign(ClaimPayload::new(subject, primary, Utc::now()), &secret).unwrap() } fn channel_pointing_at(server: &MockServer) -> ActivityPubChannel { let client = Client::builder() .user_agent("kez-channels-test/0.1") .build() .unwrap(); ActivityPubChannel::with_base(client, server.uri()) } fn mock_webfinger(user: &str, host: &str, actor_url: &str) -> Mock { Mock::given(method("GET")) .and(path("/.well-known/webfinger")) .and(query_param("resource", format!("acct:{user}@{host}"))) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "subject": format!("acct:{user}@{host}"), "links": [ {"rel": "self", "type": "application/activity+json", "href": actor_url} ] }))) } #[tokio::test] async fn verifies_proof_in_profile_attachment() { let server = MockServer::start().await; let signed = sign("ap:@jason@mastodon.social"); let compact = signed.to_compact().unwrap(); let actor_url = format!("{}/users/jason", server.uri()); mock_webfinger("jason", "mastodon.social", &actor_url) .mount(&server) .await; Mock::given(method("GET")) .and(path("/users/jason")) .and(header("accept", "application/activity+json")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": actor_url, "type": "Person", "preferredUsername": "jason", "summary": "

hi

", "attachment": [ {"type": "PropertyValue", "name": "site", "value": "x"}, {"type": "PropertyValue", "name": "kez", "value": compact} ] }))) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("ap:@jason@mastodon.social").unwrap(); let hit = channel.fetch_and_verify(&identity).await.unwrap(); assert_eq!(hit.proof, signed); } #[tokio::test] async fn verifies_proof_embedded_in_bio() { let server = MockServer::start().await; let signed = sign("ap:@jason@mastodon.social"); let compact = signed.to_compact().unwrap(); let actor_url = format!("{}/users/jason", server.uri()); mock_webfinger("jason", "mastodon.social", &actor_url) .mount(&server) .await; Mock::given(method("GET")) .and(path("/users/jason")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": actor_url, "type": "Person", "preferredUsername": "jason", "summary": format!("

portable identity: {compact}

"), "attachment": [] }))) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("ap:@jason@mastodon.social").unwrap(); let hit = channel.fetch_and_verify(&identity).await.unwrap(); assert_eq!(hit.proof, signed); } #[tokio::test] async fn rejects_proof_for_wrong_subject() { let server = MockServer::start().await; let signed = sign("ap:@mallory@mastodon.social"); let compact = signed.to_compact().unwrap(); let actor_url = format!("{}/users/jason", server.uri()); mock_webfinger("jason", "mastodon.social", &actor_url) .mount(&server) .await; Mock::given(method("GET")) .and(path("/users/jason")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": actor_url, "attachment": [ {"type": "PropertyValue", "name": "kez", "value": compact} ] }))) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("ap:@jason@mastodon.social").unwrap(); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!( matches!(err, ChannelError::SubjectMismatch { .. }), "expected SubjectMismatch, got {err:?}" ); } #[tokio::test] async fn webfinger_404_is_unreachable() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/.well-known/webfinger")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("ap:@ghost@mastodon.social").unwrap(); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!( matches!(err, ChannelError::Unreachable(_)), "expected Unreachable on 404, got {err:?}" ); } #[tokio::test] async fn actor_with_no_candidates_is_not_found() { let server = MockServer::start().await; let actor_url = format!("{}/users/jason", server.uri()); mock_webfinger("jason", "mastodon.social", &actor_url) .mount(&server) .await; Mock::given(method("GET")) .and(path("/users/jason")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": actor_url, "preferredUsername": "jason" // no summary, no attachments }))) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("ap:@jason@mastodon.social").unwrap(); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!(matches!(err, ChannelError::NotFound(_))); } #[tokio::test] async fn webfinger_with_no_self_link_is_not_found() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/.well-known/webfinger")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "subject": "acct:jason@mastodon.social", "links": [ {"rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://example/@jason"} ] }))) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("ap:@jason@mastodon.social").unwrap(); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!(matches!(err, ChannelError::NotFound(_))); }