mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
feat: instances v2 (#6431)
* feat: base of instances v2 * feat: use old profiles with compat layer * prototype: instances v2 * fix: install_from using profile * fix: skins migration fix * fix: frontend still using profile path * fix: add update proj multiselect guard * fix: cargo fmt * fix: content missing fields * feat: break up app-lib/api/instance.rs * fix: check_content_updates mismatch * fix: updater modal cleanup w/new structure * feat: better update all handling * fix: remove preview_update_all * fix: feedback on bulk update + lint * fix: rem transitions * fix: change to jsonb * feat: app db backup after update * fix: lint * fix: sqlx prepare + use sqlx macros * fix: lint * fix: bugs * feat: defuck the installing process up * fix: bug of hell * fix: shear * fix: fmt * fix: install progress spacing + change mc/content/overrides to bytes * fix: lint * fix: prepr * fix: navtabs anim not working in app * fix: worlds.vue improvements + browse page fixes * feat: optimise queries + adapter fns * fix: lint * fix: lint * feat: shared modrinth-content-management crate (#6469) * feat: disable warnings setting * feat: add instances shortcuts (#6329) * Add modrinth://launch deep link to start a profile Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck. * Change route to /launch/profile/{id} for future extensibility * fix: ensure profile path is url decoded * fix: URL-decode profile path from deep link * fix: use urlencoding crate for URL decoding * feat: implement app instance shortcuts * feat: change windows shortcut creation to use windows api instead * feat: implement creating a shortcut launching world/server * format * fmt * fix multiline inline tables * pnpm prepr * feat: move create shortcut to last item * refactor: split up shortcuts.rs for individual platforms * refactor: turn profile launch url into url type * use string literal and add safety comment * pt2 * refactor: rename anything that's profile into instance * update mac shortcut --------- Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com> --------- Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
This commit is contained in:
co-authored by
DJCheesusReal
Truman Gao
parent
ef4044534f
commit
734720e11e
@@ -0,0 +1,575 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use modrinth_content_management::{
|
||||
ContentMetadataProvider, ContentType, Dependency, DependencyType,
|
||||
Error, ResolutionPreferences, ResolveContentRequest, SkippedReason,
|
||||
Version, resolve_content,
|
||||
};
|
||||
|
||||
const QUILT_FABRIC_API_EXCEPTION_PROJECT_ID: &str = "P7dR8mSH";
|
||||
|
||||
#[derive(Default)]
|
||||
struct MemoryProvider {
|
||||
versions: HashMap<String, Version>,
|
||||
project_versions: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContentMetadataProvider for MemoryProvider {
|
||||
async fn get_version(
|
||||
&mut self,
|
||||
version_id: &str,
|
||||
) -> Result<Option<Version>, Error> {
|
||||
Ok(self.versions.get(version_id).cloned())
|
||||
}
|
||||
|
||||
async fn get_project_versions(
|
||||
&mut self,
|
||||
project_id: &str,
|
||||
) -> Result<Vec<Version>, Error> {
|
||||
Ok(self
|
||||
.project_versions
|
||||
.get(project_id)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|id| self.versions.get(id))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryProvider {
|
||||
fn with_versions(mut self, versions: Vec<Version>) -> Self {
|
||||
for version in versions {
|
||||
self.project_versions
|
||||
.entry(version.project_id.clone())
|
||||
.or_default()
|
||||
.push(version.id.clone());
|
||||
self.versions.insert(version.id.clone(), version);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn with_project_versions(
|
||||
mut self,
|
||||
project_id: &str,
|
||||
version_ids: &[&str],
|
||||
) -> Self {
|
||||
self.project_versions.insert(
|
||||
project_id.to_string(),
|
||||
version_ids.iter().map(|id| id.to_string()).collect(),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn version(
|
||||
id: &str,
|
||||
project_id: &str,
|
||||
date: &str,
|
||||
game_versions: &[&str],
|
||||
loaders: &[&str],
|
||||
dependencies: Vec<Dependency>,
|
||||
) -> Version {
|
||||
Version {
|
||||
id: id.to_string(),
|
||||
project_id: project_id.to_string(),
|
||||
date_published: DateTime::parse_from_rfc3339(date)
|
||||
.unwrap()
|
||||
.with_timezone(&Utc),
|
||||
dependencies,
|
||||
game_versions: game_versions
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect(),
|
||||
loaders: loaders.iter().map(|v| v.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dependency(
|
||||
project_id: Option<&str>,
|
||||
version_id: Option<&str>,
|
||||
dependency_type: DependencyType,
|
||||
) -> Dependency {
|
||||
Dependency {
|
||||
version_id: version_id.map(str::to_string),
|
||||
project_id: project_id.map(str::to_string),
|
||||
file_name: None,
|
||||
dependency_type,
|
||||
}
|
||||
}
|
||||
|
||||
fn required_project_dependency(project_id: &str) -> Dependency {
|
||||
dependency(Some(project_id), None, DependencyType::Required)
|
||||
}
|
||||
|
||||
fn required_version_dependency(version_id: &str) -> Dependency {
|
||||
dependency(None, Some(version_id), DependencyType::Required)
|
||||
}
|
||||
|
||||
fn request(project_id: &str) -> ResolveContentRequest {
|
||||
ResolveContentRequest {
|
||||
project_id: project_id.to_string(),
|
||||
version_id: None,
|
||||
content_type: ContentType::Mod,
|
||||
selected: ResolutionPreferences::default(),
|
||||
target: ResolutionPreferences {
|
||||
game_versions: vec!["1.20.1".to_string()],
|
||||
loaders: vec!["fabric".to_string()],
|
||||
},
|
||||
existing_project_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_primary_version_is_used() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![version(
|
||||
"v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
)]);
|
||||
let mut request = request("p1");
|
||||
request.version_id = Some("v1".to_string());
|
||||
|
||||
let plan = resolve_content(provider, request).await.unwrap();
|
||||
|
||||
assert_eq!(plan.primary.version_id, "v1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn newest_matching_primary_version_is_selected() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![
|
||||
version(
|
||||
"old",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
version(
|
||||
"new",
|
||||
"p1",
|
||||
"2024-02-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
|
||||
let plan = resolve_content(provider, request("p1")).await.unwrap();
|
||||
|
||||
assert_eq!(plan.primary.version_id, "new");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn project_only_dependency_selects_matching_version() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![
|
||||
version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![required_project_dependency("dep")],
|
||||
),
|
||||
version(
|
||||
"depv1",
|
||||
"dep",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
|
||||
let plan = resolve_content(provider, request("p1")).await.unwrap();
|
||||
|
||||
assert_eq!(plan.dependencies[0].project_id, "dep");
|
||||
assert_eq!(
|
||||
plan.dependencies[0].dependent_on_version_id.as_deref(),
|
||||
Some("p1v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exact_version_dependency_is_used_even_when_target_mismatches() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![
|
||||
version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![required_version_dependency("depv1")],
|
||||
),
|
||||
version(
|
||||
"depv1",
|
||||
"dep",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.19.4"],
|
||||
&["forge"],
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
|
||||
let plan = resolve_content(provider, request("p1")).await.unwrap();
|
||||
|
||||
assert_eq!(plan.dependencies[0].version_id, "depv1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn already_installed_dependencies_are_skipped() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![
|
||||
version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![required_project_dependency("dep")],
|
||||
),
|
||||
version(
|
||||
"depv1",
|
||||
"dep",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
let mut request = request("p1");
|
||||
request.existing_project_ids = vec!["dep".to_string()];
|
||||
|
||||
let plan = resolve_content(provider, request).await.unwrap();
|
||||
|
||||
assert!(plan.dependencies.is_empty());
|
||||
assert_eq!(plan.skipped[0].reason, SkippedReason::AlreadyInstalled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn duplicate_dependency_projects_are_skipped() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![
|
||||
version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![
|
||||
required_project_dependency("dep"),
|
||||
required_project_dependency("dep"),
|
||||
],
|
||||
),
|
||||
version(
|
||||
"depv1",
|
||||
"dep",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
|
||||
let plan = resolve_content(provider, request("p1")).await.unwrap();
|
||||
|
||||
assert_eq!(plan.dependencies.len(), 1);
|
||||
assert_eq!(plan.skipped[0].reason, SkippedReason::DuplicateProject);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conflicting_dependency_versions_are_skipped() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![
|
||||
version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![
|
||||
dependency(
|
||||
Some("dep"),
|
||||
Some("depv1"),
|
||||
DependencyType::Required,
|
||||
),
|
||||
dependency(
|
||||
Some("dep"),
|
||||
Some("depv2"),
|
||||
DependencyType::Required,
|
||||
),
|
||||
],
|
||||
),
|
||||
version(
|
||||
"depv1",
|
||||
"dep",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
version(
|
||||
"depv2",
|
||||
"dep",
|
||||
"2024-02-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
|
||||
let plan = resolve_content(provider, request("p1")).await.unwrap();
|
||||
|
||||
assert_eq!(plan.dependencies.len(), 1);
|
||||
assert_eq!(
|
||||
plan.skipped[0].reason,
|
||||
SkippedReason::ConflictingDependency
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn quilt_instances_skip_fabric_api_dependency() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["quilt"],
|
||||
vec![required_project_dependency(
|
||||
QUILT_FABRIC_API_EXCEPTION_PROJECT_ID,
|
||||
)],
|
||||
)]);
|
||||
let mut request = request("p1");
|
||||
request.target.loaders = vec!["quilt".to_string()];
|
||||
|
||||
let plan = resolve_content(provider, request).await.unwrap();
|
||||
|
||||
assert!(plan.dependencies.is_empty());
|
||||
assert_eq!(plan.skipped[0].reason, SkippedReason::QuiltFabricApi);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mods_can_fall_back_to_datapack_versions() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["datapack"],
|
||||
vec![],
|
||||
)]);
|
||||
|
||||
let plan = resolve_content(provider, request("p1")).await.unwrap();
|
||||
|
||||
assert_eq!(plan.primary.version_id, "p1v1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn neoforge_matches_neo_loader_alias() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["neo"],
|
||||
vec![],
|
||||
)]);
|
||||
let mut request = request("p1");
|
||||
request.target.loaders = vec!["neoforge".to_string()];
|
||||
|
||||
let plan = resolve_content(provider, request).await.unwrap();
|
||||
|
||||
assert_eq!(plan.primary.version_id, "p1v1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn paper_matches_bukkit_loader_alias() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![version(
|
||||
"p1v1",
|
||||
"p1",
|
||||
"2024-01-01T00:00:00Z",
|
||||
&["1.20.1"],
|
||||
&["bukkit"],
|
||||
vec![],
|
||||
)]);
|
||||
let mut request = request("p1");
|
||||
request.content_type = ContentType::Plugin;
|
||||
request.target.loaders = vec!["paper".to_string()];
|
||||
|
||||
let plan = resolve_content(provider, request).await.unwrap();
|
||||
|
||||
assert_eq!(plan.primary.version_id, "p1v1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn modpack_embedded_dependencies_are_not_installed() {
|
||||
let provider = MemoryProvider::default()
|
||||
.with_versions(vec![version(
|
||||
"Ur9uoHH5",
|
||||
"shFhR8Vx",
|
||||
"2025-01-19T06:39:41.349487Z",
|
||||
&["1.20.1"],
|
||||
&["fabric"],
|
||||
vec![
|
||||
dependency(
|
||||
Some("embedded-a"),
|
||||
None,
|
||||
DependencyType::Embedded,
|
||||
),
|
||||
dependency(
|
||||
Some("embedded-b"),
|
||||
None,
|
||||
DependencyType::Embedded,
|
||||
),
|
||||
dependency(
|
||||
Some("embedded-c"),
|
||||
None,
|
||||
DependencyType::Embedded,
|
||||
),
|
||||
],
|
||||
)])
|
||||
.with_project_versions("better-mc-fabric-bmc2", &["Ur9uoHH5"]);
|
||||
|
||||
let plan = resolve_content(
|
||||
provider,
|
||||
ResolveContentRequest {
|
||||
project_id: "better-mc-fabric-bmc2".to_string(),
|
||||
version_id: None,
|
||||
content_type: ContentType::ModPack,
|
||||
selected: ResolutionPreferences::default(),
|
||||
target: ResolutionPreferences::default(),
|
||||
existing_project_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(plan.primary.project_id, "shFhR8Vx");
|
||||
assert_eq!(plan.primary.version_id, "Ur9uoHH5");
|
||||
assert!(plan.dependencies.is_empty());
|
||||
assert!(plan.skipped.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn required_dependencies_resolve_transitively() {
|
||||
let provider = MemoryProvider::default().with_versions(vec![
|
||||
version(
|
||||
"pKFEfjEB",
|
||||
"DdAlVT8M",
|
||||
"2026-06-07T21:21:24.772638Z",
|
||||
&["1.21.1", "1.21.2"],
|
||||
&["neoforge"],
|
||||
vec![
|
||||
dependency(
|
||||
Some("LNytGWDc"),
|
||||
Some("UjX6dr61"),
|
||||
DependencyType::Required,
|
||||
),
|
||||
dependency(
|
||||
Some("oWaK0Q19"),
|
||||
Some("YhZLrAFC"),
|
||||
DependencyType::Required,
|
||||
),
|
||||
],
|
||||
),
|
||||
version(
|
||||
"UjX6dr61",
|
||||
"LNytGWDc",
|
||||
"2026-04-21T22:20:03.579201Z",
|
||||
&["1.21.1"],
|
||||
&["neoforge"],
|
||||
vec![],
|
||||
),
|
||||
version(
|
||||
"YhZLrAFC",
|
||||
"oWaK0Q19",
|
||||
"2026-05-21T22:20:03.579201Z",
|
||||
&["1.21.1"],
|
||||
&["neoforge"],
|
||||
vec![
|
||||
dependency(
|
||||
Some("T9PomCSv"),
|
||||
None,
|
||||
DependencyType::Required,
|
||||
),
|
||||
dependency(
|
||||
Some("LNytGWDc"),
|
||||
Some("UjX6dr61"),
|
||||
DependencyType::Required,
|
||||
),
|
||||
],
|
||||
),
|
||||
version(
|
||||
"hyQUls27",
|
||||
"T9PomCSv",
|
||||
"2026-06-17T04:32:50.469591Z",
|
||||
&["1.21.1"],
|
||||
&["fabric"],
|
||||
vec![],
|
||||
),
|
||||
version(
|
||||
"1L6XJqnY",
|
||||
"T9PomCSv",
|
||||
"2026-06-17T04:32:49.279302Z",
|
||||
&["1.21.1"],
|
||||
&["neoforge"],
|
||||
vec![],
|
||||
),
|
||||
]);
|
||||
|
||||
let plan = resolve_content(
|
||||
provider,
|
||||
ResolveContentRequest {
|
||||
project_id: "DdAlVT8M".to_string(),
|
||||
version_id: Some("pKFEfjEB".to_string()),
|
||||
content_type: ContentType::Mod,
|
||||
selected: ResolutionPreferences::default(),
|
||||
target: ResolutionPreferences {
|
||||
game_versions: vec![
|
||||
"1.21.1".to_string(),
|
||||
"1.21.2".to_string(),
|
||||
],
|
||||
loaders: vec!["neoforge".to_string()],
|
||||
},
|
||||
existing_project_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(plan.primary.project_id, "DdAlVT8M");
|
||||
assert_eq!(plan.primary.version_id, "pKFEfjEB");
|
||||
assert_eq!(plan.dependencies.len(), 3);
|
||||
assert!(plan.dependencies.iter().any(|dependency| {
|
||||
dependency.project_id == "LNytGWDc"
|
||||
&& dependency.version_id == "UjX6dr61"
|
||||
&& dependency.dependent_on_version_id.as_deref()
|
||||
== Some("pKFEfjEB")
|
||||
}));
|
||||
assert!(plan.dependencies.iter().any(|dependency| {
|
||||
dependency.project_id == "oWaK0Q19"
|
||||
&& dependency.version_id == "YhZLrAFC"
|
||||
&& dependency.dependent_on_version_id.as_deref()
|
||||
== Some("pKFEfjEB")
|
||||
}));
|
||||
assert!(plan.dependencies.iter().any(|dependency| {
|
||||
dependency.project_id == "T9PomCSv"
|
||||
&& dependency.version_id == "1L6XJqnY"
|
||||
&& dependency.dependent_on_version_id.as_deref()
|
||||
== Some("YhZLrAFC")
|
||||
}));
|
||||
assert_eq!(plan.skipped.len(), 1);
|
||||
assert_eq!(
|
||||
plan.skipped[0].reason,
|
||||
modrinth_content_management::SkippedReason::DuplicateProject
|
||||
);
|
||||
assert_eq!(plan.skipped[0].project_id, "LNytGWDc");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user