use tempfile::tempdir;
use super::*;
#[test]
fn part_path_uses_dot_part_suffix() {
let path = Path::new("/tmp/rootfs-x86_64-alpine.img.tar.gz");
assert_eq!(
part_path(path),
PathBuf::from("/tmp/rootfs-x86_64-alpine.img.tar.gz.part")
);
}
#[test]
fn lock_path_uses_dot_lock_suffix() {
let path = Path::new("/tmp/rootfs-x86_64-alpine.img.tar.gz");
assert_eq!(
lock_path(path),
PathBuf::from("/tmp/rootfs-x86_64-alpine.img.tar.gz.lock")
);
}
#[test]
fn github_release_asset_ref_parses_release_download_url() {
let asset_ref = github_release_asset_ref(
"https://github.com/rcore-os/tgosimages/releases/download/v0.0.8/rootfs-loongarch64-alpine.img.tar.xz",
)
.unwrap();
assert_eq!(
asset_ref.api_url,
"https://api.github.com/repos/rcore-os/tgosimages/releases/tags/v0.0.8"
);
assert_eq!(asset_ref.asset_name, "rootfs-loongarch64-alpine.img.tar.xz");
}
#[test]
fn github_release_asset_ref_rejects_non_release_download_url() {
assert!(github_release_asset_ref("https://example.com/rootfs.tar.xz").is_none());
assert!(github_release_asset_ref("https://github.com/rcore-os/tgosimages").is_none());
}
#[test]
fn classify_download_sha256_accepts_matching_github_asset_digest() {
let actual = "25443ad55c76d810532f24f6868ce66923f58b423b3d6253cec03e8c4cc4c882";
let stale_registry = "67dd38677005c55bd5e27062bb885cb3962061729d3ca933faa146dc5d17f6b9";
assert_eq!(
classify_download_sha256(actual, stale_registry, Some(actual)),
VerifyOutcome::MatchedGitHubAsset
);
}
#[tokio::test]
async fn recoverable_lock_accepts_dead_process_pid() {
let workspace = tempdir().unwrap();
let lock_path = workspace.path().join("download.lock");
fs::write(&lock_path, "pid=999999\n").unwrap();
assert!(recoverable_lock(&lock_path).await.unwrap());
}
#[tokio::test]
async fn download_file_resumes_partial_download() {
let server = TestServer::start_with_range_support(b"abcdef".to_vec(), true).await;
let workspace = tempdir().unwrap();
let output_path = workspace.path().join("rootfs.img.tar.gz");
let part_path = part_path(&output_path);
fs::write(&part_path, b"abc").unwrap();
let client = http_client().unwrap();
download_file(&client, &server.url(), &output_path)
.await
.unwrap();
assert_eq!(fs::read(&output_path).unwrap(), b"abcdef");
assert_eq!(server.last_range_header().as_deref(), Some("bytes=3-"));
}
#[tokio::test]
async fn download_file_restarts_when_range_is_ignored() {
let server = TestServer::start_with_range_support(b"abcdef".to_vec(), false).await;
let workspace = tempdir().unwrap();
let output_path = workspace.path().join("rootfs.img.tar.gz");
let part_path = part_path(&output_path);
fs::write(&part_path, b"abc").unwrap();
let client = http_client().unwrap();
download_file(&client, &server.url(), &output_path)
.await
.unwrap();
assert_eq!(fs::read(&output_path).unwrap(), b"abcdef");
assert_eq!(server.last_range_header().as_deref(), Some("bytes=3-"));
}
#[tokio::test]
async fn download_file_restarts_when_range_is_invalid() {
let server = TestServer::start_with_invalid_range(b"abcdef".to_vec()).await;
let workspace = tempdir().unwrap();
let output_path = workspace.path().join("rootfs.img.tar.gz");
let part_path = part_path(&output_path);
fs::write(&part_path, b"abcdefghi").unwrap();
let client = http_client().unwrap();
download_file(&client, &server.url(), &output_path)
.await
.unwrap();
assert_eq!(fs::read(&output_path).unwrap(), b"abcdef");
assert_eq!(server.request_count(), 2);
}
#[tokio::test]
async fn download_file_retries_transient_http_status() {
let server = TestServer::start_with_failures(
b"abcdef".to_vec(),
vec![StatusCode::TOO_MANY_REQUESTS, StatusCode::GATEWAY_TIMEOUT],
)
.await;
let workspace = tempdir().unwrap();
let output_path = workspace.path().join("rootfs.img.tar.gz");
let client = http_client().unwrap();
download_file(&client, &server.url(), &output_path)
.await
.unwrap();
assert_eq!(fs::read(&output_path).unwrap(), b"abcdef");
assert_eq!(server.request_count(), 3);
}
#[tokio::test]
async fn download_file_does_not_retry_permanent_http_status() {
let server =
TestServer::start_with_failures(b"abcdef".to_vec(), vec![StatusCode::NOT_FOUND]).await;
let workspace = tempdir().unwrap();
let output_path = workspace.path().join("rootfs.img.tar.gz");
let client = http_client().unwrap();
let err = download_file(&client, &server.url(), &output_path)
.await
.unwrap_err();
assert!(err.to_string().contains("HTTP 404 Not Found"));
assert_eq!(server.request_count(), 1);
}
struct TestServer {
handle: test_support::MockHandle,
}
impl TestServer {
async fn start_with_failures(body: Vec<u8>, statuses: Vec<StatusCode>) -> Self {
Self {
handle: test_support::register_download_with_failures(
"rootfs.img.tar.gz",
body,
test_support::MockRangeMode::Ignore,
statuses,
),
}
}
async fn start_with_invalid_range(body: Vec<u8>) -> Self {
Self::start_inner(body, RangeMode::RejectInvalid).await
}
async fn start_with_range_support(body: Vec<u8>, support_range: bool) -> Self {
let mode = if support_range {
RangeMode::Support
} else {
RangeMode::Ignore
};
Self::start_inner(body, mode).await
}
async fn start_inner(body: Vec<u8>, range_mode: RangeMode) -> Self {
let range_mode = match range_mode {
RangeMode::Ignore => test_support::MockRangeMode::Ignore,
RangeMode::Support => test_support::MockRangeMode::Support,
RangeMode::RejectInvalid => test_support::MockRangeMode::RejectInvalid,
};
Self {
handle: test_support::register_download("rootfs.img.tar.gz", body, range_mode),
}
}
fn url(&self) -> String {
self.handle.url().to_string()
}
fn request_count(&self) -> usize {
self.handle.request_count()
}
fn last_range_header(&self) -> Option<String> {
self.handle.last_range_header()
}
}
#[derive(Clone, Copy)]
enum RangeMode {
Ignore,
Support,
RejectInvalid,
}