diff --git a/src/model/project.rs b/src/model/project.rs index af8b7e8..75fa917 100644 --- a/src/model/project.rs +++ b/src/model/project.rs @@ -198,11 +198,11 @@ impl Project { // "mod_v1.0.0" let version_str = name .rsplit_once('-') - .and_then(|(_, v)| v.strip_suffix("jar")) + .and_then(|(_, v)| v.strip_suffix(".jar")) .or_else(|| { name .rsplit_once('_') - .and_then(|(_, v)| v.strip_suffix("jar")) + .and_then(|(_, v)| v.strip_suffix(".jar")) }) .unwrap_or(name); semver::Version::parse(version_str).ok() diff --git a/src/platform/modrinth.rs b/src/platform/modrinth.rs index 34b3790..31a8bd2 100644 --- a/src/platform/modrinth.rs +++ b/src/platform/modrinth.rs @@ -24,6 +24,62 @@ impl ModrinthPlatform { } } + async fn request_project_url(&self, url: &str) -> Result { + let response = self.client.get(url).send().await?; + if !response.status().is_success() { + return Err(PakkerError::ProjectNotFound(url.to_string())); + } + let mr_project: ModrinthProject = response.json().await?; + Ok(self.convert_project(mr_project)) + } + + async fn request_project_files_url( + &self, + url: &str, + ) -> Result> { + let response = self.client.get(url).send().await?; + if !response.status().is_success() { + return Err(PakkerError::ProjectNotFound(url.to_string())); + } + let mr_versions: Vec = response.json().await?; + let project_id = url + .split('/') + .nth(4) + .ok_or_else(|| { + PakkerError::InvalidResponse( + "Cannot parse project ID from URL".to_string(), + ) + })? + .to_string(); + Ok( + mr_versions + .into_iter() + .map(|v| self.convert_version(v, &project_id)) + .collect(), + ) + } + + async fn lookup_by_hash_url(&self, url: &str) -> Result> { + let response = self.client.get(url).send().await?; + if response.status().as_u16() == 404 { + return Ok(None); + } + if !response.status().is_success() { + return Err(PakkerError::PlatformApiError(format!( + "Modrinth API error: {}", + response.status() + ))); + } + let version_data: serde_json::Value = response.json().await?; + let project_id = version_data["project_id"].as_str().ok_or_else(|| { + PakkerError::InvalidResponse("Missing project_id".to_string()) + })?; + self + .request_project_with_files(project_id, &[], &[]) + .await + .map(Some) + } + fn map_project_type(type_str: &str) -> ProjectType { match type_str { "mod" => ProjectType::Mod, @@ -123,15 +179,7 @@ impl PlatformClient for ModrinthPlatform { _loaders: &[String], ) -> Result { let url = format!("{MODRINTH_API_BASE}/project/{identifier}"); - - let response = self.client.get(&url).send().await?; - - if !response.status().is_success() { - return Err(PakkerError::ProjectNotFound(identifier.to_string())); - } - - let mr_project: ModrinthProject = response.json().await?; - Ok(self.convert_project(mr_project)) + self.request_project_url(&url).await } async fn request_project_files( @@ -170,20 +218,7 @@ impl PlatformClient for ModrinthPlatform { url.push_str(¶ms.join("&")); } - let response = self.client.get(&url).send().await?; - - if !response.status().is_success() { - return Err(PakkerError::ProjectNotFound(project_id.to_string())); - } - - let mr_versions: Vec = response.json().await?; - - Ok( - mr_versions - .into_iter() - .map(|v| self.convert_version(v, project_id)) - .collect(), - ) + self.request_project_files_url(&url).await } async fn request_project_with_files( @@ -213,30 +248,7 @@ impl PlatformClient for ModrinthPlatform { async fn lookup_by_hash(&self, hash: &str) -> Result> { // Modrinth uses SHA-1 hash for file lookups let url = format!("{MODRINTH_API_BASE}/version_file/{hash}"); - - let response = self.client.get(&url).send().await?; - - if response.status().as_u16() == 404 { - return Ok(None); - } - - if !response.status().is_success() { - return Err(PakkerError::PlatformApiError(format!( - "Modrinth API error: {}", - response.status() - ))); - } - - let version_data: serde_json::Value = response.json().await?; - - let project_id = version_data["project_id"].as_str().ok_or_else(|| { - PakkerError::InvalidResponse("Missing project_id".to_string()) - })?; - - self - .request_project_with_files(project_id, &[], &[]) - .await - .map(Some) + self.lookup_by_hash_url(&url).await } } @@ -280,3 +292,124 @@ struct ModrinthDependency { project_id: Option, dependency_type: String, } + +#[cfg(test)] +mod tests { + use reqwest::Client; + + use super::*; + + impl ModrinthPlatform { + fn with_client(client: Client) -> Self { + Self { client } + } + } + + async fn create_platform_with_mock() + -> (ModrinthPlatform, mockito::ServerGuard) { + let server = mockito::Server::new_async().await; + let client = Client::new(); + let platform = ModrinthPlatform::with_client(client); + (platform, server) + } + + #[tokio::test] + async fn test_request_project_success() { + let (platform, mut server) = create_platform_with_mock().await; + let url = format!("{}/project/test-mod", server.url()); + + let _mock = server + .mock("GET", "/project/test-mod") + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + r#"{ + "id": "abc123", + "slug": "test-mod", + "title": "Test Mod", + "project_type": "mod", + "client_side": "required", + "server_side": "required" + }"#, + ) + .create(); + + let result = platform.request_project_url(&url).await; + + assert!(result.is_ok()); + let project = result.unwrap(); + assert!(project.get_platform_id("modrinth").is_some()); + } + + #[tokio::test] + async fn test_request_project_not_found() { + let (platform, mut server) = create_platform_with_mock().await; + let url = format!("{}/project/nonexistent", server.url()); + + let _mock = server + .mock("GET", "/project/nonexistent") + .with_status(404) + .create(); + + let result = platform.request_project_url(&url).await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_request_project_files() { + let (platform, mut server) = create_platform_with_mock().await; + let url = format!("{}/project/abc123/version", server.url()); + + let _mock = server + .mock("GET", "/project/abc123/version") + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + r#"[ + { + "id": "v1", + "project_id": "abc123", + "name": "Test Mod v1.0.0", + "version_number": "1.0.0", + "game_versions": ["1.20.1"], + "version_type": "release", + "loaders": ["fabric"], + "date_published": "2024-01-01T00:00:00Z", + "files": [{ + "hashes": {"sha1": "abc123def456"}, + "url": "https://example.com/mod.jar", + "filename": "test-mod-1.0.0.jar", + "primary": true, + "size": 1024 + }], + "dependencies": [] + } + ]"#, + ) + .create(); + + let result = platform.request_project_files_url(&url).await; + + assert!(result.is_ok()); + let files = result.unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0].file_name, "test-mod-1.0.0.jar"); + } + + #[tokio::test] + async fn test_lookup_by_hash_not_found() { + let (platform, mut server) = create_platform_with_mock().await; + let url = format!("{}/version_file/unknownhash123", server.url()); + + let _mock = server + .mock("GET", "/version_file/unknownhash123") + .with_status(404) + .create(); + + let result = platform.lookup_by_hash_url(&url).await; + + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } +}