chrome_for_testing/api/
mod.rs1use 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
10pub mod channel;
12
13pub mod platform;
15
16pub mod version;
18
19pub mod known_good_versions;
21
22pub mod last_known_good_versions;
24
25pub static API_BASE_URL: LazyLock<Url> =
30 LazyLock::new(|| Url::parse("https://googlechromelabs.github.io").expect("Valid URL"));
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub struct Download {
35 pub platform: Platform,
37
38 pub url: String,
40}
41
42impl Download {
43 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
58pub trait DownloadsByPlatform {
60 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
70pub trait HasVersion {
72 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}