use kopi::{
cache::MetadataCache,
config::KopiConfig,
error::{KopiError, Result},
indicator::{ProgressIndicator, SilentProgress},
metadata::{
MetadataSource, local::LocalDirectorySource, provider::MetadataProvider,
source::PackageDetails,
},
models::{
metadata::JdkMetadata,
package::{ArchiveType, ChecksumType, PackageType},
platform::{Architecture, OperatingSystem},
},
version::Version,
};
use std::{
sync::{Arc, Mutex},
time::Duration,
};
use tempfile::TempDir;
struct MockMetadataSource {
should_fail: Arc<Mutex<bool>>,
call_count: Arc<Mutex<usize>>,
metadata: Vec<JdkMetadata>,
}
impl MockMetadataSource {
fn new(metadata: Vec<JdkMetadata>) -> Self {
Self {
should_fail: Arc::new(Mutex::new(false)),
call_count: Arc::new(Mutex::new(0)),
metadata,
}
}
fn set_should_fail(&self, should_fail: bool) {
*self.should_fail.lock().unwrap() = should_fail;
}
#[allow(dead_code)]
fn get_call_count(&self) -> usize {
*self.call_count.lock().unwrap()
}
}
impl MetadataSource for MockMetadataSource {
fn id(&self) -> &str {
"mock"
}
fn name(&self) -> &str {
"Mock Metadata Source"
}
fn is_available(&self) -> Result<bool> {
*self.call_count.lock().unwrap() += 1;
if *self.should_fail.lock().unwrap() {
Ok(false)
} else {
Ok(true)
}
}
fn fetch_all(&self, _progress: &mut dyn ProgressIndicator) -> Result<Vec<JdkMetadata>> {
*self.call_count.lock().unwrap() += 1;
if *self.should_fail.lock().unwrap() {
Err(KopiError::NetworkError("Mock failure".to_string()))
} else {
Ok(self.metadata.clone())
}
}
fn fetch_distribution(
&self,
distribution: &str,
_progress: &mut dyn ProgressIndicator,
) -> Result<Vec<JdkMetadata>> {
*self.call_count.lock().unwrap() += 1;
if *self.should_fail.lock().unwrap() {
Err(KopiError::NetworkError("Mock failure".to_string()))
} else {
let results: Vec<_> = self
.metadata
.iter()
.filter(|m| m.distribution == distribution)
.cloned()
.collect();
Ok(results)
}
}
fn fetch_package_details(
&self,
package_id: &str,
_progress: &mut dyn ProgressIndicator,
) -> Result<PackageDetails> {
if *self.should_fail.lock().unwrap() {
Err(KopiError::NetworkError("Mock failure".to_string()))
} else if let Some(pkg) = self.metadata.iter().find(|m| m.id == package_id) {
Ok(PackageDetails {
download_url: pkg.download_url.clone().unwrap_or_default(),
checksum: pkg.checksum.clone(),
checksum_type: pkg.checksum_type,
})
} else {
Err(KopiError::NotFound(format!(
"Package {package_id} not found"
)))
}
}
fn last_updated(&self) -> Result<Option<chrono::DateTime<chrono::Utc>>> {
Ok(Some(chrono::Utc::now()))
}
}
#[test]
fn test_metadata_provider_basic_search() {
let metadata = vec![
JdkMetadata {
id: "temurin-21.0.1".to_string(),
distribution: "temurin".to_string(),
version: Version::new(21, 0, 1),
distribution_version: Version::new(21, 0, 1),
architecture: Architecture::X64,
operating_system: OperatingSystem::Linux,
package_type: PackageType::Jdk,
archive_type: ArchiveType::TarGz,
javafx_bundled: false,
download_url: Some("https://example.com/temurin-21.tar.gz".to_string()),
checksum: Some("abc123".to_string()),
checksum_type: Some(ChecksumType::Sha256),
size: 100_000_000,
lib_c_type: None,
term_of_support: None,
release_status: None,
latest_build_available: None,
},
JdkMetadata {
id: "zulu-21.0.2".to_string(),
distribution: "zulu".to_string(),
version: Version::new(21, 0, 2),
distribution_version: Version::new(21, 0, 2),
architecture: Architecture::X64,
operating_system: OperatingSystem::Linux,
package_type: PackageType::Jdk,
archive_type: ArchiveType::TarGz,
javafx_bundled: false,
download_url: Some("https://example.com/zulu-21.tar.gz".to_string()),
checksum: Some("def456".to_string()),
checksum_type: Some(ChecksumType::Sha256),
size: 100_000_000,
lib_c_type: None,
term_of_support: None,
release_status: None,
latest_build_available: None,
},
];
let source = MockMetadataSource::new(metadata);
let provider = MetadataProvider::new_with_source(Box::new(source));
let mut progress = SilentProgress;
let results = provider.fetch_all(&mut progress).unwrap();
assert_eq!(results.len(), 2);
let mut progress = SilentProgress;
let results = provider
.fetch_distribution("temurin", &mut progress)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].distribution, "temurin");
}
#[test]
fn test_metadata_provider_fallback() {
let primary_metadata = vec![JdkMetadata {
id: "primary-21.0.1".to_string(),
distribution: "primary".to_string(),
version: Version::new(21, 0, 1),
distribution_version: Version::new(21, 0, 1),
architecture: Architecture::X64,
operating_system: OperatingSystem::Linux,
package_type: PackageType::Jdk,
archive_type: ArchiveType::TarGz,
javafx_bundled: false,
download_url: Some("https://example.com/primary.tar.gz".to_string()),
checksum: Some("primary123".to_string()),
checksum_type: Some(ChecksumType::Sha256),
size: 100_000_000,
lib_c_type: None,
term_of_support: None,
release_status: None,
latest_build_available: None,
}];
let fallback_metadata = vec![JdkMetadata {
id: "fallback-21.0.1".to_string(),
distribution: "fallback".to_string(),
version: Version::new(21, 0, 1),
distribution_version: Version::new(21, 0, 1),
architecture: Architecture::X64,
operating_system: OperatingSystem::Linux,
package_type: PackageType::Jdk,
archive_type: ArchiveType::TarGz,
javafx_bundled: false,
download_url: Some("https://example.com/fallback.tar.gz".to_string()),
checksum: Some("fallback456".to_string()),
checksum_type: Some(ChecksumType::Sha256),
size: 100_000_000,
lib_c_type: None,
term_of_support: None,
release_status: None,
latest_build_available: None,
}];
let _primary = Arc::new(MockMetadataSource::new(primary_metadata));
let _fallback = Arc::new(MockMetadataSource::new(fallback_metadata));
let temp_dir = TempDir::new().unwrap();
let _config = KopiConfig::new(temp_dir.path().to_path_buf()).unwrap();
}
#[test]
fn test_local_directory_integration() {
let temp_dir = TempDir::new().unwrap();
let metadata_dir = temp_dir.path().join("metadata");
std::fs::create_dir(&metadata_dir).unwrap();
use kopi::platform::{get_current_architecture, get_current_os, get_foojay_libc_type};
let platform_dir = format!(
"{}-{}-{}",
get_current_os(),
get_current_architecture(),
get_foojay_libc_type()
);
let file_path = format!("{platform_dir}/metadata.json");
let index = serde_json::json!({
"version": 1,
"updated": chrono::Utc::now().to_rfc3339(),
"files": [
{
"path": file_path,
"distribution": "mixed",
"operating_systems": ["linux"],
"architectures": ["x64"],
"size": 1000
}
]
});
std::fs::write(
metadata_dir.join("index.json"),
serde_json::to_string_pretty(&index).unwrap(),
)
.unwrap();
let platform_path = metadata_dir.join(&platform_dir);
std::fs::create_dir(&platform_path).unwrap();
let all_metadata = serde_json::json!([
{
"id": "temurin-21.0.1-linux-x64",
"distribution": "temurin",
"version": {"components": [21, 0, 1], "build": null, "pre_release": null},
"distribution_version": {"components": [21, 0, 1], "build": null, "pre_release": null},
"architecture": "x64",
"operating_system": "linux",
"package_type": "jdk",
"archive_type": "targz",
"download_url": "https://example.com/temurin-21.0.1-linux-x64.tar.gz",
"checksum": "abcdef123456",
"checksum_type": "sha256",
"size": 100000000,
"javafx_bundled": false
},
{
"id": "temurin-17.0.9-linux-x64",
"distribution": "temurin",
"version": {"components": [17, 0, 9], "build": null, "pre_release": null},
"distribution_version": {"components": [17, 0, 9], "build": null, "pre_release": null},
"architecture": "x64",
"operating_system": "linux",
"package_type": "jdk",
"archive_type": "targz",
"download_url": "https://example.com/temurin-17.0.9-linux-x64.tar.gz",
"checksum": "fedcba654321",
"checksum_type": "sha256",
"size": 100000000,
"javafx_bundled": false
},
{
"id": "zulu-21.0.2-linux-x64",
"distribution": "zulu",
"version": {"components": [21, 0, 2], "build": null, "pre_release": null},
"distribution_version": {"components": [21, 0, 2], "build": null, "pre_release": null},
"architecture": "x64",
"operating_system": "linux",
"package_type": "jdk",
"archive_type": "targz",
"download_url": "https://example.com/zulu-21.0.2-linux-x64.tar.gz",
"checksum": "123abc456def",
"checksum_type": "sha256",
"size": 100000000,
"javafx_bundled": false
}
]);
std::fs::write(
platform_path.join("metadata.json"),
serde_json::to_string_pretty(&all_metadata).unwrap(),
)
.unwrap();
let local_source = LocalDirectorySource::new(metadata_dir.clone());
let provider = MetadataProvider::new_with_source(Box::new(local_source));
let mut progress = SilentProgress;
let all_metadata = provider.fetch_all(&mut progress).unwrap();
assert_eq!(all_metadata.len(), 3);
let mut progress = SilentProgress;
let temurin_results = provider
.fetch_distribution("temurin", &mut progress)
.unwrap();
assert_eq!(temurin_results.len(), 2);
let mut progress = SilentProgress;
let all_results = provider.fetch_all(&mut progress).unwrap();
let v21_results: Vec<_> = all_results
.into_iter()
.filter(|m| m.version.major() == 21)
.collect();
assert_eq!(v21_results.len(), 2);
}
#[test]
fn test_metadata_cache_integration() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().join(".kopi").join("cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let mut cache = MetadataCache::new();
cache.last_updated = chrono::Utc::now();
let _metadata = JdkMetadata {
id: "cached-21.0.1".to_string(),
distribution: "cached".to_string(),
version: Version::new(21, 0, 1),
distribution_version: Version::new(21, 0, 1),
architecture: Architecture::X64,
operating_system: OperatingSystem::Linux,
package_type: PackageType::Jdk,
archive_type: ArchiveType::TarGz,
javafx_bundled: false,
download_url: Some("https://example.com/cached.tar.gz".to_string()),
checksum: Some("cached123".to_string()),
checksum_type: Some(ChecksumType::Sha256),
size: 100_000_000,
lib_c_type: None,
term_of_support: None,
release_status: None,
latest_build_available: None,
};
}
#[test]
fn test_concurrent_metadata_access() {
use std::sync::atomic::{AtomicUsize, Ordering};
let metadata = vec![JdkMetadata {
id: "concurrent-21.0.1".to_string(),
distribution: "concurrent".to_string(),
version: Version::new(21, 0, 1),
distribution_version: Version::new(21, 0, 1),
architecture: Architecture::X64,
operating_system: OperatingSystem::Linux,
package_type: PackageType::Jdk,
archive_type: ArchiveType::TarGz,
javafx_bundled: false,
download_url: Some("https://example.com/concurrent.tar.gz".to_string()),
checksum: Some("concurrent123".to_string()),
checksum_type: Some(ChecksumType::Sha256),
size: 100_000_000,
lib_c_type: None,
term_of_support: None,
release_status: None,
latest_build_available: None,
}];
let source = MockMetadataSource::new(metadata);
let provider = Arc::new(MetadataProvider::new_with_source(Box::new(source)));
let success_count = Arc::new(AtomicUsize::new(0));
let thread_count = 10;
let handles: Vec<_> = (0..thread_count)
.map(|_| {
let provider = Arc::clone(&provider);
let success_count = Arc::clone(&success_count);
std::thread::spawn(move || {
for _ in 0..10 {
let mut progress = SilentProgress;
if let Ok(results) = provider.fetch_distribution("concurrent", &mut progress)
&& !results.is_empty()
{
success_count.fetch_add(1, Ordering::Relaxed);
}
std::thread::sleep(Duration::from_millis(1));
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
assert_eq!(success_count.load(Ordering::Relaxed), thread_count * 10);
}
#[test]
fn test_error_handling_and_recovery() {
let metadata = vec![JdkMetadata {
id: "recovery-21.0.1".to_string(),
distribution: "recovery".to_string(),
version: Version::new(21, 0, 1),
distribution_version: Version::new(21, 0, 1),
architecture: Architecture::X64,
operating_system: OperatingSystem::Linux,
package_type: PackageType::Jdk,
archive_type: ArchiveType::TarGz,
javafx_bundled: false,
download_url: Some("https://example.com/recovery.tar.gz".to_string()),
checksum: Some("recovery123".to_string()),
checksum_type: Some(ChecksumType::Sha256),
size: 100_000_000,
lib_c_type: None,
term_of_support: None,
release_status: None,
latest_build_available: None,
}];
let source = MockMetadataSource::new(metadata.clone());
let provider = MetadataProvider::new_with_source(Box::new(source));
let mut progress = SilentProgress;
let result = provider.fetch_all(&mut progress);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
let failing_source = MockMetadataSource::new(metadata);
failing_source.set_should_fail(true);
let failing_provider = MetadataProvider::new_with_source(Box::new(failing_source));
let mut progress = SilentProgress;
let result = failing_provider.fetch_all(&mut progress);
assert!(result.is_err());
}
#[test]
fn test_corrupt_metadata_handling() {
let temp_dir = TempDir::new().unwrap();
let metadata_dir = temp_dir.path().join("metadata");
std::fs::create_dir(&metadata_dir).unwrap();
std::fs::write(metadata_dir.join("index.json"), "{ invalid json").unwrap();
let local_source = LocalDirectorySource::new(metadata_dir.clone());
let provider = MetadataProvider::new_with_source(Box::new(local_source));
let mut progress = SilentProgress;
let result = provider.fetch_all(&mut progress);
assert!(result.is_err());
}
#[test]
fn test_provider_from_config() {
let temp_dir = TempDir::new().unwrap();
let kopi_home = temp_dir.path().to_path_buf();
std::fs::create_dir_all(&kopi_home).unwrap();
let config_content = r#"
[[metadata.sources]]
type = "local"
name = "bundled"
enabled = true
directory = "${KOPI_HOME}/metadata"
[[metadata.sources]]
type = "http"
name = "github"
enabled = true
base_url = "https://example.com/metadata"
[[metadata.sources]]
type = "foojay"
name = "foojay-api"
enabled = true
base_url = "https://api.foojay.io"
"#;
let config_path = kopi_home.join("config.toml");
std::fs::write(&config_path, config_content).unwrap();
let config = KopiConfig::new(kopi_home).unwrap();
let provider = MetadataProvider::from_config(&config).unwrap();
assert!(provider.source_count() > 0);
let sources = provider.list_sources();
assert!(sources.contains(&"bundled"));
assert!(sources.contains(&"github"));
assert!(sources.contains(&"foojay-api"));
}