diff --git a/axoupdater-cli/tests/integration.rs b/axoupdater-cli/tests/integration.rs index bcc964f..3781793 100644 --- a/axoupdater-cli/tests/integration.rs +++ b/axoupdater-cli/tests/integration.rs @@ -297,3 +297,73 @@ fn test_downgrade_to_specific_version() -> std::io::Result<()> { Ok(()) } + +// A similar test to the one above, but it upgrades to a significantly older +// version that's outside the GitHub API's 30 versions. +// This version isn't available for every target, so we only run it for +// certain target triples. +#[test] +fn test_downgrade_to_specific_old_version() -> std::io::Result<()> { + // Only available for x86_64 Darwin and x86_64 Linux + match std::env::consts::OS { + "linux" | "macos" => { + if std::env::consts::ARCH != "x86_64" { + return Ok(()); + } + } + "windows" => return Ok(()), + _ => return Ok(()), + } + + let tempdir = TempDir::new()?; + let bindir_path = &tempdir.path().join("bin"); + let bindir = Utf8Path::from_path(bindir_path).unwrap(); + std::fs::create_dir_all(bindir)?; + + let base_version = "0.2.116"; + let target_version = "0.2.50"; + + let url = axolotlsay_tarball_path(base_version); + + let mut response = reqwest::blocking::get(url) + .unwrap() + .error_for_status() + .unwrap(); + + let compressed_path = + Utf8PathBuf::from_path_buf(tempdir.path().join("axolotlsay.tar.gz")).unwrap(); + let mut contents = vec![]; + response.read_to_end(&mut contents)?; + std::fs::write(&compressed_path, contents)?; + + // Write the receipt for the updater to use + write_receipt(base_version, &bindir.to_path_buf())?; + + LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap(); + + // Now install our copy of the updater instead of the one axolotlsay came with + let updater_path = bindir.join(format!("axolotlsay-update{EXE_SUFFIX}")); + std::fs::copy(BIN, &updater_path)?; + + let mut updater = Cmd::new(&updater_path, "run updater"); + updater.arg("--version").arg(target_version); + updater.env("AXOUPDATER_CONFIG_PATH", bindir); + // This installer is so old it doesn't respect the install path, so we + // have to set CARGO_HOME to force it. + updater.env("CARGO_HOME", tempdir.path()); + // We'll do that manually + updater.check(false); + let result = updater.output(); + assert!(result.is_ok()); + + // Now let's check the version we just updated to + let new_axolotlsay_path = &bindir.join(format!("axolotlsay{EXE_SUFFIX}")); + assert!(new_axolotlsay_path.exists()); + let mut new_axolotlsay = Cmd::new(new_axolotlsay_path, "version test"); + new_axolotlsay.arg("--version"); + let output = new_axolotlsay.output().unwrap(); + let stderr_string = String::from_utf8(output.stdout).unwrap(); + assert_eq!(stderr_string, format!("axolotlsay {}\n", target_version)); + + Ok(()) +} diff --git a/axoupdater/src/lib.rs b/axoupdater/src/lib.rs index 155af45..3dd56b5 100644 --- a/axoupdater/src/lib.rs +++ b/axoupdater/src/lib.rs @@ -877,33 +877,86 @@ async fn get_specific_github_version( } #[cfg(feature = "github_releases")] -async fn get_github_releases( +async fn get_releases( + client: &reqwest::Client, + url: &str, name: &str, - owner: &str, app_name: &str, -) -> AxoupdateResult> { - let client = reqwest::Client::new(); - let resp: Vec = client - .get(format!("{GITHUB_API}/repos/{owner}/{name}/releases")) +) -> AxoupdateResult { + client + .get(url) .header(ACCEPT, "application/json") .header( USER_AGENT, format!("axoupdate/{}", env!("CARGO_PKG_VERSION")), ) + .header("X-GitHub-Api-Version", "2022-11-28") .send() .await? .error_for_status() .map_err(|_| AxoupdateError::ReleaseNotFound { name: name.to_owned(), app_name: app_name.to_owned(), - })? - .json::>() - .await? - .into_iter() - .filter_map(|gh| Release::try_from_github(app_name, gh).ok()) - .collect(); + }) +} + +// The format of the header looks like so: +// ``` +// ; rel="prev", ; rel="next", ; rel="last", ; rel="first" +// ``` +#[cfg(feature = "github_releases")] +fn get_next_url(link_header: &str) -> Option { + let links = link_header.split(',').collect::>(); + for entry in links { + if entry.contains("next") { + let mut link = entry.split(';').collect::>()[0] + .to_string() + .trim() + .to_string(); + link.remove(0); + link.pop(); + return Some(link); + } + } + None +} + +#[cfg(feature = "github_releases")] +async fn get_github_releases( + name: &str, + owner: &str, + app_name: &str, +) -> AxoupdateResult> { + let client = reqwest::Client::new(); + let mut url = format!("{GITHUB_API}/repos/{owner}/{name}/releases"); + let mut pages_remain = true; + let mut data: Vec = vec![]; + + while pages_remain { + let resp = get_releases(&client, &url, name, app_name).await?; + + let headers = resp.headers(); + let link_header = &headers[reqwest::header::LINK] + .to_str() + .expect("header was not ascii") + .to_string(); + pages_remain = link_header.contains("rel=\"next\""); + + let mut body: Vec = resp + .json::>() + .await? + .into_iter() + .filter_map(|gh| Release::try_from_github(app_name, gh).ok()) + .collect(); + data.append(&mut body); + dbg!(&data); + + if pages_remain { + url = get_next_url(link_header).expect("detected a next but it was a lie"); + } + } - Ok(resp + Ok(data .into_iter() .filter(|r| { r.assets @@ -1142,3 +1195,35 @@ fn load_receipt_for(app_name: &str) -> AxoupdateResult { app_name: app_name.to_owned(), }) } + +#[test] +fn test_link_header_parse() { + let sample = r#" +; rel="prev", ; rel="next", ; rel="last", ; rel="first" +"#; + + let result = get_next_url(sample); + assert!(result.is_some()); + assert_eq!( + "https://api.github.com/repositories/1300192/issues?page=4", + result.unwrap() + ); +} + +#[test] +fn test_link_header_parse_next_missing() { + let sample = r#" +; rel="prev", ; rel="last", ; rel="first" +"#; + + let result = get_next_url(sample); + assert!(result.is_none()); +} + +#[test] +fn test_link_header_parse_empty_header() { + let sample = ""; + + let result = get_next_url(sample); + assert!(result.is_none()); +}