platform: add mockito HTTP tests to modrinth

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I880c11195559fcfb9701e945a10fe87b6a6a6964
This commit is contained in:
raf 2026-02-13 00:14:21 +03:00
commit 0cc72e9916
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 182 additions and 49 deletions

View file

@ -198,11 +198,11 @@ impl Project {
// "mod_v1.0.0" // "mod_v1.0.0"
let version_str = name let version_str = name
.rsplit_once('-') .rsplit_once('-')
.and_then(|(_, v)| v.strip_suffix("jar")) .and_then(|(_, v)| v.strip_suffix(".jar"))
.or_else(|| { .or_else(|| {
name name
.rsplit_once('_') .rsplit_once('_')
.and_then(|(_, v)| v.strip_suffix("jar")) .and_then(|(_, v)| v.strip_suffix(".jar"))
}) })
.unwrap_or(name); .unwrap_or(name);
semver::Version::parse(version_str).ok() semver::Version::parse(version_str).ok()

View file

@ -24,6 +24,62 @@ impl ModrinthPlatform {
} }
} }
async fn request_project_url(&self, url: &str) -> Result<Project> {
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<Vec<ProjectFile>> {
let response = self.client.get(url).send().await?;
if !response.status().is_success() {
return Err(PakkerError::ProjectNotFound(url.to_string()));
}
let mr_versions: Vec<ModrinthVersion> = 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<Option<Project>> {
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 { fn map_project_type(type_str: &str) -> ProjectType {
match type_str { match type_str {
"mod" => ProjectType::Mod, "mod" => ProjectType::Mod,
@ -123,15 +179,7 @@ impl PlatformClient for ModrinthPlatform {
_loaders: &[String], _loaders: &[String],
) -> Result<Project> { ) -> Result<Project> {
let url = format!("{MODRINTH_API_BASE}/project/{identifier}"); let url = format!("{MODRINTH_API_BASE}/project/{identifier}");
self.request_project_url(&url).await
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))
} }
async fn request_project_files( async fn request_project_files(
@ -170,20 +218,7 @@ impl PlatformClient for ModrinthPlatform {
url.push_str(&params.join("&")); url.push_str(&params.join("&"));
} }
let response = self.client.get(&url).send().await?; self.request_project_files_url(&url).await
if !response.status().is_success() {
return Err(PakkerError::ProjectNotFound(project_id.to_string()));
}
let mr_versions: Vec<ModrinthVersion> = response.json().await?;
Ok(
mr_versions
.into_iter()
.map(|v| self.convert_version(v, project_id))
.collect(),
)
} }
async fn request_project_with_files( async fn request_project_with_files(
@ -213,30 +248,7 @@ impl PlatformClient for ModrinthPlatform {
async fn lookup_by_hash(&self, hash: &str) -> Result<Option<Project>> { async fn lookup_by_hash(&self, hash: &str) -> Result<Option<Project>> {
// Modrinth uses SHA-1 hash for file lookups // Modrinth uses SHA-1 hash for file lookups
let url = format!("{MODRINTH_API_BASE}/version_file/{hash}"); let url = format!("{MODRINTH_API_BASE}/version_file/{hash}");
self.lookup_by_hash_url(&url).await
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)
} }
} }
@ -280,3 +292,124 @@ struct ModrinthDependency {
project_id: Option<String>, project_id: Option<String>,
dependency_type: String, 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());
}
}