Skip to main content

chrome_for_testing/api/
mod.rs

1use crate::api::version::Version;
2use crate::error::Error;
3use platform::Platform;
4use reqwest::Url;
5use rootcause::prelude::ResultExt;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use std::sync::LazyLock;
9
10/// Chrome release channel definitions.
11pub mod channel;
12
13/// Platform identification for different operating systems and architectures.
14pub mod platform;
15
16/// Version parsing and representation.
17pub mod version;
18
19/// API request for a list of working releases. None are assigned to any channel.
20pub mod known_good_versions;
21
22/// The last working releases for each channel.
23pub mod last_known_good_versions;
24
25/// The standard chrome-for-testing API endpoint protocol and hostname.
26///
27/// Consult <https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints>
28/// for verification.
29pub static API_BASE_URL: LazyLock<Url> =
30    LazyLock::new(|| Url::parse("https://googlechromelabs.github.io").expect("Valid URL"));
31
32/// Represents a download link for a specific platform.
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub struct Download {
35    /// The target platform for this download.
36    pub platform: Platform,
37
38    /// The download URL.
39    pub url: String,
40}
41
42impl Download {
43    /// Parses the download URL into a typed [`Url`].
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the upstream URL string is not a valid URL.
48    pub fn parsed_url(&self) -> crate::Result<Url> {
49        Url::parse(&self.url).context_to::<Error>().attach_with(|| {
50            format!(
51                "while parsing Chrome for Testing download URL: '{}'",
52                self.url
53            )
54        })
55    }
56}
57
58/// Extension trait for download slices, providing platform-based lookup.
59pub trait DownloadsByPlatform {
60    /// Returns the download entry for the given platform, if available.
61    fn for_platform(&self, platform: Platform) -> Option<&Download>;
62}
63
64impl DownloadsByPlatform for [Download] {
65    fn for_platform(&self, platform: Platform) -> Option<&Download> {
66        self.iter().find(|d| d.platform == platform)
67    }
68}
69
70/// Trait for types that contain a version identifier.
71pub trait HasVersion {
72    /// Returns the version identifier.
73    fn version(&self) -> Version;
74}
75
76impl HasVersion for known_good_versions::VersionWithoutChannel {
77    fn version(&self) -> Version {
78        self.version
79    }
80}
81
82impl HasVersion for last_known_good_versions::VersionInChannel {
83    fn version(&self) -> Version {
84        self.version
85    }
86}
87
88pub(crate) async fn fetch_endpoint<T>(
89    client: &reqwest::Client,
90    base_url: &Url,
91    path: &str,
92    endpoint_name: &str,
93) -> crate::Result<T>
94where
95    T: DeserializeOwned,
96{
97    let url = base_url.join(path).context_to::<Error>().attach_with(|| {
98        format!("while joining Chrome for Testing {endpoint_name} endpoint path: {path}")
99    })?;
100
101    let result = client
102        .get(url)
103        .send()
104        .await
105        .context_to::<Error>()
106        .attach_with(|| format!("while sending Chrome for Testing {endpoint_name} request"))?
107        .error_for_status()
108        .context_to::<Error>()?
109        .json::<T>()
110        .await
111        .context_to::<Error>()
112        .attach_with(|| {
113            format!("while deserializing Chrome for Testing {endpoint_name} response")
114        })?;
115
116    Ok(result)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use assertr::prelude::*;
123
124    #[test]
125    fn parsed_url_returns_typed_url() {
126        let download = Download {
127            platform: Platform::Linux64,
128            url: String::from("https://example.com/chrome.zip"),
129        };
130
131        assert_that!(download.parsed_url().map(|url| url.to_string()))
132            .is_ok()
133            .is_equal_to("https://example.com/chrome.zip");
134    }
135
136    #[test]
137    fn parsed_url_reports_invalid_urls() {
138        let download = Download {
139            platform: Platform::Linux64,
140            url: String::from("not a url"),
141        };
142
143        let err = download.parsed_url().unwrap_err();
144
145        let Error::UrlParsing(url_error) = err.current_context() else {
146            panic!("expected URL parse error, got: {:?}", err.current_context());
147        };
148
149        assert_that!(url_error.to_string()).contains("relative URL without a base");
150    }
151
152    #[tokio::test]
153    async fn fetch_endpoint_path_is_root_relative_when_base_url_has_path_prefix() {
154        let mut server = mockito::Server::new_async().await;
155        let endpoint_path = "/chrome-for-testing/test-endpoint.json";
156
157        let _mock = server
158            .mock("GET", endpoint_path)
159            .with_status(200)
160            .with_header("content-type", "application/json")
161            .with_body(r#"{"ok":true}"#)
162            .create();
163
164        let url: Url = format!("{}/prefix/", server.url()).parse().unwrap();
165
166        let data = fetch_endpoint::<serde_json::Value>(
167            &reqwest::Client::new(),
168            &url,
169            endpoint_path,
170            "TestEndpoint",
171        )
172        .await
173        .unwrap();
174
175        assert_that!(data["ok"].as_bool()).is_equal_to(Some(true));
176    }
177}