181 lines
5.1 KiB
Rust
181 lines
5.1 KiB
Rust
use std::fs;
|
|
use std::io::{self, Cursor, Read};
|
|
use std::time::Duration;
|
|
|
|
use aim_core::app::add::{
|
|
InstallAppError, download_to_staged_path_with_retries,
|
|
stream_payload_to_staged_file_with_reporter,
|
|
};
|
|
use aim_core::app::progress::{NoopReporter, OperationEvent};
|
|
use aim_core::integration::install::{InstallRequest, execute_install};
|
|
use aim_core::platform::DesktopHelpers;
|
|
use aim_core::source::github::HttpClientPolicy;
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
fn payload_streaming_writes_staged_file_and_reports_progress() {
|
|
let root = tempdir().unwrap();
|
|
let staged_path = root.path().join("staging/bat.download");
|
|
let bytes = b"\x7fELFAppImage";
|
|
let mut reader = Cursor::new(bytes.as_slice());
|
|
let mut events = Vec::new();
|
|
let mut reporter = |event: &OperationEvent| events.push(event.clone());
|
|
|
|
let written = stream_payload_to_staged_file_with_reporter(
|
|
&mut reader,
|
|
Some(bytes.len() as u64),
|
|
&staged_path,
|
|
&mut reporter,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(written, bytes.len() as u64);
|
|
assert_eq!(
|
|
fs::metadata(&staged_path).unwrap().len(),
|
|
bytes.len() as u64
|
|
);
|
|
assert!(events.iter().any(|event| {
|
|
matches!(
|
|
event,
|
|
OperationEvent::Progress {
|
|
current,
|
|
total: Some(total)
|
|
} if *current == bytes.len() as u64 && *total == bytes.len() as u64
|
|
)
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn install_commits_from_staged_payload_path() {
|
|
let root = tempdir().unwrap();
|
|
let staged_path = root.path().join("staging/bat.download");
|
|
let final_payload_path = root.path().join("payloads/bat.AppImage");
|
|
fs::create_dir_all(staged_path.parent().unwrap()).unwrap();
|
|
fs::write(&staged_path, b"\x7fELFAppImage").unwrap();
|
|
|
|
let outcome = execute_install(&InstallRequest {
|
|
staged_payload_path: &staged_path,
|
|
final_payload_path: &final_payload_path,
|
|
trusted_checksum: None,
|
|
weak_checksum_md5: None,
|
|
desktop: None,
|
|
helpers: DesktopHelpers::default(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(outcome.final_payload_path, final_payload_path);
|
|
assert!(outcome.final_payload_path.exists());
|
|
assert!(!staged_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn failed_streaming_download_removes_partial_staged_payload() {
|
|
let root = tempdir().unwrap();
|
|
let staged_path = root.path().join("staging/bat.download");
|
|
let mut reader = FailingReader::new(b"\x7fELFpartial".to_vec(), 4);
|
|
let mut reporter = NoopReporter;
|
|
|
|
let result = stream_payload_to_staged_file_with_reporter(
|
|
&mut reader,
|
|
Some(12),
|
|
&staged_path,
|
|
&mut reporter,
|
|
);
|
|
|
|
assert!(result.is_err());
|
|
assert!(!staged_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn retry_policy_retries_transient_failures_before_success() {
|
|
let root = tempdir().unwrap();
|
|
let staged_path = root.path().join("staging/bat.download");
|
|
let bytes = b"\x7fELFAppImage";
|
|
let mut attempts = 0;
|
|
|
|
let written = download_to_staged_path_with_retries(
|
|
&staged_path,
|
|
&mut NoopReporter,
|
|
HttpClientPolicy {
|
|
timeout: Duration::from_secs(30),
|
|
max_retries: 3,
|
|
},
|
|
|| {
|
|
attempts += 1;
|
|
if attempts == 1 {
|
|
return Err(InstallAppError::DownloadIo(io::Error::other(
|
|
"transient failure",
|
|
)));
|
|
}
|
|
|
|
Ok((
|
|
Box::new(Cursor::new(bytes.to_vec())) as Box<dyn Read>,
|
|
Some(bytes.len() as u64),
|
|
))
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(attempts, 2);
|
|
assert_eq!(written, bytes.len() as u64);
|
|
assert!(staged_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn retry_exhaustion_returns_error_and_cleans_staged_payload() {
|
|
let root = tempdir().unwrap();
|
|
let staged_path = root.path().join("staging/bat.download");
|
|
let mut attempts = 0;
|
|
|
|
let result = download_to_staged_path_with_retries(
|
|
&staged_path,
|
|
&mut NoopReporter,
|
|
HttpClientPolicy {
|
|
timeout: Duration::from_secs(30),
|
|
max_retries: 2,
|
|
},
|
|
|| {
|
|
attempts += 1;
|
|
Ok((
|
|
Box::new(FailingReader::new(b"\x7fELFpartial".to_vec(), 4)) as Box<dyn Read>,
|
|
Some(12),
|
|
))
|
|
},
|
|
);
|
|
|
|
assert!(result.is_err());
|
|
assert_eq!(attempts, 2);
|
|
assert!(!staged_path.exists());
|
|
}
|
|
|
|
struct FailingReader {
|
|
bytes: Vec<u8>,
|
|
chunk_size: usize,
|
|
position: usize,
|
|
}
|
|
|
|
impl FailingReader {
|
|
fn new(bytes: Vec<u8>, chunk_size: usize) -> Self {
|
|
Self {
|
|
bytes,
|
|
chunk_size,
|
|
position: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Read for FailingReader {
|
|
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
|
|
if self.position >= self.chunk_size {
|
|
return Err(io::Error::other("fixture read failure"));
|
|
}
|
|
|
|
let remaining = self.chunk_size - self.position;
|
|
let to_read = remaining
|
|
.min(buffer.len())
|
|
.min(self.bytes.len() - self.position);
|
|
buffer[..to_read].copy_from_slice(&self.bytes[self.position..self.position + to_read]);
|
|
self.position += to_read;
|
|
Ok(to_read)
|
|
}
|
|
}
|