//! Integration tests for the GitHub channel using a `wiremock` HTTP server //! standing in for `api.github.com` and `raw.githubusercontent.com`. use chrono::Utc; use kez_channels::{Channel, ChannelError, github::GithubChannel}; use kez_core::{ClaimPayload, Identity, NostrSecret, SignedClaim}; use reqwest::Client; use serde_json::json; use wiremock::matchers::{header, method, path}; 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) -> GithubChannel { let client = Client::builder() .user_agent("kez-channels-test/0.1") .build() .unwrap(); GithubChannel::with_bases(client, server.uri(), server.uri()) } #[tokio::test] async fn verifies_proof_published_in_a_gist() { let server = MockServer::start().await; let signed = sign("github:jason"); let markdown = signed.to_markdown_proof().unwrap(); Mock::given(method("GET")) .and(path("/users/jason/gists")) .and(header("accept", "application/vnd.github+json")) .respond_with(ResponseTemplate::new(200).set_body_json(json!([ { "files": { "notes.txt": { "raw_url": format!("{}/raw/notes.txt", server.uri()) }, "github-jason.kez.md": { "raw_url": format!("{}/raw/github-jason.kez.md", server.uri()) } } } ]))) .mount(&server) .await; Mock::given(method("GET")) .and(path("/raw/github-jason.kez.md")) .respond_with(ResponseTemplate::new(200).set_body_string(markdown)) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("github:jason").unwrap(); let hit = channel.fetch_and_verify(&identity).await.unwrap(); assert_eq!(hit.proof, signed); } #[tokio::test] async fn falls_back_to_profile_readme_on_main() { let server = MockServer::start().await; let signed = sign("github:jason"); let markdown = signed.to_markdown_proof().unwrap(); // No matching gists. Mock::given(method("GET")) .and(path("/users/jason/gists")) .respond_with(ResponseTemplate::new(200).set_body_json(json!([]))) .mount(&server) .await; Mock::given(method("GET")) .and(path("/jason/jason/main/README.md")) .respond_with(ResponseTemplate::new(200).set_body_string(markdown)) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("github:jason").unwrap(); let hit = channel.fetch_and_verify(&identity).await.unwrap(); assert_eq!(hit.proof, signed); } #[tokio::test] async fn falls_back_to_master_when_main_missing() { let server = MockServer::start().await; let signed = sign("github:jason"); let markdown = signed.to_markdown_proof().unwrap(); Mock::given(method("GET")) .and(path("/users/jason/gists")) .respond_with(ResponseTemplate::new(200).set_body_json(json!([]))) .mount(&server) .await; // main is 404, master serves the proof. Mock::given(method("GET")) .and(path("/jason/jason/main/README.md")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; Mock::given(method("GET")) .and(path("/jason/jason/master/README.md")) .respond_with(ResponseTemplate::new(200).set_body_string(markdown)) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("github:jason").unwrap(); let hit = channel.fetch_and_verify(&identity).await.unwrap(); assert_eq!(hit.proof, signed); } #[tokio::test] async fn rejects_proof_signed_for_wrong_subject() { let server = MockServer::start().await; // Signed for github:mallory, but published in jason's gist. let signed = sign("github:mallory"); let markdown = signed.to_markdown_proof().unwrap(); Mock::given(method("GET")) .and(path("/users/jason/gists")) .respond_with(ResponseTemplate::new(200).set_body_json(json!([ { "files": { "kez.md": { "raw_url": format!("{}/raw/kez.md", server.uri()) } } } ]))) .mount(&server) .await; Mock::given(method("GET")) .and(path("/raw/kez.md")) .respond_with(ResponseTemplate::new(200).set_body_string(markdown)) .mount(&server) .await; // No fallback README either. Mock::given(method("GET")) .and(path("/jason/jason/main/README.md")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; Mock::given(method("GET")) .and(path("/jason/jason/master/README.md")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("github:jason").unwrap(); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!( matches!(err, ChannelError::SubjectMismatch { .. }), "expected SubjectMismatch, got {err:?}" ); } #[tokio::test] async fn returns_not_found_when_no_proof_anywhere() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/users/jason/gists")) .respond_with(ResponseTemplate::new(200).set_body_json(json!([]))) .mount(&server) .await; Mock::given(method("GET")) .and(path("/jason/jason/main/README.md")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; Mock::given(method("GET")) .and(path("/jason/jason/master/README.md")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; let channel = channel_pointing_at(&server); let identity = Identity::parse("github:jason").unwrap(); let err = channel.fetch_and_verify(&identity).await.unwrap_err(); assert!( matches!(err, ChannelError::NotFound(_) | ChannelError::Unreachable(_)), "expected NotFound/Unreachable, got {err:?}" ); }