actpub_nodeinfo/
client.rs1use reqwest::{Client, header};
4use tracing::debug;
5use url::Url;
6
7use crate::{Discovery, Error, NodeInfo, Version};
8
9pub async fn fetch_discovery(host: &Url, client: &Client) -> Result<Discovery, Error> {
20 let url = host.join(crate::WELL_KNOWN_PATH)?;
21 debug!(%url, "fetching NodeInfo discovery");
22
23 let response = client
24 .get(url)
25 .header(header::ACCEPT, "application/json")
26 .send()
27 .await?;
28
29 let status = response.status();
30 if !status.is_success() {
31 return Err(Error::BadStatus(status.as_u16()));
32 }
33
34 Ok(response.json::<Discovery>().await?)
35}
36
37pub async fn fetch(host: &Url, version: Version, client: &Client) -> Result<NodeInfo, Error> {
48 let discovery = fetch_discovery(host, client).await?;
49 let link = discovery
50 .find_link(version)
51 .ok_or(Error::VersionNotAdvertised {
52 requested: version.as_str(),
53 })?;
54
55 debug!(url = %link.href, "fetching NodeInfo document");
56
57 let response = client
58 .get(link.href.clone())
59 .header(header::ACCEPT, "application/json")
60 .send()
61 .await?;
62
63 let status = response.status();
64 if !status.is_success() {
65 return Err(Error::BadStatus(status.as_u16()));
66 }
67
68 Ok(response.json::<NodeInfo>().await?)
69}
70
71#[cfg(test)]
72mod tests {
73 use pretty_assertions::assert_eq;
74 use serde_json::json;
75 use wiremock::matchers::{method, path};
76 use wiremock::{Mock, MockServer, ResponseTemplate};
77
78 use super::*;
79
80 #[tokio::test]
81 async fn end_to_end_fetch_via_mock_server() {
82 let server = MockServer::start().await;
83 let base: Url = server.uri().parse().unwrap();
84
85 let nodeinfo_url = format!("{base}nodeinfo/2.1");
86
87 Mock::given(method("GET"))
88 .and(path("/.well-known/nodeinfo"))
89 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
90 "links": [{
91 "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
92 "href": nodeinfo_url
93 }]
94 })))
95 .mount(&server)
96 .await;
97
98 Mock::given(method("GET"))
99 .and(path("/nodeinfo/2.1"))
100 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
101 "version": "2.1",
102 "software": { "name": "mock-server", "version": "0.1.0" },
103 "protocols": ["activitypub"],
104 "openRegistrations": false,
105 "usage": {}
106 })))
107 .mount(&server)
108 .await;
109
110 let client = Client::new();
111 let info = fetch(&base, Version::V2_1, &client).await.unwrap();
112
113 assert_eq!(info.version, Version::V2_1);
114 assert_eq!(info.software.name, "mock-server");
115 }
116
117 #[tokio::test]
118 async fn missing_version_returns_specific_error() {
119 let server = MockServer::start().await;
120 let base: Url = server.uri().parse().unwrap();
121
122 Mock::given(method("GET"))
123 .and(path("/.well-known/nodeinfo"))
124 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
125 "links": [{
126 "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
127 "href": format!("{base}nodeinfo/2.0")
128 }]
129 })))
130 .mount(&server)
131 .await;
132
133 let client = Client::new();
134 let err = fetch(&base, Version::V2_1, &client).await.unwrap_err();
135 assert!(matches!(
136 err,
137 Error::VersionNotAdvertised { requested: "2.1" }
138 ));
139 }
140}