Merge branch 'feat/cli-ux-progress'
This commit is contained in:
commit
27a1b806cd
44 changed files with 4995 additions and 106 deletions
95
crates/aim-core/tests/checksum_verification.rs
Normal file
95
crates/aim-core/tests/checksum_verification.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use std::fs;
|
||||
|
||||
use aim_core::integration::install::{InstallRequest, PayloadInstallError, execute_install};
|
||||
use aim_core::platform::DesktopHelpers;
|
||||
use tempfile::tempdir;
|
||||
|
||||
const VALID_FIXTURE_SHA512: &str =
|
||||
"ZZma4ZD+9XB4GGTHCNZu8I92OY02YrEvIG89ZtRNi99W8SZKwWkmGZz/QyNBxqAt0XeiKtcR80/dMnKlwpcIWw==";
|
||||
|
||||
#[test]
|
||||
fn install_succeeds_with_valid_trusted_checksum() {
|
||||
let root = tempdir().unwrap();
|
||||
let staged_path = write_staged_payload(
|
||||
root.path(),
|
||||
b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
|
||||
);
|
||||
let final_payload_path = root.path().join("payloads/bat.AppImage");
|
||||
|
||||
let outcome = execute_install(&InstallRequest {
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &final_payload_path,
|
||||
trusted_checksum: Some(VALID_FIXTURE_SHA512),
|
||||
desktop: None,
|
||||
helpers: DesktopHelpers::default(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(outcome.final_payload_path, final_payload_path);
|
||||
assert!(outcome.final_payload_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_succeeds_without_trusted_checksum() {
|
||||
let root = tempdir().unwrap();
|
||||
let staged_path = write_staged_payload(root.path(), b"\x7fELFAppImage");
|
||||
let final_payload_path = root.path().join("payloads/bat.AppImage");
|
||||
|
||||
let outcome = execute_install(&InstallRequest {
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &final_payload_path,
|
||||
trusted_checksum: None,
|
||||
desktop: None,
|
||||
helpers: DesktopHelpers::default(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(outcome.final_payload_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_fails_before_commit_when_trusted_checksum_mismatches() {
|
||||
let root = tempdir().unwrap();
|
||||
let staged_path = write_staged_payload(root.path(), b"\x7fELFAppImage");
|
||||
let final_payload_path = root.path().join("payloads/bat.AppImage");
|
||||
|
||||
let error = execute_install(&InstallRequest {
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &final_payload_path,
|
||||
trusted_checksum: Some(VALID_FIXTURE_SHA512),
|
||||
desktop: None,
|
||||
helpers: DesktopHelpers::default(),
|
||||
})
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, PayloadInstallError::ChecksumMismatch));
|
||||
assert!(!final_payload_path.exists());
|
||||
assert!(!staged_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_trusted_checksum_fails_before_commit() {
|
||||
let root = tempdir().unwrap();
|
||||
let staged_path = write_staged_payload(root.path(), b"\x7fELFAppImage");
|
||||
let final_payload_path = root.path().join("payloads/bat.AppImage");
|
||||
|
||||
let error = execute_install(&InstallRequest {
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &final_payload_path,
|
||||
trusted_checksum: Some("not-base64"),
|
||||
desktop: None,
|
||||
helpers: DesktopHelpers::default(),
|
||||
})
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, PayloadInstallError::InvalidTrustedChecksum));
|
||||
assert!(!final_payload_path.exists());
|
||||
assert!(!staged_path.exists());
|
||||
}
|
||||
|
||||
fn write_staged_payload(root: &std::path::Path, bytes: &[u8]) -> std::path::PathBuf {
|
||||
let staged_path = root.join("staging/bat.download");
|
||||
fs::create_dir_all(staged_path.parent().unwrap()).unwrap();
|
||||
fs::write(&staged_path, bytes).unwrap();
|
||||
staged_path
|
||||
}
|
||||
180
crates/aim-core/tests/download_pipeline.rs
Normal file
180
crates/aim-core/tests/download_pipeline.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
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,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
use aim_core::app::query::resolve_query;
|
||||
use aim_core::source::github::{FixtureGitHubTransport, discover_github_candidates_with};
|
||||
use aim_core::source::github::{
|
||||
FixtureGitHubTransport, discover_github_candidates_with, http_client_policy,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn discovery_reports_appimage_assets_and_latest_linux_yml() {
|
||||
|
|
@ -31,3 +34,11 @@ fn discovery_marks_explicit_older_release_against_latest_fixture_release() {
|
|||
assert_eq!(discovery.releases[0].tag, "v0.0.12");
|
||||
assert!(discovery.requested_is_older_release);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_http_policy_uses_explicit_timeout_and_retry_defaults() {
|
||||
let policy = http_client_policy();
|
||||
|
||||
assert_eq!(policy.timeout, Duration::from_secs(30));
|
||||
assert_eq!(policy.max_retries, 3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,15 @@ fn integration_failure_removes_new_payload_and_generated_files() {
|
|||
fs::create_dir(&staging_root).unwrap();
|
||||
fs::create_dir(&payload_root).unwrap();
|
||||
fs::write(&blocking_path, "blocker").unwrap();
|
||||
let staged_path = staging_root.join("bat.download");
|
||||
fs::write(&staged_path, b"\x7fELFAppImage").unwrap();
|
||||
|
||||
let final_payload_path = payload_root.join("bat.AppImage");
|
||||
let desktop_entry_path = blocking_path.join("aim-bat.desktop");
|
||||
let error = execute_install(&InstallRequest {
|
||||
staging_root: &staging_root,
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &final_payload_path,
|
||||
artifact_bytes: b"\x7fELFAppImage",
|
||||
trusted_checksum: None,
|
||||
desktop: Some(DesktopIntegrationRequest {
|
||||
desktop_entry_path: &desktop_entry_path,
|
||||
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
|
||||
|
|
|
|||
|
|
@ -9,21 +9,27 @@ use std::fs;
|
|||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn write_staged_payload(root: &std::path::Path, name: &str, bytes: &[u8]) -> std::path::PathBuf {
|
||||
let staged_path = root.join("staging").join(format!("{name}.download"));
|
||||
fs::create_dir_all(staged_path.parent().unwrap()).unwrap();
|
||||
fs::write(&staged_path, bytes).unwrap();
|
||||
staged_path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_writes_desktop_entry_and_reports_refresh_warning_only() {
|
||||
let root = tempdir().unwrap();
|
||||
let staging_root = root.path().join("staging");
|
||||
let payload_root = root.path().join("payloads");
|
||||
let desktop_root = root.path().join("applications");
|
||||
|
||||
fs::create_dir(&staging_root).unwrap();
|
||||
fs::create_dir(&payload_root).unwrap();
|
||||
fs::create_dir(&desktop_root).unwrap();
|
||||
let staged_path = write_staged_payload(root.path(), "bat", b"\x7fELFAppImage");
|
||||
|
||||
let outcome = execute_install(&InstallRequest {
|
||||
staging_root: &staging_root,
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &payload_root.join("bat.AppImage"),
|
||||
artifact_bytes: b"\x7fELFAppImage",
|
||||
trusted_checksum: None,
|
||||
desktop: Some(DesktopIntegrationRequest {
|
||||
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
|
||||
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
|
||||
|
|
@ -41,16 +47,19 @@ fn install_writes_desktop_entry_and_reports_refresh_warning_only() {
|
|||
#[test]
|
||||
fn install_executes_refresh_helpers_when_available() {
|
||||
let root = tempdir().unwrap();
|
||||
let staging_root = root.path().join("staging");
|
||||
let payload_root = root.path().join("payloads");
|
||||
let desktop_root = root.path().join("applications");
|
||||
let helper_root = root.path().join("helpers");
|
||||
let log_path = root.path().join("helpers.log");
|
||||
|
||||
fs::create_dir(&staging_root).unwrap();
|
||||
fs::create_dir(&payload_root).unwrap();
|
||||
fs::create_dir(&desktop_root).unwrap();
|
||||
fs::create_dir(&helper_root).unwrap();
|
||||
let staged_path = write_staged_payload(
|
||||
root.path(),
|
||||
"bat",
|
||||
b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
|
||||
);
|
||||
|
||||
let update_helper = helper_root.join("update-desktop-database");
|
||||
let icon_helper = helper_root.join("gtk-update-icon-cache");
|
||||
|
|
@ -71,9 +80,9 @@ fn install_executes_refresh_helpers_when_available() {
|
|||
fs::create_dir_all(&icon_root).unwrap();
|
||||
|
||||
let outcome = execute_install(&InstallRequest {
|
||||
staging_root: &staging_root,
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &payload_root.join("bat.AppImage"),
|
||||
artifact_bytes: b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
|
||||
trusted_checksum: None,
|
||||
desktop: Some(DesktopIntegrationRequest {
|
||||
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
|
||||
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
|
||||
|
|
@ -98,20 +107,23 @@ fn install_executes_refresh_helpers_when_available() {
|
|||
#[test]
|
||||
fn install_extracts_icon_from_appimage_payload_when_icon_path_is_requested() {
|
||||
let root = tempdir().unwrap();
|
||||
let staging_root = root.path().join("staging");
|
||||
let payload_root = root.path().join("payloads");
|
||||
let desktop_root = root.path().join("applications");
|
||||
let icon_root = root.path().join("icons/hicolor/256x256/apps");
|
||||
|
||||
fs::create_dir(&staging_root).unwrap();
|
||||
fs::create_dir(&payload_root).unwrap();
|
||||
fs::create_dir(&desktop_root).unwrap();
|
||||
fs::create_dir_all(&icon_root).unwrap();
|
||||
let staged_path = write_staged_payload(
|
||||
root.path(),
|
||||
"bat",
|
||||
b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
|
||||
);
|
||||
|
||||
let outcome = execute_install(&InstallRequest {
|
||||
staging_root: &staging_root,
|
||||
staged_payload_path: &staged_path,
|
||||
final_payload_path: &payload_root.join("bat.AppImage"),
|
||||
artifact_bytes: b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82",
|
||||
trusted_checksum: None,
|
||||
desktop: Some(DesktopIntegrationRequest {
|
||||
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
|
||||
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ fn payload_commit_moves_staged_appimage_into_final_location() {
|
|||
fs::create_dir(&staging_root).unwrap();
|
||||
fs::create_dir(&payload_root).unwrap();
|
||||
|
||||
let staged_path = staging_root.join("bat.download");
|
||||
fs::write(&staged_path, b"\x7fELFAppImage").unwrap();
|
||||
let final_payload_path = payload_root.join("bat.AppImage");
|
||||
let outcome =
|
||||
stage_and_commit_payload(&staging_root, &final_payload_path, b"\x7fELFAppImage").unwrap();
|
||||
let outcome = stage_and_commit_payload(&staged_path, &final_payload_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outcome
|
||||
|
|
|
|||
|
|
@ -205,3 +205,84 @@ fn registry_round_trips_source_identity_for_new_provider_kinds() {
|
|||
"https://example.com/downloads/team-app.AppImage"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_save_is_atomic_and_cleans_up_temp_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let store = RegistryStore::new(registry_path.clone());
|
||||
|
||||
store
|
||||
.save(&aim_core::registry::model::Registry {
|
||||
version: 1,
|
||||
apps: vec![aim_core::domain::app::AppRecord {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
source_input: None,
|
||||
source: None,
|
||||
installed_version: None,
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: None,
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(registry_path.exists());
|
||||
assert!(!dir.path().join("registry.toml.tmp").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_exclusive_lock_rejects_second_mutator() {
|
||||
let dir = tempdir().unwrap();
|
||||
let store = RegistryStore::new(dir.path().join("registry.toml"));
|
||||
let _guard = store.lock_exclusive().unwrap();
|
||||
|
||||
let error = store.lock_exclusive().unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
aim_core::registry::store::RegistryStoreError::LockUnavailable
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_mutate_exclusive_reloads_and_writes_latest_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let store = RegistryStore::new(dir.path().join("registry.toml"));
|
||||
store
|
||||
.save(&aim_core::registry::model::Registry {
|
||||
version: 1,
|
||||
apps: vec![aim_core::domain::app::AppRecord {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
source_input: None,
|
||||
source: None,
|
||||
installed_version: None,
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: None,
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
store
|
||||
.mutate_exclusive(|registry| {
|
||||
registry.apps.push(aim_core::domain::app::AppRecord {
|
||||
stable_id: "t3code".to_owned(),
|
||||
display_name: "T3 Code".to_owned(),
|
||||
source_input: None,
|
||||
source: None,
|
||||
installed_version: None,
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: None,
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let loaded = store.load().unwrap();
|
||||
assert_eq!(loaded.apps.len(), 2);
|
||||
assert_eq!(loaded.apps[0].stable_id, "bat");
|
||||
assert_eq!(loaded.apps[1].stable_id, "t3code");
|
||||
}
|
||||
|
|
|
|||
212
crates/aim-core/tests/search_github.rs
Normal file
212
crates/aim-core/tests/search_github.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
use aim_core::app::search::{
|
||||
GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with,
|
||||
};
|
||||
use aim_core::domain::app::AppRecord;
|
||||
use aim_core::domain::search::{SearchInstallStatus, SearchQuery};
|
||||
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
|
||||
use aim_core::source::github::{FixtureGitHubTransport, search_github_repositories_with};
|
||||
|
||||
#[test]
|
||||
fn github_fixtures_return_normalized_remote_hits() {
|
||||
let query = SearchQuery::new("bat");
|
||||
let provider = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||
|
||||
let results = build_search_results_with(&query, &[], &[&provider]).unwrap();
|
||||
|
||||
assert_eq!(query.remote_limit, 10);
|
||||
assert!(results.installed_matches.is_empty());
|
||||
assert!(results.warnings.is_empty());
|
||||
assert_eq!(results.remote_hits.len(), 3);
|
||||
|
||||
let first = &results.remote_hits[0];
|
||||
assert_eq!(first.provider_id, "github");
|
||||
assert_eq!(first.display_name, "sharkdp/bat");
|
||||
assert_eq!(
|
||||
first.description.as_deref(),
|
||||
Some("A cat(1) clone with wings.")
|
||||
);
|
||||
assert_eq!(first.source_locator, "https://github.com/sharkdp/bat");
|
||||
assert_eq!(first.install_query, "sharkdp/bat");
|
||||
assert_eq!(first.canonical_locator, "sharkdp/bat");
|
||||
assert_eq!(first.version.as_deref(), Some("1.0.0"));
|
||||
assert_eq!(first.install_status, SearchInstallStatus::Available);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_search_respects_limit_and_fixture_order() {
|
||||
let query = SearchQuery::with_remote_limit("bat", 2);
|
||||
let provider = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||
|
||||
let results = build_search_results_with(&query, &[], &[&provider]).unwrap();
|
||||
|
||||
let locators = results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.map(|hit| hit.canonical_locator.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(locators, vec!["sharkdp/bat", "astatine/bat"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_search_ranks_full_name_matches_above_description_only_matches() {
|
||||
let query = SearchQuery::new("pingdotgg");
|
||||
let provider = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||
|
||||
let results = build_search_results_with(&query, &[], &[&provider]).unwrap();
|
||||
|
||||
let locators = results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.map(|hit| hit.canonical_locator.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(locators[0], "pingdotgg/t3code");
|
||||
assert_eq!(locators, vec!["pingdotgg/t3code"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_search_backfills_description_matches_after_name_matches() {
|
||||
let query = SearchQuery::with_remote_limit("pingdotgg", 3);
|
||||
let provider = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||
|
||||
let results = build_search_results_with(&query, &[], &[&provider]).unwrap();
|
||||
|
||||
let locators = results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.map(|hit| hit.canonical_locator.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(locators, vec!["pingdotgg/t3code"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_search_only_returns_repositories_with_appimage_release_assets() {
|
||||
let query = SearchQuery::new("pingdotgg");
|
||||
let provider = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||
|
||||
let results = build_search_results_with(&query, &[], &[&provider]).unwrap();
|
||||
|
||||
assert!(
|
||||
results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.all(|hit| hit.canonical_locator == "pingdotgg/t3code")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_name_only_search_excludes_description_only_matches() {
|
||||
let hits =
|
||||
search_github_repositories_with("pingdotgg in:name", 10, &FixtureGitHubTransport).unwrap();
|
||||
|
||||
let locators = hits
|
||||
.iter()
|
||||
.map(|hit| hit.full_name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(locators, vec!["pingdotgg/t3code"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_search_results_can_carry_local_matches_and_warnings() {
|
||||
let query = SearchQuery::new("bat");
|
||||
let installed = vec![AppRecord {
|
||||
stable_id: "bat".to_owned(),
|
||||
display_name: "Bat".to_owned(),
|
||||
source_input: None,
|
||||
source: None,
|
||||
installed_version: Some("1.0.0".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: None,
|
||||
}];
|
||||
let provider = FailingProvider;
|
||||
|
||||
let results = build_search_results_with(&query, &installed, &[&provider]).unwrap();
|
||||
|
||||
assert!(results.remote_hits.is_empty());
|
||||
assert_eq!(results.installed_matches.len(), 1);
|
||||
assert_eq!(results.installed_matches[0].stable_id, "bat");
|
||||
assert_eq!(results.installed_matches[0].display_name, "Bat");
|
||||
assert_eq!(results.warnings.len(), 1);
|
||||
assert_eq!(results.warnings[0].provider_id.as_deref(), Some("github"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_search_marks_matching_current_install_as_installed() {
|
||||
let query = SearchQuery::new("bat");
|
||||
let installed = vec![installed_github_app("sharkdp/bat", "1.0.0")];
|
||||
let provider = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||
|
||||
let results = build_search_results_with(&query, &installed, &[&provider]).unwrap();
|
||||
let bat = results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.find(|hit| hit.install_query == "sharkdp/bat")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
bat.install_status,
|
||||
SearchInstallStatus::Installed {
|
||||
installed_version: Some("1.0.0".to_owned()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_search_marks_older_install_as_update_available() {
|
||||
let query = SearchQuery::new("pingdotgg");
|
||||
let installed = vec![installed_github_app("pingdotgg/t3code", "0.0.11")];
|
||||
let provider = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||
|
||||
let results = build_search_results_with(&query, &installed, &[&provider]).unwrap();
|
||||
let t3code = results
|
||||
.remote_hits
|
||||
.iter()
|
||||
.find(|hit| hit.install_query == "pingdotgg/t3code")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(t3code.version.as_deref(), Some("0.0.12"));
|
||||
assert_eq!(
|
||||
t3code.install_status,
|
||||
SearchInstallStatus::UpdateAvailable {
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
latest_version: Some("0.0.12".to_owned()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn installed_github_app(locator: &str, installed_version: &str) -> AppRecord {
|
||||
AppRecord {
|
||||
stable_id: locator.replace('/', "-"),
|
||||
display_name: locator.split('/').next_back().unwrap().to_owned(),
|
||||
source_input: Some(locator.to_owned()),
|
||||
source: Some(SourceRef {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: locator.to_owned(),
|
||||
input_kind: SourceInputKind::RepoShorthand,
|
||||
normalized_kind: NormalizedSourceKind::GitHubRepository,
|
||||
canonical_locator: Some(locator.to_owned()),
|
||||
requested_tag: None,
|
||||
requested_asset_name: None,
|
||||
tracks_latest: true,
|
||||
}),
|
||||
installed_version: Some(installed_version.to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: None,
|
||||
}
|
||||
}
|
||||
|
||||
struct FailingProvider;
|
||||
|
||||
impl SearchProvider for FailingProvider {
|
||||
fn search(
|
||||
&self,
|
||||
_query: &SearchQuery,
|
||||
) -> Result<Vec<aim_core::domain::search::SearchResult>, SearchProviderError> {
|
||||
Err(SearchProviderError::new("github", "fixture rate limit"))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue