use crate::api::channel::Channel;
use crate::api::platform::Platform;
use crate::api::version::Version;
use crate::api::{API_BASE_URL, Download, DownloadsByPlatform, fetch_endpoint};
use serde::de::Error as DeError;
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::collections::HashMap;
const LAST_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS_JSON_PATH: &str =
"/chrome-for-testing/last-known-good-versions-with-downloads.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Downloads {
pub chrome: Vec<Download>,
pub chromedriver: Vec<Download>,
#[serde(rename = "chrome-headless-shell")]
pub chrome_headless_shell: Vec<Download>,
}
impl Downloads {
#[must_use]
pub fn chrome_for_platform(&self, platform: Platform) -> Option<&Download> {
self.chrome.for_platform(platform)
}
#[must_use]
pub fn chromedriver_for_platform(&self, platform: Platform) -> Option<&Download> {
self.chromedriver.for_platform(platform)
}
#[must_use]
pub fn chrome_headless_shell_for_platform(&self, platform: Platform) -> Option<&Download> {
self.chrome_headless_shell.for_platform(platform)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VersionInChannel {
pub channel: Channel,
pub version: Version,
pub revision: String,
pub downloads: Downloads,
}
fn deserialize_channels<'de, D>(
deserializer: D,
) -> Result<HashMap<Channel, VersionInChannel>, D::Error>
where
D: serde::Deserializer<'de>,
{
let channels = HashMap::<Channel, VersionInChannel>::deserialize(deserializer)?;
for (key, value) in &channels {
if key != &value.channel {
return Err(D::Error::custom(format!(
"expected channels.{key}.channel to be {key}, got {}",
value.channel
)));
}
}
Ok(channels)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LastKnownGoodVersions {
#[serde(with = "time::serde::rfc3339")]
pub timestamp: time::OffsetDateTime,
#[serde(deserialize_with = "deserialize_channels")]
channels: HashMap<Channel, VersionInChannel>,
}
impl LastKnownGoodVersions {
pub async fn fetch(client: &reqwest::Client) -> crate::Result<Self> {
Self::fetch_with_base_url(client, &API_BASE_URL).await
}
pub async fn fetch_with_base_url(
client: &reqwest::Client,
base_url: &reqwest::Url,
) -> crate::Result<LastKnownGoodVersions> {
fetch_endpoint::<Self>(
client,
base_url,
LAST_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS_JSON_PATH,
"LastKnownGoodVersions",
)
.await
}
#[must_use]
pub fn channel(&self, channel: impl Borrow<Channel>) -> Option<&VersionInChannel> {
self.channels.get(channel.borrow())
}
#[must_use]
pub fn channels(&self) -> &HashMap<Channel, VersionInChannel> {
&self.channels
}
#[must_use]
pub fn stable(&self) -> Option<&VersionInChannel> {
self.channel(Channel::Stable)
}
#[must_use]
pub fn beta(&self) -> Option<&VersionInChannel> {
self.channel(Channel::Beta)
}
#[must_use]
pub fn dev(&self) -> Option<&VersionInChannel> {
self.channel(Channel::Dev)
}
#[must_use]
pub fn canary(&self) -> Option<&VersionInChannel> {
self.channel(Channel::Canary)
}
}
#[cfg(test)]
mod tests {
use crate::api::Download;
use crate::api::channel::Channel;
use crate::api::last_known_good_versions::{
Downloads, LAST_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS_JSON_PATH, LastKnownGoodVersions,
VersionInChannel,
};
use crate::api::platform::Platform;
use crate::api::version::Version;
use crate::error::Error;
use assertr::prelude::*;
use std::collections::HashMap;
use time::macros::datetime;
use url::Url;
#[tokio::test]
async fn can_request_from_real_world_endpoint() {
let result = LastKnownGoodVersions::fetch(&reqwest::Client::new()).await;
assert_that!(result).is_ok();
}
#[tokio::test]
#[allow(clippy::too_many_lines)]
async fn can_query_last_known_good_versions_api_endpoint_and_deserialize_response() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", LAST_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS_JSON_PATH)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(include_str!(
"./../../test-data/last_known_good_versions_with_downloads_test_response.json"
))
.create();
let url: Url = server.url().parse().unwrap();
let data = LastKnownGoodVersions::fetch_with_base_url(&reqwest::Client::new(), &url)
.await
.unwrap();
assert_that!(data).is_equal_to(LastKnownGoodVersions {
timestamp: datetime!(2026-04-13 08:53:52.841 UTC),
channels: HashMap::from([
(
Channel::Stable,
VersionInChannel {
channel: Channel::Stable,
version: Version { major: 147, minor: 0, patch: 7727, build: 56 },
revision: String::from("1596535"),
downloads: Downloads {
chrome: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/linux64/chrome-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/mac-arm64/chrome-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/mac-x64/chrome-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/win32/chrome-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/win64/chrome-win64.zip") },
],
chromedriver: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/linux64/chromedriver-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/mac-arm64/chromedriver-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/mac-x64/chromedriver-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/win32/chromedriver-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/win64/chromedriver-win64.zip") },
],
chrome_headless_shell: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/linux64/chrome-headless-shell-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/mac-arm64/chrome-headless-shell-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/mac-x64/chrome-headless-shell-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/win32/chrome-headless-shell-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/147.0.7727.56/win64/chrome-headless-shell-win64.zip") },
],
},
}
),
(Channel::Beta, VersionInChannel {
channel: Channel::Beta,
version: Version { major: 148, minor: 0, patch: 7778, build: 5 },
revision: String::from("1610480"),
downloads: Downloads {
chrome: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/linux64/chrome-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/mac-arm64/chrome-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/mac-x64/chrome-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/win32/chrome-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/win64/chrome-win64.zip") },
],
chromedriver: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/linux64/chromedriver-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/mac-arm64/chromedriver-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/mac-x64/chromedriver-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/win32/chromedriver-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/win64/chromedriver-win64.zip") },
],
chrome_headless_shell: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/linux64/chrome-headless-shell-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/mac-arm64/chrome-headless-shell-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/mac-x64/chrome-headless-shell-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/win32/chrome-headless-shell-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7778.5/win64/chrome-headless-shell-win64.zip") },
],
},
}),
(Channel::Dev, VersionInChannel {
channel: Channel::Dev,
version: Version { major: 148, minor: 0, patch: 7766, build: 3 },
revision: String::from("1607787"),
downloads: Downloads {
chrome: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/linux64/chrome-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/mac-arm64/chrome-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/mac-x64/chrome-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/win32/chrome-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/win64/chrome-win64.zip") },
],
chromedriver: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/linux64/chromedriver-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/mac-arm64/chromedriver-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/mac-x64/chromedriver-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/win32/chromedriver-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/win64/chromedriver-win64.zip") },
],
chrome_headless_shell: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/linux64/chrome-headless-shell-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/mac-arm64/chrome-headless-shell-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/mac-x64/chrome-headless-shell-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/win32/chrome-headless-shell-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/148.0.7766.3/win64/chrome-headless-shell-win64.zip") },
],
},
}),
(Channel::Canary, VersionInChannel {
channel: Channel::Canary,
version: Version { major: 149, minor: 0, patch: 7789, build: 0 },
revision: String::from("1613465"),
downloads: Downloads {
chrome: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/linux64/chrome-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/mac-arm64/chrome-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/mac-x64/chrome-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/win32/chrome-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/win64/chrome-win64.zip") },
],
chromedriver: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/linux64/chromedriver-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/mac-arm64/chromedriver-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/mac-x64/chromedriver-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/win32/chromedriver-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/win64/chromedriver-win64.zip") },
],
chrome_headless_shell: vec![
Download { platform: Platform::Linux64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/linux64/chrome-headless-shell-linux64.zip") },
Download { platform: Platform::MacArm64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/mac-arm64/chrome-headless-shell-mac-arm64.zip") },
Download { platform: Platform::MacX64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/mac-x64/chrome-headless-shell-mac-x64.zip") },
Download { platform: Platform::Win32, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/win32/chrome-headless-shell-win32.zip") },
Download { platform: Platform::Win64, url: String::from("https://storage.googleapis.com/chrome-for-testing-public/149.0.7789.0/win64/chrome-headless-shell-win64.zip") },
],
},
}),
]),
});
}
#[tokio::test]
async fn unsuccessful_http_status_is_reported_as_request_error() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", LAST_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS_JSON_PATH)
.with_status(500)
.with_header("content-type", "application/json")
.with_body(include_str!(
"./../../test-data/last_known_good_versions_with_downloads_test_response.json"
))
.create();
let url: Url = server.url().parse().unwrap();
let err = LastKnownGoodVersions::fetch_with_base_url(&reqwest::Client::new(), &url)
.await
.unwrap_err();
let Error::Request(request_error) = err.current_context() else {
panic!("expected request error, got: {:?}", err.current_context());
};
assert_that!(request_error.status())
.is_equal_to(Some(reqwest::StatusCode::INTERNAL_SERVER_ERROR));
}
#[test]
fn deserialization_rejects_channel_mismatch() {
let json = include_str!(
"./../../test-data/last_known_good_versions_with_downloads_test_response.json"
)
.replacen(r#""channel": "Stable""#, r#""channel": "Beta""#, 1);
let result = serde_json::from_str::<LastKnownGoodVersions>(&json);
assert_that!(result)
.is_err()
.derive(|it| it.to_string())
.contains("expected channels.Stable.channel to be Stable, got Beta");
}
#[test]
fn deserialization_preserves_unknown_channels() {
let json = include_str!(
"./../../test-data/last_known_good_versions_with_downloads_test_response.json"
)
.replacen(r#""Canary": {"#, r#""Extended": {"#, 1)
.replacen(r#""channel": "Canary""#, r#""channel": "Extended""#, 1);
let data = serde_json::from_str::<LastKnownGoodVersions>(&json).unwrap();
let extended = Channel::Other(String::from("Extended"));
assert_that!(data.canary()).is_none();
assert_that!(data.channel(&extended))
.is_some()
.derive(|it| it.channel.clone())
.is_equal_to(extended);
}
}